@triflux/core 10.0.2 → 10.1.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/error-context.mjs +35 -7
- package/hooks/hook-orchestrator.mjs +14 -0
- package/hooks/hook-registry.json +256 -246
- package/hooks/safety-guard.mjs +70 -1
- package/hub/assign-callbacks.mjs +4 -4
- package/hub/bridge.mjs +1 -1
- package/hub/codex-adapter.mjs +1 -1
- package/hub/codex-compat.mjs +1 -0
- package/hub/fullcycle.mjs +1 -0
- package/hub/intent.mjs +4 -1
- package/hub/lib/memory-store.mjs +22 -0
- package/hub/quality/deslop.mjs +1 -1
- package/hub/reflexion.mjs +40 -56
- package/hub/research.mjs +1 -0
- package/hub/router.mjs +1 -1
- package/hub/session-fingerprint.mjs +2 -2
- package/hub/token-mode.mjs +4 -1
- package/package.json +1 -1
- package/scripts/lib/mcp-filter.mjs +2 -2
- package/scripts/lib/mcp-guard-engine.mjs +1 -1
- package/scripts/lib/psmux-info.mjs +2 -2
- package/scripts/lib/skill-template.mjs +4 -4
package/hooks/error-context.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// 도구 실패 시 에러 패턴을 분석하여 해결 힌트를 additionalContext로 주입한다.
|
|
5
5
|
// Claude가 동일 에러를 반복하지 않도록 구체적 가이드를 제공.
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
8
9
|
|
|
9
10
|
// ── 에러 패턴 → 해결 힌트 매핑 ─────────────────────────────
|
|
10
11
|
const ERROR_HINTS = [
|
|
@@ -128,16 +129,43 @@ function main() {
|
|
|
128
129
|
JSON.stringify(input.tool_result || ""),
|
|
129
130
|
].join("\n");
|
|
130
131
|
|
|
132
|
+
// ── reflexion 적응형 학습: safety-guard/headless-guard 차단을 패널티로 기록 ──
|
|
133
|
+
const isSafetyBlock = /\[(?:safety-guard|headless-guard)\].*(?:BLOCKED|차단)/i.test(errorText);
|
|
134
|
+
if (isSafetyBlock) {
|
|
135
|
+
try {
|
|
136
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
137
|
+
const penaltyDir = join(home, ".triflux", "reflexion");
|
|
138
|
+
mkdirSync(penaltyDir, { recursive: true });
|
|
139
|
+
const penaltyFile = join(penaltyDir, "pending-penalties.jsonl");
|
|
140
|
+
const command = input.tool_input?.command || "";
|
|
141
|
+
const entry = {
|
|
142
|
+
ts: new Date().toISOString(),
|
|
143
|
+
type: "guard_block",
|
|
144
|
+
tool: input.tool_name || "Bash",
|
|
145
|
+
error_pattern: errorText.match(/\[.*?\]\s*(.{0,120})/)?.[1] || errorText.slice(0, 120),
|
|
146
|
+
command_preview: command.slice(0, 200),
|
|
147
|
+
source: errorText.includes("safety-guard") ? "safety-guard" : "headless-guard",
|
|
148
|
+
};
|
|
149
|
+
writeFileSync(penaltyFile, JSON.stringify(entry) + "\n", { flag: "a" });
|
|
150
|
+
} catch { /* reflexion 기록 실패는 무시 — 힌트 출력에 영향 주지 않음 */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
const hints = findHints(errorText);
|
|
132
|
-
|
|
154
|
+
// safety-guard 차단에는 guard가 이미 구체적 안내를 제공하므로 추가 힌트 불필요
|
|
155
|
+
if (hints.length === 0 && !isSafetyBlock) process.exit(0);
|
|
133
156
|
|
|
134
157
|
const toolName = input.tool_name || "Unknown";
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
const parts = [];
|
|
159
|
+
if (hints.length > 0) {
|
|
160
|
+
parts.push(`[error-context] ${toolName} 실패 — 해결 힌트:\n` + hints.map((h) => ` → ${h}`).join("\n"));
|
|
161
|
+
}
|
|
162
|
+
if (isSafetyBlock) {
|
|
163
|
+
parts.push("[reflexion] 이 패턴이 적응형 학습에 기록되었습니다. 다음 세션에서 동일 패턴 시 사전 차단됩니다.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (parts.length === 0) process.exit(0);
|
|
140
167
|
|
|
168
|
+
const output = { systemMessage: parts.join("\n") };
|
|
141
169
|
process.stdout.write(JSON.stringify(output));
|
|
142
170
|
}
|
|
143
171
|
|
|
@@ -340,6 +340,20 @@ async function main() {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
// ── PostToolUse:Skill 완료 시 라우팅 가중치 기록 ──
|
|
344
|
+
if (eventName === "PostToolUse" && toolName === "Skill" && !blocked) {
|
|
345
|
+
try {
|
|
346
|
+
const input = JSON.parse(stdinRaw);
|
|
347
|
+
const skillName = input.tool_input?.skill || "";
|
|
348
|
+
if (skillName && skillName.startsWith("tfx-")) {
|
|
349
|
+
const mode = skillName.replace(/^tfx-/, "");
|
|
350
|
+
const gitRoot = process.env.GIT_WORK_TREE || process.cwd();
|
|
351
|
+
const slug = gitRoot.split(/[\\/]/).pop() || "unknown";
|
|
352
|
+
recordRouteOutcome(slug, mode, "completion");
|
|
353
|
+
}
|
|
354
|
+
} catch { /* 가중치 기록 실패 무시 */ }
|
|
355
|
+
}
|
|
356
|
+
|
|
343
357
|
// 결과 출력
|
|
344
358
|
if (blocked) {
|
|
345
359
|
process.exit(2);
|
package/hooks/hook-registry.json
CHANGED
|
@@ -1,246 +1,256 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "hook-registry-schema",
|
|
3
|
-
"version": 1,
|
|
4
|
-
"description": "훅 우선순위 레지스트리 — 오케스트레이터가 이 파일을 읽어 순차 실행한다. priority 낮을수록 먼저 실행.",
|
|
5
|
-
"defaults": {
|
|
6
|
-
"triflux_priority": 0,
|
|
7
|
-
"omc_priority": 50,
|
|
8
|
-
"external_priority": 100
|
|
9
|
-
},
|
|
10
|
-
"events": {
|
|
11
|
-
"PreToolUse": [
|
|
12
|
-
{
|
|
13
|
-
"id": "tfx-safety-guard",
|
|
14
|
-
"source": "triflux",
|
|
15
|
-
"matcher": "Bash",
|
|
16
|
-
"command": "node \"${PLUGIN_ROOT}/hooks/safety-guard.mjs\"",
|
|
17
|
-
"priority": 0,
|
|
18
|
-
"enabled": true,
|
|
19
|
-
"timeout": 3,
|
|
20
|
-
"blocking": true,
|
|
21
|
-
"description": "위험 Bash 명령 사전 차단 (rm -rf, force push 등)"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"id": "tfx-agent-route-guard",
|
|
25
|
-
"source": "triflux",
|
|
26
|
-
"matcher": "Agent",
|
|
27
|
-
"command": "node \"${PLUGIN_ROOT}/hooks/agent-route-guard.mjs\"",
|
|
28
|
-
"priority": 0,
|
|
29
|
-
"enabled": true,
|
|
30
|
-
"timeout": 3,
|
|
31
|
-
"blocking": false,
|
|
32
|
-
"description": "서브에이전트 스폰 시 triflux 컨텍스트 주입"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"id": "tfx-cross-review-gate",
|
|
36
|
-
"source": "triflux",
|
|
37
|
-
"matcher": "Bash",
|
|
38
|
-
"command": "node \"${PLUGIN_ROOT}/scripts/cross-review-gate.mjs\"",
|
|
39
|
-
"priority": 1,
|
|
40
|
-
"enabled": true,
|
|
41
|
-
"timeout": 3,
|
|
42
|
-
"blocking": true,
|
|
43
|
-
"description": "git commit 전 교차 리뷰 미검증 파일 차단/경고"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"id": "omc-headless-guard",
|
|
47
|
-
"source": "omc",
|
|
48
|
-
"matcher": "Bash|Agent",
|
|
49
|
-
"command": "bash \"${HOME}/.claude/scripts/headless-guard-fast.sh\"",
|
|
50
|
-
"priority": 50,
|
|
51
|
-
"enabled": true,
|
|
52
|
-
"timeout": 3,
|
|
53
|
-
"blocking": false,
|
|
54
|
-
"description": "psmux headless 가드"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"id": "omc-tfx-gate-activate",
|
|
58
|
-
"source": "omc",
|
|
59
|
-
"matcher": "Skill",
|
|
60
|
-
"command": "node \"${HOME}/.claude/scripts/tfx-gate-activate.mjs\"",
|
|
61
|
-
"priority": 50,
|
|
62
|
-
"enabled": true,
|
|
63
|
-
"timeout": 2,
|
|
64
|
-
"blocking": false,
|
|
65
|
-
"description": "tfx-multi 상태 추적 게이트"
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
1
|
+
{
|
|
2
|
+
"$schema": "hook-registry-schema",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "훅 우선순위 레지스트리 — 오케스트레이터가 이 파일을 읽어 순차 실행한다. priority 낮을수록 먼저 실행.",
|
|
5
|
+
"defaults": {
|
|
6
|
+
"triflux_priority": 0,
|
|
7
|
+
"omc_priority": 50,
|
|
8
|
+
"external_priority": 100
|
|
9
|
+
},
|
|
10
|
+
"events": {
|
|
11
|
+
"PreToolUse": [
|
|
12
|
+
{
|
|
13
|
+
"id": "tfx-safety-guard",
|
|
14
|
+
"source": "triflux",
|
|
15
|
+
"matcher": "Bash",
|
|
16
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/safety-guard.mjs\"",
|
|
17
|
+
"priority": 0,
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"timeout": 3,
|
|
20
|
+
"blocking": true,
|
|
21
|
+
"description": "위험 Bash 명령 사전 차단 (rm -rf, force push 등)"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "tfx-agent-route-guard",
|
|
25
|
+
"source": "triflux",
|
|
26
|
+
"matcher": "Agent",
|
|
27
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/agent-route-guard.mjs\"",
|
|
28
|
+
"priority": 0,
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"timeout": 3,
|
|
31
|
+
"blocking": false,
|
|
32
|
+
"description": "서브에이전트 스폰 시 triflux 컨텍스트 주입"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "tfx-cross-review-gate",
|
|
36
|
+
"source": "triflux",
|
|
37
|
+
"matcher": "Bash",
|
|
38
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/cross-review-gate.mjs\"",
|
|
39
|
+
"priority": 1,
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"timeout": 3,
|
|
42
|
+
"blocking": true,
|
|
43
|
+
"description": "git commit 전 교차 리뷰 미검증 파일 차단/경고"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "omc-headless-guard",
|
|
47
|
+
"source": "omc",
|
|
48
|
+
"matcher": "Bash|Agent",
|
|
49
|
+
"command": "bash \"${HOME}/.claude/scripts/headless-guard-fast.sh\"",
|
|
50
|
+
"priority": 50,
|
|
51
|
+
"enabled": true,
|
|
52
|
+
"timeout": 3,
|
|
53
|
+
"blocking": false,
|
|
54
|
+
"description": "psmux headless 가드"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "omc-tfx-gate-activate",
|
|
58
|
+
"source": "omc",
|
|
59
|
+
"matcher": "Skill",
|
|
60
|
+
"command": "node \"${HOME}/.claude/scripts/tfx-gate-activate.mjs\"",
|
|
61
|
+
"priority": 50,
|
|
62
|
+
"enabled": true,
|
|
63
|
+
"timeout": 2,
|
|
64
|
+
"blocking": false,
|
|
65
|
+
"description": "tfx-multi 상태 추적 게이트"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "tfx-write-edit-passthrough",
|
|
69
|
+
"source": "triflux",
|
|
70
|
+
"matcher": "Edit|Write",
|
|
71
|
+
"command": "exit 0",
|
|
72
|
+
"priority": 999,
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"blocking": false,
|
|
75
|
+
"description": "Edit/Write passthrough"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"PostToolUse": [
|
|
79
|
+
{
|
|
80
|
+
"id": "tfx-cross-review-tracker",
|
|
81
|
+
"source": "triflux",
|
|
82
|
+
"matcher": "Edit|Write",
|
|
83
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/cross-review-tracker.mjs\"",
|
|
84
|
+
"priority": 0,
|
|
85
|
+
"enabled": true,
|
|
86
|
+
"timeout": 3,
|
|
87
|
+
"blocking": false,
|
|
88
|
+
"description": "파일 수정 추적 → 교차 리뷰 nudge"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "tfx-mcp-config-watcher",
|
|
92
|
+
"source": "triflux",
|
|
93
|
+
"matcher": "Edit|Write",
|
|
94
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/mcp-config-watcher.mjs\"",
|
|
95
|
+
"priority": 1,
|
|
96
|
+
"enabled": true,
|
|
97
|
+
"timeout": 3,
|
|
98
|
+
"blocking": false,
|
|
99
|
+
"description": "감시 대상 MCP 설정 변경 감지 → stdio MCP 자동 치환"
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"PostToolUseFailure": [
|
|
103
|
+
{
|
|
104
|
+
"id": "tfx-error-context",
|
|
105
|
+
"source": "triflux",
|
|
106
|
+
"matcher": "*",
|
|
107
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/error-context.mjs\"",
|
|
108
|
+
"priority": 0,
|
|
109
|
+
"enabled": true,
|
|
110
|
+
"timeout": 3,
|
|
111
|
+
"blocking": false,
|
|
112
|
+
"description": "도구 실패 시 에러 힌트 자동 주입"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "tfx-adaptive-collector",
|
|
116
|
+
"source": "triflux",
|
|
117
|
+
"matcher": "*",
|
|
118
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/hook-adaptive-collector.mjs\"",
|
|
119
|
+
"priority": 1,
|
|
120
|
+
"enabled": true,
|
|
121
|
+
"timeout": 3,
|
|
122
|
+
"blocking": false,
|
|
123
|
+
"description": "에러 패턴 수집 → Neural Memory adaptive engine 학습"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"UserPromptSubmit": [
|
|
127
|
+
{
|
|
128
|
+
"id": "tfx-keyword-detector",
|
|
129
|
+
"source": "triflux",
|
|
130
|
+
"matcher": "*",
|
|
131
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/run.cjs\" \"${PLUGIN_ROOT}/scripts/keyword-detector.mjs\"",
|
|
132
|
+
"priority": 0,
|
|
133
|
+
"enabled": true,
|
|
134
|
+
"timeout": 5,
|
|
135
|
+
"blocking": false,
|
|
136
|
+
"description": "키워드 매칭 → 스킬 자동 라우팅"
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"SessionStart": [
|
|
140
|
+
{
|
|
141
|
+
"id": "tfx-setup",
|
|
142
|
+
"source": "triflux",
|
|
143
|
+
"matcher": "*",
|
|
144
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/setup.mjs\"",
|
|
145
|
+
"priority": 0,
|
|
146
|
+
"enabled": true,
|
|
147
|
+
"timeout": 10,
|
|
148
|
+
"blocking": false,
|
|
149
|
+
"description": "triflux 환경 초기화"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "tfx-mcp-safety-guard",
|
|
153
|
+
"source": "triflux",
|
|
154
|
+
"matcher": "*",
|
|
155
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/mcp-safety-guard.mjs\"",
|
|
156
|
+
"priority": 1,
|
|
157
|
+
"enabled": true,
|
|
158
|
+
"timeout": 3,
|
|
159
|
+
"blocking": false,
|
|
160
|
+
"description": "Gemini stdio MCP 자동 감지 + 제거 (spawn EPERM 방지)"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"id": "tfx-hub-ensure",
|
|
164
|
+
"source": "triflux",
|
|
165
|
+
"matcher": "*",
|
|
166
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/hub-ensure.mjs\"",
|
|
167
|
+
"priority": 2,
|
|
168
|
+
"enabled": true,
|
|
169
|
+
"timeout": 8,
|
|
170
|
+
"blocking": false,
|
|
171
|
+
"description": "tfx-hub 서비스 헬스체크 및 시작"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "tfx-preflight-cache",
|
|
175
|
+
"source": "triflux",
|
|
176
|
+
"matcher": "*",
|
|
177
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/preflight-cache.mjs\"",
|
|
178
|
+
"priority": 3,
|
|
179
|
+
"enabled": true,
|
|
180
|
+
"timeout": 5,
|
|
181
|
+
"blocking": false,
|
|
182
|
+
"description": "CLI/Hub 가용성 캐시 (hub-ensure 완료 후 실행)"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"id": "tfx-mcp-gateway-ensure",
|
|
186
|
+
"source": "triflux",
|
|
187
|
+
"matcher": "*",
|
|
188
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/mcp-gateway-ensure.mjs\"",
|
|
189
|
+
"priority": 4,
|
|
190
|
+
"enabled": true,
|
|
191
|
+
"timeout": 8,
|
|
192
|
+
"blocking": false,
|
|
193
|
+
"description": "supergateway MCP 서비스 헬스체크 및 자동 기동"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"id": "ext-session-vault-start",
|
|
197
|
+
"source": "session-vault",
|
|
198
|
+
"matcher": "*",
|
|
199
|
+
"command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/start_hook.sh\"",
|
|
200
|
+
"priority": 100,
|
|
201
|
+
"enabled": true,
|
|
202
|
+
"timeout": 10,
|
|
203
|
+
"blocking": false,
|
|
204
|
+
"description": "세션 볼트 로깅 시작"
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"Stop": [
|
|
208
|
+
{
|
|
209
|
+
"id": "tfx-pipeline-stop",
|
|
210
|
+
"source": "triflux",
|
|
211
|
+
"matcher": "*",
|
|
212
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/pipeline-stop.mjs\"",
|
|
213
|
+
"priority": 0,
|
|
214
|
+
"enabled": true,
|
|
215
|
+
"timeout": 5,
|
|
216
|
+
"blocking": true,
|
|
217
|
+
"description": "활성 파이프라인 감지 → 종료 차단"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": "ext-session-vault-export",
|
|
221
|
+
"source": "session-vault",
|
|
222
|
+
"matcher": "*",
|
|
223
|
+
"command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/export_hook.sh\"",
|
|
224
|
+
"priority": 100,
|
|
225
|
+
"enabled": true,
|
|
226
|
+
"timeout": 30,
|
|
227
|
+
"blocking": false,
|
|
228
|
+
"description": "세션 트랜스크립트 내보내기"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"id": "ext-mcp-cleanup",
|
|
232
|
+
"source": "system",
|
|
233
|
+
"matcher": "*",
|
|
234
|
+
"command": "powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"${HOME}/.claude/scripts/mcp-cleanup.ps1\"",
|
|
235
|
+
"priority": 100,
|
|
236
|
+
"enabled": true,
|
|
237
|
+
"timeout": 8,
|
|
238
|
+
"blocking": false,
|
|
239
|
+
"description": "MCP 고아 프로세스 정리"
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
"SubagentStop": [
|
|
243
|
+
{
|
|
244
|
+
"id": "tfx-subagent-verifier",
|
|
245
|
+
"source": "triflux",
|
|
246
|
+
"matcher": "*",
|
|
247
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/subagent-verifier.mjs\"",
|
|
248
|
+
"priority": 0,
|
|
249
|
+
"enabled": true,
|
|
250
|
+
"timeout": 3,
|
|
251
|
+
"blocking": false,
|
|
252
|
+
"description": "서브에이전트 결과 품질 체크"
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
}
|
package/hooks/safety-guard.mjs
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// BLOCK (exit 2) — 복구 불가능한 파괴적 명령
|
|
9
9
|
// WARN (allow + context) — 주의가 필요한 명령
|
|
10
10
|
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
11
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
12
13
|
|
|
13
14
|
// ── 차단 규칙 ──────────────────────────────────────────────
|
|
14
15
|
const BLOCK_RULES = [
|
|
@@ -38,6 +39,21 @@ const WT_DIRECT_PATTERNS = [
|
|
|
38
39
|
const WT_DIRECT_BLOCK_MESSAGE =
|
|
39
40
|
"[safety-guard] wt.exe 직접 호출 차단됨. → hub/team/wt-manager.mjs의 createTab() / splitPane() / applySplitLayout()을 사용하세요.";
|
|
40
41
|
|
|
42
|
+
// ── SSH+PowerShell bash 문법 차단 ────────────────────────────
|
|
43
|
+
// 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
|
|
44
|
+
const BASH_SYNTAX_IN_SSH = [
|
|
45
|
+
/2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
|
|
46
|
+
/>\s*\/dev\/null/, // >/dev/null
|
|
47
|
+
/&>\s*\/dev\/null/, // &>/dev/null
|
|
48
|
+
/\$\(/, // $(cmd) → PowerShell에서 다른 의미
|
|
49
|
+
/\bsource\s+/, // source → PowerShell에 없음
|
|
50
|
+
/\bexport\s+\w+=/, // export VAR= → PowerShell에 없음
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const SSH_POWERSHELL_HINT =
|
|
54
|
+
"원격 셸이 PowerShell입니다. bash 문법 직접 전달 금지. scp + pwsh -File 패턴 사용. " +
|
|
55
|
+
"2>/dev/null → 2>$null, $() → $(), export → $env:, source → . (dot-source)";
|
|
56
|
+
|
|
41
57
|
// ── 경고 규칙 ──────────────────────────────────────────────
|
|
42
58
|
const WARN_RULES = [
|
|
43
59
|
{ pattern: /\bgit\s+push\b(?!.*--force)/i, warn: "git push 감지. 원격 저장소에 반영됩니다." },
|
|
@@ -50,6 +66,20 @@ const WARN_RULES = [
|
|
|
50
66
|
{ pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i, warn: "curl | sh 감지. 원격 스크립트 실행 주의." },
|
|
51
67
|
];
|
|
52
68
|
|
|
69
|
+
// ── reflexion 적응형 패널티 로드 ──────────────────────────────
|
|
70
|
+
function loadReflexionPenalties() {
|
|
71
|
+
try {
|
|
72
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
73
|
+
const penaltyFile = join(home, ".triflux", "reflexion", "pending-penalties.jsonl");
|
|
74
|
+
if (!existsSync(penaltyFile)) return [];
|
|
75
|
+
return readFileSync(penaltyFile, "utf8")
|
|
76
|
+
.split("\n")
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
} catch { return []; }
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
function readStdin() {
|
|
54
84
|
try {
|
|
55
85
|
return readFileSync(0, "utf8");
|
|
@@ -127,6 +157,45 @@ function main() {
|
|
|
127
157
|
blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
|
|
128
158
|
}
|
|
129
159
|
|
|
160
|
+
// 0.1. reflexion 적응형 패널티 — 이전 세션에서 차단된 패턴 사전 경고
|
|
161
|
+
const penalties = loadReflexionPenalties();
|
|
162
|
+
if (penalties.length > 0) {
|
|
163
|
+
for (const penalty of penalties) {
|
|
164
|
+
if (penalty.error_pattern && new RegExp(penalty.error_pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 80), "i").test(command)) {
|
|
165
|
+
const output = {
|
|
166
|
+
hookSpecificOutput: {
|
|
167
|
+
hookEventName: "PreToolUse",
|
|
168
|
+
permissionDecision: "allow",
|
|
169
|
+
additionalContext:
|
|
170
|
+
`[reflexion] 이전 세션에서 차단된 패턴과 유사합니다 (${penalty.source}, ${penalty.ts?.slice(0, 10)}). ` +
|
|
171
|
+
`이전 차단 사유: ${penalty.error_pattern?.slice(0, 100)}`,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
process.stdout.write(JSON.stringify(output));
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 0.5. SSH → PowerShell 호스트에 bash 문법 전달 차단
|
|
181
|
+
// 세그먼트 시작 위치에서 ssh 명령인 경우만 (문자열/코드 안의 ssh는 무시)
|
|
182
|
+
if (hasSegmentInvocation(command, [/^\s*ssh\s+/i])) {
|
|
183
|
+
// 세그먼트 분리 후 ssh로 시작하는 세그먼트의 payload만 검사
|
|
184
|
+
const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
|
|
185
|
+
for (const seg of segments) {
|
|
186
|
+
const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
|
|
187
|
+
if (!sshMatch) continue;
|
|
188
|
+
const sshPayload = sshMatch[1];
|
|
189
|
+
const bashSyntax = BASH_SYNTAX_IN_SSH.find(p => p.test(sshPayload));
|
|
190
|
+
if (bashSyntax) {
|
|
191
|
+
blockCommand(
|
|
192
|
+
`[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
|
|
193
|
+
command,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
130
199
|
// 1. BLOCK 체크 — exit 2로 차단
|
|
131
200
|
for (const rule of BLOCK_RULES) {
|
|
132
201
|
if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
|
package/hub/assign-callbacks.mjs
CHANGED
|
@@ -34,12 +34,12 @@ function buildAssignCallbackEvent(event = {}, row = null) {
|
|
|
34
34
|
trace_id: source.trace_id || null,
|
|
35
35
|
correlation_id: source.correlation_id || null,
|
|
36
36
|
last_message_id: source.last_message_id || null,
|
|
37
|
-
result: Object.
|
|
37
|
+
result: Object.hasOwn(source, 'result')
|
|
38
38
|
? source.result
|
|
39
|
-
: (Object.
|
|
40
|
-
error: Object.
|
|
39
|
+
: (Object.hasOwn(event, 'result') ? event.result : null),
|
|
40
|
+
error: Object.hasOwn(source, 'error')
|
|
41
41
|
? source.error
|
|
42
|
-
: (Object.
|
|
42
|
+
: (Object.hasOwn(event, 'error') ? event.error : null),
|
|
43
43
|
created_at_ms: Number.isFinite(createdAtMs) ? createdAtMs : null,
|
|
44
44
|
updated_at_ms: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
|
|
45
45
|
started_at_ms: Number.isFinite(Number(source.started_at_ms)) ? Number(source.started_at_ms) : null,
|
package/hub/bridge.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export async function requestJson(path, { method = 'POST', body, timeoutMs = 500
|
|
|
96
96
|
const headers = {};
|
|
97
97
|
const token = readHubToken();
|
|
98
98
|
if (token) {
|
|
99
|
-
headers
|
|
99
|
+
headers.Authorization = `Bearer ${token}`;
|
|
100
100
|
}
|
|
101
101
|
if (body !== undefined) {
|
|
102
102
|
headers['Content-Type'] = 'application/json';
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -29,7 +29,7 @@ function inferStallMode(stdout, stderr) {
|
|
|
29
29
|
// ── Codex command building ──────────────────────────────────────
|
|
30
30
|
|
|
31
31
|
function commandWithOverrides(command, prompt, codexPath, overrides = []) {
|
|
32
|
-
|
|
32
|
+
const next = codexPath ? command.replace(/^codex\b/u, shellQuote(codexPath)) : command;
|
|
33
33
|
if (!overrides.length) return next;
|
|
34
34
|
const promptArg = JSON.stringify(prompt);
|
|
35
35
|
const flags = overrides.flatMap((value) => ['-c', shellQuote(value)]).join(' ');
|
package/hub/codex-compat.mjs
CHANGED
package/hub/fullcycle.mjs
CHANGED
package/hub/intent.mjs
CHANGED
|
@@ -54,7 +54,10 @@ function _tryCodexClassify(prompt) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* @experimental
|
|
59
|
+
* triflux 특화 의도 카테고리 (10개)
|
|
60
|
+
*/
|
|
58
61
|
export const INTENT_CATEGORIES = {
|
|
59
62
|
implement: { agent: 'executor', mcp: 'implement', effort: 'codex53_high' },
|
|
60
63
|
debug: { agent: 'debugger', mcp: 'implement', effort: 'codex53_high' },
|
package/hub/lib/memory-store.mjs
CHANGED
|
@@ -68,6 +68,9 @@ export function buildAdaptiveRuleRow({
|
|
|
68
68
|
hit_count = 1,
|
|
69
69
|
last_seen_ms,
|
|
70
70
|
created_ms,
|
|
71
|
+
error_message,
|
|
72
|
+
solution,
|
|
73
|
+
context,
|
|
71
74
|
} = {}) {
|
|
72
75
|
const identity = normalizeAdaptiveRuleIdentity(project_slug, pattern);
|
|
73
76
|
if (!identity) return null;
|
|
@@ -79,6 +82,9 @@ export function buildAdaptiveRuleRow({
|
|
|
79
82
|
hit_count: clampHitCount(hit_count, 1),
|
|
80
83
|
last_seen_ms: lastSeenAt,
|
|
81
84
|
created_ms: createdAt,
|
|
85
|
+
error_message: typeof error_message === 'string' ? error_message : null,
|
|
86
|
+
solution: typeof solution === 'string' ? solution : null,
|
|
87
|
+
context: typeof context === 'string' ? context : null,
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -741,6 +747,22 @@ export function createMemoryStore() {
|
|
|
741
747
|
}
|
|
742
748
|
return removed;
|
|
743
749
|
},
|
|
750
|
+
|
|
751
|
+
listAdaptiveRules(projectSlug) {
|
|
752
|
+
const results = [];
|
|
753
|
+
for (const rule of adaptiveRules.values()) {
|
|
754
|
+
if (!projectSlug || rule.project_slug === projectSlug) {
|
|
755
|
+
results.push(clone(rule));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return results.sort((a, b) => b.confidence - a.confidence);
|
|
759
|
+
},
|
|
760
|
+
|
|
761
|
+
deleteAdaptiveRule(projectSlug, pattern) {
|
|
762
|
+
const identity = normalizeAdaptiveRuleIdentity(projectSlug, pattern);
|
|
763
|
+
if (!identity) return false;
|
|
764
|
+
return adaptiveRules.delete(getAdaptiveRuleKey(identity.project_slug, identity.pattern));
|
|
765
|
+
},
|
|
744
766
|
};
|
|
745
767
|
|
|
746
768
|
return store;
|
package/hub/quality/deslop.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readdir, readFile, writeFile, stat } from 'node:fs/promises';
|
|
7
|
-
import { join,
|
|
7
|
+
import { join, } from 'node:path';
|
|
8
8
|
|
|
9
9
|
/** @type {ReadonlyArray<{type: string, pattern: RegExp, severity: string, autoFixable: boolean, multiline: boolean}>} */
|
|
10
10
|
export const SLOP_PATTERNS = Object.freeze([
|
package/hub/reflexion.mjs
CHANGED
|
@@ -7,6 +7,7 @@ const DEFAULT_CONFIDENCE = 0.5;
|
|
|
7
7
|
const ACTIVE_RULE_CONFIDENCE = 0.5;
|
|
8
8
|
const ADAPTIVE_PROMOTION_STEP = 0.1;
|
|
9
9
|
const ADAPTIVE_DECAY_STEP = 0.1;
|
|
10
|
+
/** @deprecated 세션 기반 decay에서 시간 기반으로 변경됨. 미사용. */
|
|
10
11
|
const ADAPTIVE_DECAY_WINDOW = 5;
|
|
11
12
|
const ADAPTIVE_DELETE_THRESHOLD = 0.3;
|
|
12
13
|
|
|
@@ -31,6 +32,7 @@ function pickString(...values) {
|
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/** @deprecated 세션 기반 decay 제거로 미사용. 세션 인식 복원 시 재활용 예정. */
|
|
34
36
|
function pickSessionCount(...values) {
|
|
35
37
|
const raw = values.find((value) => Number.isFinite(Number(value)));
|
|
36
38
|
return raw == null ? 0 : Math.max(0, Math.trunc(Number(raw)));
|
|
@@ -109,6 +111,7 @@ function normalizeSessionIds(sessionIds) {
|
|
|
109
111
|
];
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
/** @deprecated reflexion_entries 기반. adaptive_rules에서는 미사용. 세션 인식 복원 시 재활용 예정. */
|
|
112
115
|
function getAdaptiveState(rule = {}) {
|
|
113
116
|
const state = rule.adaptive_state || {};
|
|
114
117
|
const session_ids = normalizeSessionIds(state.session_ids);
|
|
@@ -125,6 +128,7 @@ function getAdaptiveState(rule = {}) {
|
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
/** @deprecated reflexion_entries 기반. adaptive_rules에서는 미사용. 세션 인식 복원 시 재활용 예정. */
|
|
128
132
|
function mergeAdaptiveState(rule, errorContext = {}) {
|
|
129
133
|
const current = getAdaptiveState(rule);
|
|
130
134
|
const sessionId = pickSessionId(errorContext);
|
|
@@ -185,6 +189,7 @@ export function normalizeError(errorMessage) {
|
|
|
185
189
|
|
|
186
190
|
/**
|
|
187
191
|
* 에러에 대한 기존 솔루션 검색
|
|
192
|
+
* @deprecated reflexion_entries 기반. adaptive_rules로 통합 예정. 현재 런타임 호출 없음.
|
|
188
193
|
* @param {object} store - createStore() 반환 객체
|
|
189
194
|
* @param {string} errorMessage - 원본 에러 메시지
|
|
190
195
|
* @param {object} [context={}] - { file, function, cli, agent }
|
|
@@ -200,6 +205,7 @@ export function lookupSolution(store, errorMessage, context = {}) {
|
|
|
200
205
|
|
|
201
206
|
/**
|
|
202
207
|
* 에러 해결 후 학습 저장
|
|
208
|
+
* @deprecated reflexion_entries 기반. adaptive_rules로 통합 예정. 현재 런타임 호출 없음.
|
|
203
209
|
* 동일 패턴이 존재하면 hit 업데이트, 없으면 새로 생성
|
|
204
210
|
* @param {object} store
|
|
205
211
|
* @param {{ error: string, solution: string, context?: object, success?: boolean }} opts
|
|
@@ -233,6 +239,7 @@ export function learnFromError(
|
|
|
233
239
|
|
|
234
240
|
/**
|
|
235
241
|
* 솔루션 적용 결과 피드백
|
|
242
|
+
* @deprecated reflexion_entries 기반. adaptive_rules로 통합 예정. 현재 런타임 호출 없음.
|
|
236
243
|
* @param {object} store
|
|
237
244
|
* @param {string} entryId
|
|
238
245
|
* @param {boolean} success
|
|
@@ -280,82 +287,59 @@ export function adaptiveRuleFromError(errorContext = {}) {
|
|
|
280
287
|
|
|
281
288
|
/**
|
|
282
289
|
* 동일 패턴이 여러 세션에서 재발하면 adaptive rule confidence를 승격
|
|
283
|
-
* @param {object} store
|
|
284
|
-
* @param {string}
|
|
285
|
-
* @param {
|
|
290
|
+
* @param {object} store — store-adapter 인스턴스 (findAdaptiveRule, updateRuleConfidence 필요)
|
|
291
|
+
* @param {string} projectSlug
|
|
292
|
+
* @param {string} pattern
|
|
286
293
|
* @returns {object|null}
|
|
287
294
|
*/
|
|
288
|
-
export function promoteRule(store,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const newSession = next.session_occurrences > current.session_occurrences;
|
|
295
|
-
const promoted =
|
|
296
|
-
newSession && next.session_occurrences >= 2
|
|
297
|
-
? clampConfidence(rule.confidence + ADAPTIVE_PROMOTION_STEP)
|
|
298
|
-
: rule.confidence;
|
|
299
|
-
return store.patchReflexion(ruleId, {
|
|
300
|
-
adaptive_state: next,
|
|
301
|
-
confidence: promoted,
|
|
302
|
-
hit_count: (rule.hit_count || 0) + 1,
|
|
303
|
-
last_hit_ms: Date.now(),
|
|
304
|
-
});
|
|
295
|
+
export function promoteRule(store, projectSlug, pattern) {
|
|
296
|
+
if (!store.findAdaptiveRule || !store.updateRuleConfidence) return null;
|
|
297
|
+
const rule = store.findAdaptiveRule(projectSlug, pattern);
|
|
298
|
+
if (!rule) return null;
|
|
299
|
+
const promoted = clampConfidence(rule.confidence + ADAPTIVE_PROMOTION_STEP);
|
|
300
|
+
return store.updateRuleConfidence(projectSlug, pattern, promoted, { hit_count_increment: 1 });
|
|
305
301
|
}
|
|
306
302
|
|
|
307
303
|
/**
|
|
308
|
-
*
|
|
309
|
-
* @param {object} store
|
|
310
|
-
* @param {number}
|
|
304
|
+
* 7일 이상 미관측된 adaptive rules의 confidence를 -0.1 감소, 임계값 이하면 삭제
|
|
305
|
+
* @param {object} store — store-adapter 인스턴스 (listAdaptiveRules, updateRuleConfidence, deleteAdaptiveRule 필요)
|
|
306
|
+
* @param {number} _sessionCount — 하위 호환용 (현재 미사용)
|
|
307
|
+
* @param {string} [projectSlug] — 특정 프로젝트만 대상. 미지정 시 전체 스캔
|
|
311
308
|
* @returns {{ updated: Array, deleted: string[] }}
|
|
312
309
|
*/
|
|
313
|
-
export function decayRules(store,
|
|
314
|
-
|
|
315
|
-
if (!store.listReflexion || !store.patchReflexion || !store.deleteReflexion) {
|
|
316
|
-
return { updated: [], deleted: [] };
|
|
317
|
-
}
|
|
310
|
+
export function decayRules(store, _sessionCount, projectSlug) {
|
|
311
|
+
if (!store.listAdaptiveRules) return { updated: [], deleted: [] };
|
|
318
312
|
const result = { updated: [], deleted: [] };
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const confidence = clampConfidence(
|
|
330
|
-
rule.confidence - decaySteps * ADAPTIVE_DECAY_STEP,
|
|
331
|
-
);
|
|
332
|
-
if (confidence <= ADAPTIVE_DELETE_THRESHOLD) {
|
|
333
|
-
if (store.deleteReflexion(rule.id)) result.deleted.push(rule.id);
|
|
313
|
+
const rules = store.listAdaptiveRules(projectSlug);
|
|
314
|
+
for (const rule of rules) {
|
|
315
|
+
// 7일 이상 미관측된 규칙만 decay 대상
|
|
316
|
+
const ageDays = (Date.now() - (rule.last_seen_ms || 0)) / (24 * 3600 * 1000);
|
|
317
|
+
if (ageDays < 7) continue;
|
|
318
|
+
const decayed = clampConfidence(rule.confidence - ADAPTIVE_DECAY_STEP);
|
|
319
|
+
if (decayed <= ADAPTIVE_DELETE_THRESHOLD) {
|
|
320
|
+
if (store.deleteAdaptiveRule(rule.project_slug, rule.pattern)) {
|
|
321
|
+
result.deleted.push(`${rule.project_slug}:${rule.pattern}`);
|
|
322
|
+
}
|
|
334
323
|
continue;
|
|
335
324
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
last_decay_session: baseline + decaySteps * ADAPTIVE_DECAY_WINDOW,
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
if (updated) result.updated.push(updated);
|
|
325
|
+
if (decayed < rule.confidence) {
|
|
326
|
+
const updated = store.updateRuleConfidence(rule.project_slug, rule.pattern, decayed, { hit_count_increment: 0 });
|
|
327
|
+
if (updated) result.updated.push(updated);
|
|
328
|
+
}
|
|
344
329
|
}
|
|
345
330
|
return result;
|
|
346
331
|
}
|
|
347
332
|
|
|
348
333
|
/**
|
|
349
334
|
* 현재 활성화된 adaptive rules 조회
|
|
350
|
-
* @param {object} store
|
|
335
|
+
* @param {object} store — store-adapter 인스턴스 (listAdaptiveRules 필요)
|
|
351
336
|
* @param {string} projectSlug
|
|
352
337
|
* @returns {Array}
|
|
353
338
|
*/
|
|
354
339
|
export function getActiveAdaptiveRules(store, projectSlug) {
|
|
355
|
-
if (!store.
|
|
356
|
-
return store
|
|
357
|
-
.
|
|
358
|
-
.filter((rule) => rule.confidence > ACTIVE_RULE_CONFIDENCE);
|
|
340
|
+
if (!store.listAdaptiveRules) return [];
|
|
341
|
+
return store.listAdaptiveRules(projectSlug)
|
|
342
|
+
.filter((rule) => rule.confidence >= ACTIVE_RULE_CONFIDENCE);
|
|
359
343
|
}
|
|
360
344
|
|
|
361
345
|
/**
|
package/hub/research.mjs
CHANGED
package/hub/router.mjs
CHANGED
|
@@ -610,7 +610,7 @@ export function createRouter(store) {
|
|
|
610
610
|
status || payload?.status,
|
|
611
611
|
mergedMetadata,
|
|
612
612
|
);
|
|
613
|
-
const nextResult = result ?? (Object.
|
|
613
|
+
const nextResult = result ?? (Object.hasOwn(payload || {}, 'result') ? payload.result : payload);
|
|
614
614
|
const nextError = error ?? payload?.error ?? null;
|
|
615
615
|
|
|
616
616
|
if (normalizedStatus === 'running') {
|
|
@@ -87,7 +87,7 @@ function collectRawPathCandidates(context = {}) {
|
|
|
87
87
|
const direct = [context.file_path, context.filePath, context.path, context.target_path, context.targetPath];
|
|
88
88
|
const fromArrays = [context.files, context.paths, context.targets]
|
|
89
89
|
.filter(Array.isArray)
|
|
90
|
-
.
|
|
90
|
+
.flat()
|
|
91
91
|
.map((entry) => (typeof entry === 'string' ? entry : entry?.path));
|
|
92
92
|
return [...direct, ...fromArrays].filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
|
|
93
93
|
}
|
|
@@ -130,7 +130,7 @@ function collectActivityTimestamps(context = {}, now = Date.now) {
|
|
|
130
130
|
const nowValue = typeof now === 'function' ? now() : now;
|
|
131
131
|
const fromList = [context.activity_timestamps, context.activityTimestamps, context.timestamps]
|
|
132
132
|
.filter(Array.isArray)
|
|
133
|
-
.
|
|
133
|
+
.flat()
|
|
134
134
|
.map(toTimestamp)
|
|
135
135
|
.filter((entry) => entry != null);
|
|
136
136
|
const singles = [context.timestamp, context.started_at, context.startedAt]
|
package/hub/token-mode.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// hub/token-mode.mjs — Token Efficiency Mode
|
|
2
2
|
// GAP 분석 P2 #7: 심볼 통신 + 약어로 30-50% 토큰 절감
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* @experimental
|
|
6
|
+
* @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>}
|
|
7
|
+
*/
|
|
5
8
|
export const COMPACT_RULES = [
|
|
6
9
|
// ── 심볼 치환 (긴 매칭 우선 정렬) ──
|
|
7
10
|
{ from: ['greater than or equal'], to: '≥', type: 'symbol' },
|
package/package.json
CHANGED
|
@@ -606,7 +606,7 @@ export function buildMcpPolicy(options = {}) {
|
|
|
606
606
|
// Phase-aware post-filter: 파이프라인 단계별 서버 제한 적용
|
|
607
607
|
const phase = options.phase;
|
|
608
608
|
const phaseOverride = phase && PHASE_OVERRIDES[phase];
|
|
609
|
-
if (phaseOverride
|
|
609
|
+
if (phaseOverride?.blockedServers) {
|
|
610
610
|
const blocked = new Set(phaseOverride.blockedServers);
|
|
611
611
|
allowedServers = allowedServers.filter((s) => !blocked.has(s));
|
|
612
612
|
}
|
|
@@ -626,7 +626,7 @@ export function buildMcpPolicy(options = {}) {
|
|
|
626
626
|
}
|
|
627
627
|
|
|
628
628
|
function shellEscape(value) {
|
|
629
|
-
return `'${String(value).replace(/'/g, `'
|
|
629
|
+
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
function shellArray(name, values) {
|
|
@@ -84,10 +84,10 @@ export function probePsmuxSupport(options = {}) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const missingCommands = PSMUX_REQUIRED_COMMANDS.filter(
|
|
87
|
-
(command) => !helpOutput
|
|
87
|
+
(command) => !helpOutput?.includes(command),
|
|
88
88
|
);
|
|
89
89
|
const missingOptionalCommands = PSMUX_OPTIONAL_COMMANDS.filter(
|
|
90
|
-
(command) => !helpOutput
|
|
90
|
+
(command) => !helpOutput?.includes(command),
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
return {
|
|
@@ -13,9 +13,9 @@ function isTruthy(value) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function normalizeVariable(context, key) {
|
|
16
|
-
if (Object.
|
|
16
|
+
if (Object.hasOwn(context, key)) return context[key];
|
|
17
17
|
const upper = key.toUpperCase();
|
|
18
|
-
if (Object.
|
|
18
|
+
if (Object.hasOwn(context, upper)) return context[upper];
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -176,7 +176,7 @@ function normalizeIncludeName(includeName) {
|
|
|
176
176
|
|
|
177
177
|
function resolveIncludeContent(includeName, options) {
|
|
178
178
|
const normalizedName = normalizeIncludeName(includeName);
|
|
179
|
-
if (Object.
|
|
179
|
+
if (Object.hasOwn(options.includes, normalizedName)) {
|
|
180
180
|
return options.includes[normalizedName];
|
|
181
181
|
}
|
|
182
182
|
if (!options.includeBaseDir) {
|
|
@@ -264,7 +264,7 @@ function readAllTemplateFiles(rootDir, currentDir = rootDir) {
|
|
|
264
264
|
|
|
265
265
|
function setPartial(partials, key, value) {
|
|
266
266
|
if (!key) return;
|
|
267
|
-
if (!Object.
|
|
267
|
+
if (!Object.hasOwn(partials, key)) {
|
|
268
268
|
partials[key] = value;
|
|
269
269
|
}
|
|
270
270
|
}
|