@triflux/core 10.0.3 → 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.
@@ -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
- if (hints.length === 0) process.exit(0);
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 output = {
136
- systemMessage:
137
- `[error-context] ${toolName} 실패 — 해결 힌트:\n` +
138
- hints.map((h) => ` → ${h}`).join("\n"),
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);
@@ -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
- "PostToolUse": [
69
- {
70
- "id": "tfx-cross-review-tracker",
71
- "source": "triflux",
72
- "matcher": "Edit|Write",
73
- "command": "node \"${PLUGIN_ROOT}/hooks/cross-review-tracker.mjs\"",
74
- "priority": 0,
75
- "enabled": true,
76
- "timeout": 3,
77
- "blocking": false,
78
- "description": "파일 수정 추적 → 교차 리뷰 nudge"
79
- },
80
- {
81
- "id": "tfx-mcp-config-watcher",
82
- "source": "triflux",
83
- "matcher": "Edit|Write",
84
- "command": "node \"${PLUGIN_ROOT}/hooks/mcp-config-watcher.mjs\"",
85
- "priority": 1,
86
- "enabled": true,
87
- "timeout": 3,
88
- "blocking": false,
89
- "description": "감시 대상 MCP 설정 변경 감지 → stdio MCP 자동 치환"
90
- }
91
- ],
92
- "PostToolUseFailure": [
93
- {
94
- "id": "tfx-error-context",
95
- "source": "triflux",
96
- "matcher": "*",
97
- "command": "node \"${PLUGIN_ROOT}/hooks/error-context.mjs\"",
98
- "priority": 0,
99
- "enabled": true,
100
- "timeout": 3,
101
- "blocking": false,
102
- "description": "도구 실패 시 에러 힌트 자동 주입"
103
- },
104
- {
105
- "id": "tfx-adaptive-collector",
106
- "source": "triflux",
107
- "matcher": "*",
108
- "command": "node \"${PLUGIN_ROOT}/hooks/hook-adaptive-collector.mjs\"",
109
- "priority": 1,
110
- "enabled": true,
111
- "timeout": 3,
112
- "blocking": false,
113
- "description": "에러 패턴 수집 → Neural Memory adaptive engine 학습"
114
- }
115
- ],
116
- "UserPromptSubmit": [
117
- {
118
- "id": "tfx-keyword-detector",
119
- "source": "triflux",
120
- "matcher": "*",
121
- "command": "node \"${PLUGIN_ROOT}/scripts/run.cjs\" \"${PLUGIN_ROOT}/scripts/keyword-detector.mjs\"",
122
- "priority": 0,
123
- "enabled": true,
124
- "timeout": 5,
125
- "blocking": false,
126
- "description": "키워드 매칭 → 스킬 자동 라우팅"
127
- }
128
- ],
129
- "SessionStart": [
130
- {
131
- "id": "tfx-setup",
132
- "source": "triflux",
133
- "matcher": "*",
134
- "command": "node \"${PLUGIN_ROOT}/scripts/setup.mjs\"",
135
- "priority": 0,
136
- "enabled": true,
137
- "timeout": 10,
138
- "blocking": false,
139
- "description": "triflux 환경 초기화"
140
- },
141
- {
142
- "id": "tfx-mcp-safety-guard",
143
- "source": "triflux",
144
- "matcher": "*",
145
- "command": "node \"${PLUGIN_ROOT}/scripts/mcp-safety-guard.mjs\"",
146
- "priority": 1,
147
- "enabled": true,
148
- "timeout": 3,
149
- "blocking": false,
150
- "description": "Gemini stdio MCP 자동 감지 + 제거 (spawn EPERM 방지)"
151
- },
152
- {
153
- "id": "tfx-hub-ensure",
154
- "source": "triflux",
155
- "matcher": "*",
156
- "command": "node \"${PLUGIN_ROOT}/scripts/hub-ensure.mjs\"",
157
- "priority": 2,
158
- "enabled": true,
159
- "timeout": 8,
160
- "blocking": false,
161
- "description": "tfx-hub 서비스 헬스체크 및 시작"
162
- },
163
- {
164
- "id": "tfx-preflight-cache",
165
- "source": "triflux",
166
- "matcher": "*",
167
- "command": "node \"${PLUGIN_ROOT}/scripts/preflight-cache.mjs\"",
168
- "priority": 3,
169
- "enabled": true,
170
- "timeout": 5,
171
- "blocking": false,
172
- "description": "CLI/Hub 가용성 캐시 (hub-ensure 완료 후 실행)"
173
- },
174
- {
175
- "id": "tfx-mcp-gateway-ensure",
176
- "source": "triflux",
177
- "matcher": "*",
178
- "command": "node \"${PLUGIN_ROOT}/scripts/mcp-gateway-ensure.mjs\"",
179
- "priority": 4,
180
- "enabled": true,
181
- "timeout": 8,
182
- "blocking": false,
183
- "description": "supergateway MCP 서비스 헬스체크 및 자동 기동"
184
- },
185
- {
186
- "id": "ext-session-vault-start",
187
- "source": "session-vault",
188
- "matcher": "*",
189
- "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/start_hook.sh\"",
190
- "priority": 100,
191
- "enabled": true,
192
- "timeout": 10,
193
- "blocking": false,
194
- "description": "세션 볼트 로깅 시작"
195
- }
196
- ],
197
- "Stop": [
198
- {
199
- "id": "tfx-pipeline-stop",
200
- "source": "triflux",
201
- "matcher": "*",
202
- "command": "node \"${PLUGIN_ROOT}/hooks/pipeline-stop.mjs\"",
203
- "priority": 0,
204
- "enabled": true,
205
- "timeout": 5,
206
- "blocking": true,
207
- "description": "활성 파이프라인 감지 → 종료 차단"
208
- },
209
- {
210
- "id": "ext-session-vault-export",
211
- "source": "session-vault",
212
- "matcher": "*",
213
- "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/export_hook.sh\"",
214
- "priority": 100,
215
- "enabled": true,
216
- "timeout": 30,
217
- "blocking": false,
218
- "description": "세션 트랜스크립트 내보내기"
219
- },
220
- {
221
- "id": "ext-mcp-cleanup",
222
- "source": "system",
223
- "matcher": "*",
224
- "command": "powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"${HOME}/.claude/scripts/mcp-cleanup.ps1\"",
225
- "priority": 100,
226
- "enabled": true,
227
- "timeout": 8,
228
- "blocking": false,
229
- "description": "MCP 고아 프로세스 정리"
230
- }
231
- ],
232
- "SubagentStop": [
233
- {
234
- "id": "tfx-subagent-verifier",
235
- "source": "triflux",
236
- "matcher": "*",
237
- "command": "node \"${PLUGIN_ROOT}/hooks/subagent-verifier.mjs\"",
238
- "priority": 0,
239
- "enabled": true,
240
- "timeout": 3,
241
- "blocking": false,
242
- "description": "서브에이전트 결과 품질 체크"
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
+ }
@@ -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;
@@ -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.prototype.hasOwnProperty.call(source, 'result')
37
+ result: Object.hasOwn(source, 'result')
38
38
  ? source.result
39
- : (Object.prototype.hasOwnProperty.call(event, 'result') ? event.result : null),
40
- error: Object.prototype.hasOwnProperty.call(source, 'error')
39
+ : (Object.hasOwn(event, 'result') ? event.result : null),
40
+ error: Object.hasOwn(source, 'error')
41
41
  ? source.error
42
- : (Object.prototype.hasOwnProperty.call(event, 'error') ? event.error : null),
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['Authorization'] = `Bearer ${token}`;
99
+ headers.Authorization = `Bearer ${token}`;
100
100
  }
101
101
  if (body !== undefined) {
102
102
  headers['Content-Type'] = 'application/json';
@@ -29,7 +29,7 @@ function inferStallMode(stdout, stderr) {
29
29
  // ── Codex command building ──────────────────────────────────────
30
30
 
31
31
  function commandWithOverrides(command, prompt, codexPath, overrides = []) {
32
- let next = codexPath ? command.replace(/^codex\b/u, shellQuote(codexPath)) : command;
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(' ');
@@ -1,5 +1,6 @@
1
1
  // hub/codex-compat.mjs — backward-compatible facade for legacy imports
2
2
 
3
+ /** @experimental */
3
4
  export {
4
5
  CODEX_MCP_EXECUTION_EXIT_CODE,
5
6
  CODEX_MCP_TRANSPORT_EXIT_CODE,
package/hub/fullcycle.mjs CHANGED
@@ -13,6 +13,7 @@ function safeResolve(baseDir, relativePath) {
13
13
  return target;
14
14
  }
15
15
 
16
+ /** @experimental */
16
17
  export function createFullcycleRunId(now = new Date()) {
17
18
  return now
18
19
  .toISOString()
package/hub/intent.mjs CHANGED
@@ -54,7 +54,10 @@ function _tryCodexClassify(prompt) {
54
54
  }
55
55
  }
56
56
 
57
- /** triflux 특화 의도 카테고리 (10개) */
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' },
@@ -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;
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { readdir, readFile, writeFile, stat } from 'node:fs/promises';
7
- import { join, relative } from 'node:path';
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} ruleId
285
- * @param {object} [errorContext={}]
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, ruleId, errorContext = {}) {
289
- const rule = store.getReflexion(ruleId);
290
- if (!rule || rule.type !== ADAPTIVE_RULE_TYPE || !store.patchReflexion)
291
- return null;
292
- const current = getAdaptiveState(rule);
293
- const next = mergeAdaptiveState(rule, errorContext);
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
- * 지정 세션 수만큼 관측되지 않은 adaptive rules confidence 감소
309
- * @param {object} store
310
- * @param {number} sessionCount
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, sessionCount) {
314
- const currentSession = pickSessionCount(sessionCount);
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
- for (const rule of store.listReflexion({ type: ADAPTIVE_RULE_TYPE })) {
320
- const state = getAdaptiveState(rule);
321
- const baseline = Math.max(
322
- state.last_seen_session,
323
- state.last_decay_session,
324
- );
325
- const decaySteps = Math.floor(
326
- (currentSession - baseline) / ADAPTIVE_DECAY_WINDOW,
327
- );
328
- if (decaySteps <= 0) continue;
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
- const updated = store.patchReflexion(rule.id, {
337
- confidence,
338
- adaptive_state: {
339
- ...state,
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.listReflexion) return [];
356
- return store
357
- .listReflexion({ type: ADAPTIVE_RULE_TYPE, projectSlug })
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
@@ -6,6 +6,7 @@ import { join, resolve } from 'node:path';
6
6
  import { TFX_REPORTS_DIR } from './paths.mjs';
7
7
 
8
8
  /**
9
+ * @experimental
9
10
  * 주제에서 검색 쿼리 3-5개를 자동 생성한다.
10
11
  * 한국어 주제 → 한국어 + 영어 혼합, 영어 주제 → 영어 쿼리.
11
12
  * @param {string} topic - 리서치 주제
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.prototype.hasOwnProperty.call(payload || {}, 'result') ? payload.result : payload);
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
- .flatMap((entry) => entry)
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
- .flatMap((entry) => entry)
133
+ .flat()
134
134
  .map(toTimestamp)
135
135
  .filter((entry) => entry != null);
136
136
  const singles = [context.timestamp, context.started_at, context.startedAt]
@@ -1,7 +1,10 @@
1
1
  // hub/token-mode.mjs — Token Efficiency Mode
2
2
  // GAP 분석 P2 #7: 심볼 통신 + 약어로 30-50% 토큰 절감
3
3
 
4
- /** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.0.3",
3
+ "version": "10.1.0",
4
4
  "description": "triflux core — CLI routing, pipeline, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -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 && phaseOverride.blockedServers) {
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) {
@@ -218,7 +218,7 @@ function getHubServerEntry(registry) {
218
218
  || entries[0];
219
219
  }
220
220
 
221
- function makeHubRuntimeConfig() {
221
+ function _makeHubRuntimeConfig() {
222
222
  return { url: resolveHubUrl() };
223
223
  }
224
224
 
@@ -84,10 +84,10 @@ export function probePsmuxSupport(options = {}) {
84
84
  }
85
85
 
86
86
  const missingCommands = PSMUX_REQUIRED_COMMANDS.filter(
87
- (command) => !helpOutput || !helpOutput.includes(command),
87
+ (command) => !helpOutput?.includes(command),
88
88
  );
89
89
  const missingOptionalCommands = PSMUX_OPTIONAL_COMMANDS.filter(
90
- (command) => !helpOutput || !helpOutput.includes(command),
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.prototype.hasOwnProperty.call(context, key)) return context[key];
16
+ if (Object.hasOwn(context, key)) return context[key];
17
17
  const upper = key.toUpperCase();
18
- if (Object.prototype.hasOwnProperty.call(context, upper)) return context[upper];
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.prototype.hasOwnProperty.call(options.includes, normalizedName)) {
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.prototype.hasOwnProperty.call(partials, key)) {
267
+ if (!Object.hasOwn(partials, key)) {
268
268
  partials[key] = value;
269
269
  }
270
270
  }