@wooojin/forgen 0.2.0 → 0.3.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/CHANGELOG.md +72 -0
- package/README.ja.md +79 -14
- package/README.ko.md +100 -14
- package/README.md +124 -17
- package/README.zh.md +79 -14
- package/agents/analyst.md +48 -4
- package/agents/architect.md +39 -4
- package/agents/code-reviewer.md +107 -77
- package/agents/critic.md +47 -4
- package/agents/debugger.md +46 -4
- package/agents/designer.md +40 -4
- package/agents/executor.md +112 -30
- package/agents/explore.md +45 -5
- package/agents/git-master.md +48 -4
- package/agents/planner.md +121 -18
- package/agents/test-engineer.md +58 -4
- package/agents/verifier.md +92 -77
- package/commands/architecture-decision.md +127 -258
- package/commands/calibrate.md +225 -0
- package/commands/code-review.md +163 -178
- package/commands/compound.md +127 -68
- package/commands/deep-interview.md +273 -0
- package/commands/docker.md +68 -178
- package/commands/forge-loop.md +215 -0
- package/commands/learn.md +231 -0
- package/commands/retro.md +215 -0
- package/commands/ship.md +277 -0
- package/dist/cli.js +26 -9
- package/dist/core/auto-compound-runner.js +14 -0
- package/dist/core/config-injector.d.ts +2 -1
- package/dist/core/config-injector.js +2 -1
- package/dist/core/dashboard.d.ts +108 -0
- package/dist/core/dashboard.js +495 -0
- package/dist/core/doctor.js +151 -21
- package/dist/core/drift-score.d.ts +49 -0
- package/dist/core/drift-score.js +87 -0
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/mcp-config.d.ts +2 -0
- package/dist/core/mcp-config.js +6 -1
- package/dist/core/paths.d.ts +6 -1
- package/dist/core/paths.js +18 -2
- package/dist/core/spawn.d.ts +3 -2
- package/dist/core/spawn.js +27 -8
- package/dist/core/types.d.ts +34 -0
- package/dist/engine/compound-export.d.ts +41 -0
- package/dist/engine/compound-export.js +169 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/compound-loop.js +18 -0
- package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
- package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
- package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
- package/dist/engine/meta-learning/extraction-tuner.js +99 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
- package/dist/engine/meta-learning/runner.d.ts +14 -0
- package/dist/engine/meta-learning/runner.js +90 -0
- package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
- package/dist/engine/meta-learning/scope-promoter.js +84 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
- package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
- package/dist/engine/meta-learning/types.d.ts +114 -0
- package/dist/engine/meta-learning/types.js +43 -0
- package/dist/engine/solution-format.d.ts +2 -2
- package/dist/engine/solution-format.js +249 -34
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-matcher.d.ts +30 -1
- package/dist/engine/solution-matcher.js +235 -45
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +15 -0
- package/dist/hooks/context-guard.js +218 -56
- package/dist/hooks/db-guard.js +2 -2
- package/dist/hooks/hook-config.d.ts +27 -1
- package/dist/hooks/hook-config.js +72 -12
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/intent-classifier.d.ts +0 -2
- package/dist/hooks/intent-classifier.js +32 -18
- package/dist/hooks/keyword-detector.js +126 -204
- package/dist/hooks/notepad-injector.js +2 -2
- package/dist/hooks/permission-handler.js +2 -2
- package/dist/hooks/post-tool-failure.js +12 -6
- package/dist/hooks/post-tool-handlers.d.ts +1 -1
- package/dist/hooks/post-tool-handlers.js +14 -11
- package/dist/hooks/post-tool-use.d.ts +11 -0
- package/dist/hooks/post-tool-use.js +184 -71
- package/dist/hooks/pre-compact.d.ts +11 -1
- package/dist/hooks/pre-compact.js +112 -37
- package/dist/hooks/pre-tool-use.js +86 -56
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/session-recovery.js +256 -236
- package/dist/hooks/shared/hook-response.d.ts +4 -4
- package/dist/hooks/shared/hook-response.js +13 -24
- package/dist/hooks/shared/hook-timing.d.ts +15 -0
- package/dist/hooks/shared/hook-timing.js +64 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +47 -16
- package/dist/hooks/slop-detector.js +3 -3
- package/dist/hooks/solution-injector.js +224 -197
- package/dist/hooks/subagent-tracker.js +2 -2
- package/dist/host/codex-adapter.d.ts +10 -0
- package/dist/host/codex-adapter.js +154 -0
- package/dist/mcp/solution-reader.d.ts +5 -5
- package/dist/mcp/solution-reader.js +34 -24
- package/dist/renderer/rule-renderer.js +9 -11
- package/dist/services/session.d.ts +19 -0
- package/dist/services/session.js +62 -0
- package/hooks/hooks.json +2 -2
- package/package.json +2 -1
- package/skills/architecture-decision/SKILL.md +113 -257
- package/skills/calibrate/SKILL.md +207 -0
- package/skills/code-review/SKILL.md +151 -178
- package/skills/compound/SKILL.md +126 -68
- package/skills/deep-interview/SKILL.md +266 -0
- package/skills/docker/SKILL.md +57 -179
- package/skills/forge-loop/SKILL.md +198 -0
- package/skills/learn/SKILL.md +216 -0
- package/skills/retro/SKILL.md +199 -0
- package/skills/ship/SKILL.md +259 -0
- package/agents/code-simplifier.md +0 -197
- package/agents/performance-reviewer.md +0 -172
- package/agents/qa-tester.md +0 -158
- package/agents/refactoring-expert.md +0 -168
- package/agents/scientist.md +0 -144
- package/agents/security-reviewer.md +0 -137
- package/agents/writer.md +0 -184
- package/commands/api-design.md +0 -268
- package/commands/ci-cd.md +0 -270
- package/commands/database.md +0 -263
- package/commands/debug-detective.md +0 -99
- package/commands/documentation.md +0 -276
- package/commands/ecomode.md +0 -51
- package/commands/frontend.md +0 -271
- package/commands/git-master.md +0 -90
- package/commands/incident-response.md +0 -292
- package/commands/migrate.md +0 -101
- package/commands/performance.md +0 -288
- package/commands/refactor.md +0 -105
- package/commands/security-review.md +0 -288
- package/commands/tdd.md +0 -183
- package/commands/testing-strategy.md +0 -265
- package/skills/api-design/SKILL.md +0 -262
- package/skills/ci-cd/SKILL.md +0 -264
- package/skills/database/SKILL.md +0 -257
- package/skills/debug-detective/SKILL.md +0 -95
- package/skills/documentation/SKILL.md +0 -270
- package/skills/ecomode/SKILL.md +0 -46
- package/skills/frontend/SKILL.md +0 -265
- package/skills/git-master/SKILL.md +0 -86
- package/skills/incident-response/SKILL.md +0 -286
- package/skills/migrate/SKILL.md +0 -96
- package/skills/performance/SKILL.md +0 -282
- package/skills/refactor/SKILL.md +0 -100
- package/skills/security-review/SKILL.md +0 -282
- package/skills/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
|
@@ -16,14 +16,18 @@ import { createLogger } from '../core/logger.js';
|
|
|
16
16
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
17
17
|
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
18
18
|
import { loadHookConfig, isHookEnabled } from './hook-config.js';
|
|
19
|
-
import { approve, approveWithContext, approveWithWarning,
|
|
19
|
+
import { approve, approveWithContext, approveWithWarning, failOpenWithTracking } from './shared/hook-response.js';
|
|
20
20
|
import { HANDOFFS_DIR, STATE_DIR } from '../core/paths.js';
|
|
21
|
+
import { recordHookTiming } from './shared/hook-timing.js';
|
|
21
22
|
const log = createLogger('context-guard');
|
|
22
23
|
const CONTEXT_STATE_PATH = path.join(STATE_DIR, 'context-guard.json');
|
|
23
24
|
// 경고 임계값: 프롬프트 50회 또는 총 문자 수 200K 이상
|
|
24
25
|
const PROMPT_WARNING_THRESHOLD = 50;
|
|
25
26
|
const CHARS_WARNING_THRESHOLD = 200_000;
|
|
26
27
|
const WARNING_COOLDOWN_MS = 10 * 60 * 1000; // 10분 쿨다운
|
|
28
|
+
// Auto-compact 임계값: 추적된 문자 120K ≈ 실제 context ~20%
|
|
29
|
+
const AUTO_COMPACT_CHARS_THRESHOLD = 120_000;
|
|
30
|
+
const AUTO_COMPACT_COOLDOWN_MS = 5 * 60 * 1000; // 5분 쿨다운
|
|
27
31
|
/** 경고 표시 여부 판정 (순수 함수) */
|
|
28
32
|
export function shouldWarn(contextPercent, thresholds = {}) {
|
|
29
33
|
const promptThreshold = thresholds.promptThreshold ?? PROMPT_WARNING_THRESHOLD;
|
|
@@ -33,6 +37,21 @@ export function shouldWarn(contextPercent, thresholds = {}) {
|
|
|
33
37
|
return ((contextPercent.promptCount >= promptThreshold || contextPercent.totalChars >= charsThreshold) &&
|
|
34
38
|
(now - contextPercent.lastWarningAt > cooldownMs));
|
|
35
39
|
}
|
|
40
|
+
/** auto-compact 트리거 여부 판정 (순수 함수) */
|
|
41
|
+
export function shouldAutoCompact(state, thresholds = {}) {
|
|
42
|
+
const charsThreshold = thresholds.charsThreshold ?? AUTO_COMPACT_CHARS_THRESHOLD;
|
|
43
|
+
const cooldownMs = thresholds.cooldownMs ?? AUTO_COMPACT_COOLDOWN_MS;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
return state.totalChars >= charsThreshold && (now - state.lastAutoCompactAt > cooldownMs);
|
|
46
|
+
}
|
|
47
|
+
/** auto-compact 지시 메시지 생성 (순수 함수) */
|
|
48
|
+
export function buildAutoCompactMessage(totalChars) {
|
|
49
|
+
return `<forgen-auto-compact>
|
|
50
|
+
[Forgen] Context 사용량이 ${Math.round(totalChars / 1000)}K 문자에 도달했습니다 (추정 ~20%+).
|
|
51
|
+
지금 즉시 /compact를 실행하여 컨텍스트를 압축하세요. 이 지시는 자동으로 생성되었습니다.
|
|
52
|
+
현재 작업을 마무리하지 말고, 다음 응답에서 바로 compact를 실행하세요.
|
|
53
|
+
</forgen-auto-compact>`;
|
|
54
|
+
}
|
|
36
55
|
/** 경고 메시지 생성 (순수 함수) */
|
|
37
56
|
export function buildContextWarningMessage(promptCount, totalChars) {
|
|
38
57
|
return `<compound-context-warning>\n[Forgen] Context limit approaching: ${promptCount} prompts, ${Math.round(totalChars / 1000)}K characters.\nIf you have important progress, save it now:\n- Use cancelforgen to reset mode state and start a new session\n- Or continue current work (auto compaction may occur)\n</compound-context-warning>`;
|
|
@@ -48,77 +67,220 @@ function loadContextState(sessionId) {
|
|
|
48
67
|
catch (e) {
|
|
49
68
|
log.debug('context state 파일 읽기/파싱 실패', e);
|
|
50
69
|
}
|
|
51
|
-
return { promptCount: 0, totalChars: 0, lastWarningAt: 0, sessionId };
|
|
70
|
+
return { promptCount: 0, totalChars: 0, lastWarningAt: 0, lastAutoCompactAt: 0, sessionId };
|
|
52
71
|
}
|
|
53
72
|
function saveContextState(state) {
|
|
54
73
|
atomicWriteJSON(CONTEXT_STATE_PATH, state);
|
|
55
74
|
}
|
|
56
75
|
export async function main() {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
reason: 'token-limit',
|
|
78
|
-
sessionId,
|
|
79
|
-
savedAt: new Date().toISOString(),
|
|
80
|
-
cwd: process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd(),
|
|
81
|
-
}, null, 2));
|
|
82
|
-
}
|
|
83
|
-
catch { /* fail-open */ }
|
|
84
|
-
console.log(approveWithWarning(`[Forgen] Context limit reached. Current state has been saved to ~/.forgen/handoffs/.\nThe previous work will be automatically recovered in the next session.`));
|
|
76
|
+
const _hookStart = Date.now();
|
|
77
|
+
let _hookEvent = 'UserPromptSubmit';
|
|
78
|
+
try {
|
|
79
|
+
const input = await readStdinJSON();
|
|
80
|
+
if (!isHookEnabled('context-guard')) {
|
|
81
|
+
console.log(approve());
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!input) {
|
|
85
|
+
console.log(approve());
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const sessionId = input.session_id ?? 'default';
|
|
89
|
+
// Stop 훅: stop_hook_type이 있으면 처리
|
|
90
|
+
if (input.stop_hook_type) {
|
|
91
|
+
_hookEvent = 'Stop';
|
|
92
|
+
// forge-loop 활성 시 미완료 스토리 감지 → 지속 메시지 주입 (polite-stop 방지)
|
|
93
|
+
const forgeLoopBlock = checkForgeLoopActive();
|
|
94
|
+
if (forgeLoopBlock) {
|
|
95
|
+
console.log(forgeLoopBlock);
|
|
85
96
|
return;
|
|
86
97
|
}
|
|
98
|
+
// 에러가 포함된 경우: context limit 감지
|
|
99
|
+
if (input.error) {
|
|
100
|
+
const errorMsg = input.error;
|
|
101
|
+
if (/context.*limit|token.*limit|conversation.*too.*long/i.test(errorMsg)) {
|
|
102
|
+
saveHandoff(sessionId, 'context-limit', errorMsg);
|
|
103
|
+
try {
|
|
104
|
+
const resumePath = path.join(STATE_DIR, 'pending-resume.json');
|
|
105
|
+
fs.writeFileSync(resumePath, JSON.stringify({
|
|
106
|
+
reason: 'token-limit',
|
|
107
|
+
sessionId,
|
|
108
|
+
savedAt: new Date().toISOString(),
|
|
109
|
+
cwd: process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd(),
|
|
110
|
+
}, null, 2));
|
|
111
|
+
}
|
|
112
|
+
catch { /* fail-open */ }
|
|
113
|
+
console.log(approveWithWarning(`[Forgen] Context limit reached. Current state has been saved to ~/.forgen/handoffs/.\nThe previous work will be automatically recovered in the next session.`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// 정상 종료 시: 의미 있는 세션이었으면 compound 안내/자동 트리거
|
|
118
|
+
if (input.stop_hook_type === 'user' || input.stop_hook_type === 'end_turn') {
|
|
119
|
+
const state = loadContextState(sessionId);
|
|
120
|
+
if (state.promptCount >= 20) {
|
|
121
|
+
// 20+ prompts: auto-trigger compound by writing marker
|
|
122
|
+
try {
|
|
123
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
124
|
+
const marker = { reason: 'session-end', promptCount: state.promptCount, detectedAt: new Date().toISOString() };
|
|
125
|
+
fs.writeFileSync(path.join(STATE_DIR, 'pending-compound.json'), JSON.stringify(marker));
|
|
126
|
+
}
|
|
127
|
+
catch { /* fail-open: marker write failure is non-critical */ }
|
|
128
|
+
const summary = buildSessionSummary(sessionId, state.promptCount);
|
|
129
|
+
console.log(approveWithWarning(`[Forgen] Session with ${state.promptCount} prompts ended.\n${summary}\nCompound loop will auto-trigger on next session start.`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (state.promptCount >= 10) {
|
|
133
|
+
// 10-19 prompts: suggest /compound manually
|
|
134
|
+
const summary = buildSessionSummary(sessionId, state.promptCount);
|
|
135
|
+
console.log(approveWithWarning(`[Forgen] 이 세션에서 ${state.promptCount}개의 프롬프트를 처리했습니다.\n${summary}/compound 를 실행하면 이 세션의 학습 내용을 축적할 수 있습니다.`));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log(approve());
|
|
140
|
+
return;
|
|
87
141
|
}
|
|
88
|
-
//
|
|
89
|
-
if (input.
|
|
142
|
+
// error만 있는 경우 (stop_hook_type 없이)
|
|
143
|
+
if (input.error) {
|
|
144
|
+
console.log(approve());
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// UserPromptSubmit 훅: 대화 길이 추적
|
|
148
|
+
if (input.prompt) {
|
|
149
|
+
const config = loadHookConfig('context-guard');
|
|
150
|
+
// maxTokens가 설정되어 있으면 chars threshold로 사용 (토큰 ≈ 4자 기준 환산)
|
|
151
|
+
const charsThreshold = typeof config?.maxTokens === 'number' ? config.maxTokens * 4 : undefined;
|
|
90
152
|
const state = loadContextState(sessionId);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
153
|
+
state.promptCount++;
|
|
154
|
+
state.totalChars += input.prompt.length;
|
|
155
|
+
// auto-compact: 추적 문자 120K 이상이면 compact 지시 주입
|
|
156
|
+
const autoCompactThreshold = typeof config?.autoCompactChars === 'number' ? config.autoCompactChars : undefined;
|
|
157
|
+
if (shouldAutoCompact(state, autoCompactThreshold !== undefined ? { charsThreshold: autoCompactThreshold } : {})) {
|
|
158
|
+
state.lastAutoCompactAt = Date.now();
|
|
159
|
+
saveContextState(state);
|
|
160
|
+
console.log(approveWithContext(buildAutoCompactMessage(state.totalChars), 'UserPromptSubmit'));
|
|
94
161
|
return;
|
|
95
162
|
}
|
|
163
|
+
if (shouldWarn(state, charsThreshold !== undefined ? { charsThreshold } : {})) {
|
|
164
|
+
state.lastWarningAt = Date.now();
|
|
165
|
+
saveContextState(state);
|
|
166
|
+
console.log(approveWithContext(buildContextWarningMessage(state.promptCount, state.totalChars), 'UserPromptSubmit'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
saveContextState(state);
|
|
96
170
|
}
|
|
97
171
|
console.log(approve());
|
|
98
|
-
return;
|
|
99
172
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.log(approve());
|
|
103
|
-
return;
|
|
173
|
+
finally {
|
|
174
|
+
recordHookTiming('context-guard', Date.now() - _hookStart, _hookEvent);
|
|
104
175
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 세션 종료 시 "forgen이 도움이 된 정도"를 요약.
|
|
179
|
+
* solution-cache에서 이번 세션에 주입된 compound 솔루션 수를 집계하여
|
|
180
|
+
* 카운터팩추얼 "forgen 없었으면 ~N분 더 걸렸을 것" 메시지 생성.
|
|
181
|
+
*/
|
|
182
|
+
function buildSessionSummary(sessionId, promptCount) {
|
|
183
|
+
try {
|
|
184
|
+
const cachePath = path.join(STATE_DIR, `solution-cache-${sessionId}.json`);
|
|
185
|
+
if (!fs.existsSync(cachePath))
|
|
186
|
+
return '';
|
|
187
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
188
|
+
const injected = Array.isArray(cache.injected) ? cache.injected : [];
|
|
189
|
+
if (injected.length === 0)
|
|
190
|
+
return '';
|
|
191
|
+
// 카운터팩추얼: 주입된 compound 1건당 평균 8분 절약 가정 (하한 추정)
|
|
192
|
+
const savedMins = injected.length * 8;
|
|
193
|
+
const savedStr = savedMins >= 60
|
|
194
|
+
? `${Math.floor(savedMins / 60)}시간 ${savedMins % 60}분`
|
|
195
|
+
: `${savedMins}분`;
|
|
196
|
+
// 상위 3개 솔루션
|
|
197
|
+
const topNames = injected.slice(0, 3).map(i => `"${i.name}"`).join(', ');
|
|
198
|
+
const moreCount = injected.length - 3;
|
|
199
|
+
const topStr = moreCount > 0 ? `${topNames} 외 ${moreCount}개` : topNames;
|
|
200
|
+
return [
|
|
201
|
+
`\n📊 이번 세션 forgen 효과:`,
|
|
202
|
+
` 주입된 compound: ${injected.length}건 (${topStr})`,
|
|
203
|
+
` 추정 절약 시간: ${savedStr} (forgen 없었으면 시행착오 필요)`,
|
|
204
|
+
` 프롬프트 대비 효율: ${(injected.length / promptCount * 100).toFixed(0)}% 의 대화가 축적된 지식의 도움을 받음\n`,
|
|
205
|
+
].join('\n');
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// forge-loop 상태 파일 경로
|
|
212
|
+
const FORGE_LOOP_STATE_PATH = path.join(STATE_DIR, 'forge-loop.json');
|
|
213
|
+
// forge-loop 차단 안전 상한 (무한 루프 방지)
|
|
214
|
+
const FORGE_LOOP_MAX_BLOCKS = 30;
|
|
215
|
+
const FORGE_LOOP_STALE_MS = 2 * 60 * 60 * 1000; // 2시간
|
|
216
|
+
/**
|
|
217
|
+
* forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
|
|
218
|
+
* OMC의 persistent-mode.cjs 패턴 참고.
|
|
219
|
+
*/
|
|
220
|
+
export function checkForgeLoopActive() {
|
|
221
|
+
try {
|
|
222
|
+
if (!fs.existsSync(FORGE_LOOP_STATE_PATH))
|
|
223
|
+
return null;
|
|
224
|
+
const state = JSON.parse(fs.readFileSync(FORGE_LOOP_STATE_PATH, 'utf-8'));
|
|
225
|
+
if (!state.active)
|
|
226
|
+
return null;
|
|
227
|
+
// Stale 감지: 2시간+ 미활동 → 자동 비활성화
|
|
228
|
+
const startedAt = new Date(state.startedAt).getTime();
|
|
229
|
+
if (Number.isFinite(startedAt) && Date.now() - startedAt > FORGE_LOOP_STALE_MS) {
|
|
230
|
+
state.active = false;
|
|
231
|
+
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
// 확인 대기 중이면 차단하지 않음 (사용자 개입 허용)
|
|
235
|
+
if (state.awaitingConfirmation)
|
|
236
|
+
return null;
|
|
237
|
+
// 안전 상한: 30회 이상 차단 시 무한 루프로 간주하여 해제
|
|
238
|
+
const blockCount = state.blockCount ?? 0;
|
|
239
|
+
if (blockCount >= FORGE_LOOP_MAX_BLOCKS) {
|
|
240
|
+
state.active = false;
|
|
241
|
+
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
// 미완료 스토리 확인
|
|
245
|
+
const stories = Array.isArray(state.stories) ? state.stories : [];
|
|
246
|
+
const pending = stories.filter((s) => !s.passes);
|
|
247
|
+
if (pending.length === 0) {
|
|
248
|
+
// 모든 스토리 완료 → forge-loop 종료
|
|
249
|
+
state.active = false;
|
|
250
|
+
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
251
|
+
return null;
|
|
118
252
|
}
|
|
119
|
-
|
|
253
|
+
// 차단 카운트 증가 + 지속 메시지 주입
|
|
254
|
+
state.blockCount = blockCount + 1;
|
|
255
|
+
state.lastBlockAt = new Date().toISOString();
|
|
256
|
+
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
257
|
+
const nextStory = pending[0];
|
|
258
|
+
const message = [
|
|
259
|
+
`<forgen-forge-loop iteration="${state.blockCount}/${FORGE_LOOP_MAX_BLOCKS}">`,
|
|
260
|
+
`[FORGE-LOOP] ${pending.length}개 스토리가 미완료입니다.`,
|
|
261
|
+
`현재 스토리: ${nextStory.id} — ${nextStory.title}`,
|
|
262
|
+
``,
|
|
263
|
+
`계속 진행하세요. 보고는 다음 시점에만 합니다:`,
|
|
264
|
+
` 1. 모든 스토리 완료 (최종 리포트)`,
|
|
265
|
+
` 2. 3회 실패 (에스컬레이션)`,
|
|
266
|
+
` 3. Context limit 접근 (handoff)`,
|
|
267
|
+
``,
|
|
268
|
+
`중간 "완료했습니다" 보고는 polite-stop anti-pattern입니다.`,
|
|
269
|
+
`취소하려면: "/forge-loop cancel" 또는 "cancelforgen" 입력`,
|
|
270
|
+
`</forgen-forge-loop>`,
|
|
271
|
+
].join('\n');
|
|
272
|
+
// block 결정으로 Claude가 계속 작업하도록 강제
|
|
273
|
+
return JSON.stringify({
|
|
274
|
+
continue: true,
|
|
275
|
+
decision: 'block',
|
|
276
|
+
reason: message,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
// fail-open: forge-loop 상태 읽기 실패는 차단하지 않음
|
|
281
|
+
log.debug('forge-loop 상태 확인 실패', e);
|
|
282
|
+
return null;
|
|
120
283
|
}
|
|
121
|
-
console.log(approve());
|
|
122
284
|
}
|
|
123
285
|
function saveHandoff(sessionId, reason, detail) {
|
|
124
286
|
fs.mkdirSync(HANDOFFS_DIR, { recursive: true });
|
|
@@ -161,6 +323,6 @@ function saveHandoff(sessionId, reason, detail) {
|
|
|
161
323
|
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
162
324
|
main().catch((e) => {
|
|
163
325
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
164
|
-
console.log(
|
|
326
|
+
console.log(failOpenWithTracking('context-guard'));
|
|
165
327
|
});
|
|
166
328
|
}
|
package/dist/hooks/db-guard.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as path from 'node:path';
|
|
|
9
9
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
10
10
|
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
11
11
|
import { isHookEnabled } from './hook-config.js';
|
|
12
|
-
import { approve, approveWithWarning, deny,
|
|
12
|
+
import { approve, approveWithWarning, deny, failOpenWithTracking } from './shared/hook-response.js';
|
|
13
13
|
import { STATE_DIR } from '../core/paths.js';
|
|
14
14
|
const FAIL_COUNTER_PATH = path.join(STATE_DIR, 'db-guard-fail-counter.json');
|
|
15
15
|
const FAIL_CLOSE_THRESHOLD = 3;
|
|
@@ -101,5 +101,5 @@ async function main() {
|
|
|
101
101
|
}
|
|
102
102
|
main().catch((e) => {
|
|
103
103
|
process.stderr.write(`[ch-hook] DB Guard error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
104
|
-
console.log(
|
|
104
|
+
console.log(failOpenWithTracking('db-guard'));
|
|
105
105
|
});
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forgen — Hook Config Loader
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* hook-config.json 에서 훅별 설정을 읽어 반환합니다.
|
|
5
5
|
* 파일이 없거나 읽기에 실패하면 null 을 반환합니다 (failure-tolerant).
|
|
6
6
|
*
|
|
7
|
+
* 설정 로딩 우선순위:
|
|
8
|
+
* 1. 프로젝트 레벨: {cwd}/.forgen/hook-config.json
|
|
9
|
+
* 2. 글로벌 레벨: FORGEN_HOME/hook-config.json (~/.forgen/hook-config.json)
|
|
10
|
+
* 프로젝트 설정은 글로벌 설정과 머지됩니다 (훅 단위 오버라이드).
|
|
11
|
+
* 프로젝트 설정이 없으면 글로벌 설정만 사용 (하위호환).
|
|
12
|
+
*
|
|
13
|
+
* cwd 결정: process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd()
|
|
14
|
+
*
|
|
7
15
|
* 설정 형식 (hook-config.json):
|
|
8
16
|
* {
|
|
9
17
|
* "tiers": { "compound-core": { "enabled": true }, "safety": { "enabled": true }, "workflow": { "enabled": true } },
|
|
@@ -15,6 +23,24 @@
|
|
|
15
23
|
* - compound-core 티어는 tiers 설정으로 비활성화 불가 (복리화 보호)
|
|
16
24
|
* - 개별 hooks.hookName.enabled: false 로만 비활성화 가능
|
|
17
25
|
*/
|
|
26
|
+
/** 훅 설정 파일의 전체 구조 타입 */
|
|
27
|
+
export type HookConfig = Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* 프로젝트의 작업 디렉토리를 결정합니다.
|
|
30
|
+
* FORGEN_CWD → COMPOUND_CWD → process.cwd() 순서.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveProjectCwd(): string;
|
|
33
|
+
/**
|
|
34
|
+
* 글로벌 설정과 프로젝트 설정을 머지합니다.
|
|
35
|
+
* 프로젝트 설정이 글로벌 설정을 훅 단위로 오버라이드합니다.
|
|
36
|
+
*
|
|
37
|
+
* 머지 규칙:
|
|
38
|
+
* - tiers: 프로젝트가 글로벌을 훅 단위로 오버라이드 (shallow merge)
|
|
39
|
+
* - hooks: 프로젝트가 글로벌을 훅 단위로 오버라이드 (shallow merge)
|
|
40
|
+
* - 최상위 레거시 키: 프로젝트가 글로벌을 키 단위로 오버라이드
|
|
41
|
+
* - 프로젝트에 없는 키는 글로벌에서 상속
|
|
42
|
+
*/
|
|
43
|
+
export declare function mergeHookConfigs(global: HookConfig, project: HookConfig): HookConfig;
|
|
18
44
|
/** 특정 훅의 설정을 반환합니다. 실패 시 null 반환. */
|
|
19
45
|
export declare function loadHookConfig(hookName: string): Record<string, unknown> | null;
|
|
20
46
|
/**
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forgen — Hook Config Loader
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* hook-config.json 에서 훅별 설정을 읽어 반환합니다.
|
|
5
5
|
* 파일이 없거나 읽기에 실패하면 null 을 반환합니다 (failure-tolerant).
|
|
6
6
|
*
|
|
7
|
+
* 설정 로딩 우선순위:
|
|
8
|
+
* 1. 프로젝트 레벨: {cwd}/.forgen/hook-config.json
|
|
9
|
+
* 2. 글로벌 레벨: FORGEN_HOME/hook-config.json (~/.forgen/hook-config.json)
|
|
10
|
+
* 프로젝트 설정은 글로벌 설정과 머지됩니다 (훅 단위 오버라이드).
|
|
11
|
+
* 프로젝트 설정이 없으면 글로벌 설정만 사용 (하위호환).
|
|
12
|
+
*
|
|
13
|
+
* cwd 결정: process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd()
|
|
14
|
+
*
|
|
7
15
|
* 설정 형식 (hook-config.json):
|
|
8
16
|
* {
|
|
9
17
|
* "tiers": { "compound-core": { "enabled": true }, "safety": { "enabled": true }, "workflow": { "enabled": true } },
|
|
@@ -19,30 +27,82 @@ import * as fs from 'node:fs';
|
|
|
19
27
|
import * as path from 'node:path';
|
|
20
28
|
import { HOOK_REGISTRY } from './hook-registry.js';
|
|
21
29
|
import { FORGEN_HOME } from '../core/paths.js';
|
|
22
|
-
const
|
|
30
|
+
const GLOBAL_CONFIG_PATH = path.join(FORGEN_HOME, 'hook-config.json');
|
|
23
31
|
/**
|
|
24
32
|
* 훅 → 티어 매핑 (hook-registry.ts에서 자동 파생).
|
|
25
33
|
* 이중 구현 방지: HOOK_REGISTRY가 단일 소스 오브 트루스.
|
|
26
34
|
*/
|
|
27
35
|
const HOOK_TIER_MAP = Object.fromEntries(HOOK_REGISTRY.map(h => [h.name, h.tier]));
|
|
36
|
+
/**
|
|
37
|
+
* 프로젝트의 작업 디렉토리를 결정합니다.
|
|
38
|
+
* FORGEN_CWD → COMPOUND_CWD → process.cwd() 순서.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveProjectCwd() {
|
|
41
|
+
return process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
42
|
+
}
|
|
43
|
+
/** JSON 파일을 파싱하여 반환. 파일 없음 또는 파싱 실패 시 null. */
|
|
44
|
+
function loadJsonFile(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(filePath))
|
|
47
|
+
return null;
|
|
48
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 글로벌 설정과 프로젝트 설정을 머지합니다.
|
|
56
|
+
* 프로젝트 설정이 글로벌 설정을 훅 단위로 오버라이드합니다.
|
|
57
|
+
*
|
|
58
|
+
* 머지 규칙:
|
|
59
|
+
* - tiers: 프로젝트가 글로벌을 훅 단위로 오버라이드 (shallow merge)
|
|
60
|
+
* - hooks: 프로젝트가 글로벌을 훅 단위로 오버라이드 (shallow merge)
|
|
61
|
+
* - 최상위 레거시 키: 프로젝트가 글로벌을 키 단위로 오버라이드
|
|
62
|
+
* - 프로젝트에 없는 키는 글로벌에서 상속
|
|
63
|
+
*/
|
|
64
|
+
export function mergeHookConfigs(global, project) {
|
|
65
|
+
const merged = { ...global };
|
|
66
|
+
// tiers 머지 (shallow per-tier)
|
|
67
|
+
const globalTiers = global.tiers;
|
|
68
|
+
const projectTiers = project.tiers;
|
|
69
|
+
if (globalTiers || projectTiers) {
|
|
70
|
+
merged.tiers = { ...globalTiers, ...projectTiers };
|
|
71
|
+
}
|
|
72
|
+
// hooks 머지 (shallow per-hook)
|
|
73
|
+
const globalHooks = global.hooks;
|
|
74
|
+
const projectHooks = project.hooks;
|
|
75
|
+
if (globalHooks || projectHooks) {
|
|
76
|
+
merged.hooks = { ...globalHooks, ...projectHooks };
|
|
77
|
+
}
|
|
78
|
+
// 나머지 최상위 키: 프로젝트가 글로벌을 오버라이드
|
|
79
|
+
for (const key of Object.keys(project)) {
|
|
80
|
+
if (key === 'tiers' || key === 'hooks')
|
|
81
|
+
continue;
|
|
82
|
+
merged[key] = project[key];
|
|
83
|
+
}
|
|
84
|
+
return merged;
|
|
85
|
+
}
|
|
28
86
|
/** 프로세스 내 설정 캐시 (각 훅은 별도 프로세스이므로 수명 = 1회 실행) */
|
|
29
87
|
let _configCache;
|
|
30
|
-
/** 전체 설정 파일을
|
|
88
|
+
/** 전체 설정 파일을 파싱합니다 (글로벌 + 프로젝트 머지). 실패 시 null. 프로세스 내 캐싱. */
|
|
31
89
|
function loadFullConfig() {
|
|
32
90
|
if (_configCache !== undefined)
|
|
33
91
|
return _configCache;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
_configCache = JSON.parse(fs.readFileSync(HOOK_CONFIG_PATH, 'utf-8'));
|
|
40
|
-
return _configCache;
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
92
|
+
const globalConfig = loadJsonFile(GLOBAL_CONFIG_PATH);
|
|
93
|
+
const projectConfigPath = path.join(resolveProjectCwd(), '.forgen', 'hook-config.json');
|
|
94
|
+
const projectConfig = loadJsonFile(projectConfigPath);
|
|
95
|
+
if (!globalConfig && !projectConfig) {
|
|
43
96
|
_configCache = null;
|
|
44
97
|
return null;
|
|
45
98
|
}
|
|
99
|
+
if (globalConfig && projectConfig) {
|
|
100
|
+
_configCache = mergeHookConfigs(globalConfig, projectConfig);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
_configCache = globalConfig ?? projectConfig ?? null;
|
|
104
|
+
}
|
|
105
|
+
return _configCache;
|
|
46
106
|
}
|
|
47
107
|
/** 특정 훅의 설정을 반환합니다. 실패 시 null 반환. */
|
|
48
108
|
export function loadHookConfig(hookName) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - forgen config hooks (사용자 설정 변경 후)
|
|
10
10
|
* - forgen install (플러그인 설치 후)
|
|
11
11
|
*/
|
|
12
|
+
import { type RuntimeHost } from '../core/types.js';
|
|
12
13
|
interface HookCommand {
|
|
13
14
|
type: 'command';
|
|
14
15
|
command: string;
|
|
@@ -27,6 +28,8 @@ interface GenerateOptions {
|
|
|
27
28
|
cwd?: string;
|
|
28
29
|
/** 훅 실행 스크립트의 루트 경로 */
|
|
29
30
|
pluginRoot?: string;
|
|
31
|
+
/** 런타임 (claude|codex) */
|
|
32
|
+
runtime?: RuntimeHost;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* 활성 훅만 포함한 hooks.json 객체를 생성합니다.
|
|
@@ -14,6 +14,27 @@ import * as path from 'node:path';
|
|
|
14
14
|
import { HOOK_REGISTRY } from './hook-registry.js';
|
|
15
15
|
import { isHookEnabled } from './hook-config.js';
|
|
16
16
|
import { detectInstalledPlugins, getHookConflicts } from '../core/plugin-detector.js';
|
|
17
|
+
function splitCommand(raw) {
|
|
18
|
+
const tokens = raw.match(/"([^"]+)"|\S+/g) ?? [];
|
|
19
|
+
const unquoted = tokens.map(token => token.replace(/^"/, '').replace(/"$/, ''));
|
|
20
|
+
return { script: unquoted[0] ?? '', args: unquoted.slice(1) };
|
|
21
|
+
}
|
|
22
|
+
function quoteArg(raw) {
|
|
23
|
+
return `"${raw.replace(/"/g, '\\"')}"`;
|
|
24
|
+
}
|
|
25
|
+
function buildHookCommand(pluginRoot, rawScript, runtime) {
|
|
26
|
+
const { script, args } = splitCommand(rawScript);
|
|
27
|
+
const scriptPath = `${pluginRoot}/${script}`;
|
|
28
|
+
const quotedArgs = args.map(quoteArg).join(' ');
|
|
29
|
+
if (runtime === 'codex') {
|
|
30
|
+
const adapterPath = `${pluginRoot}/host/codex-adapter.js`;
|
|
31
|
+
const baseCommand = `node ${quoteArg(adapterPath)} ${quoteArg(scriptPath)}`;
|
|
32
|
+
return `${baseCommand}${quotedArgs ? ` ${quotedArgs}` : ''}`;
|
|
33
|
+
}
|
|
34
|
+
return quotedArgs
|
|
35
|
+
? `node ${quoteArg(scriptPath)} ${quotedArgs}`
|
|
36
|
+
: `node ${quoteArg(scriptPath)}`;
|
|
37
|
+
}
|
|
17
38
|
/**
|
|
18
39
|
* 활성 훅만 포함한 hooks.json 객체를 생성합니다.
|
|
19
40
|
*
|
|
@@ -27,6 +48,7 @@ export function generateHooksJson(options) {
|
|
|
27
48
|
const cwd = options?.cwd;
|
|
28
49
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: CLAUDE_PLUGIN_ROOT is a Claude Code Plugin SDK variable resolved at runtime
|
|
29
50
|
const pluginRoot = options?.pluginRoot ?? '${CLAUDE_PLUGIN_ROOT}/dist';
|
|
51
|
+
const runtime = options?.runtime ?? 'claude';
|
|
30
52
|
// 다른 플러그인의 충돌 훅 감지
|
|
31
53
|
const hookConflicts = getHookConflicts(cwd);
|
|
32
54
|
const detectedPlugins = detectInstalledPlugins(cwd);
|
|
@@ -64,12 +86,7 @@ export function generateHooksJson(options) {
|
|
|
64
86
|
hooks[event] = [...byMatcher.entries()].map(([matcher, matcherEntries]) => ({
|
|
65
87
|
matcher,
|
|
66
88
|
hooks: matcherEntries.map(h => {
|
|
67
|
-
|
|
68
|
-
// 파일 경로와 인자를 분리해야 셸에서 ENOENT를 방지
|
|
69
|
-
const spaceIdx = h.script.indexOf(' ');
|
|
70
|
-
const command = spaceIdx === -1
|
|
71
|
-
? `node "${pluginRoot}/${h.script}"`
|
|
72
|
-
: `node "${pluginRoot}/${h.script.slice(0, spaceIdx)}" ${h.script.slice(spaceIdx + 1)}`;
|
|
89
|
+
const command = buildHookCommand(pluginRoot, h.script, runtime);
|
|
73
90
|
return { type: 'command', command, timeout: h.timeout };
|
|
74
91
|
}),
|
|
75
92
|
}));
|
|
@@ -10,5 +10,3 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export type Intent = 'implement' | 'debug' | 'refactor' | 'explain' | 'review' | 'explore' | 'design' | 'general';
|
|
12
12
|
export declare function classifyIntent(prompt: string): Intent;
|
|
13
|
-
/** 프롬프트에 매칭되는 모든 의도를 반환. 없으면 ['general']. */
|
|
14
|
-
export declare function classifyAllIntents(prompt: string): Intent[];
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
12
12
|
import { isHookEnabled } from './hook-config.js';
|
|
13
|
-
import { approve, approveWithContext,
|
|
13
|
+
import { approve, approveWithContext, failOpenWithTracking } from './shared/hook-response.js';
|
|
14
14
|
const INTENT_RULES = [
|
|
15
|
-
{ intent: 'implement', pattern: /(?:만들어|추가해|구현해|생성해|작성해|넣어|create|add|implement|build|write|make)
|
|
15
|
+
{ intent: 'implement', pattern: /(?:만들어|추가해|구현해|생성해|작성해|넣어|create|add|implement|build|write|make)(?:\b|(?=[가-힣\s]|$))/i },
|
|
16
16
|
{ intent: 'debug', pattern: /(?:에러|버그|안돼|안\s*되|안\s*됨|왜|고쳐|수정해|fix|bug|error|debug|문제|실패|fail|crash|broken)/i },
|
|
17
17
|
{ intent: 'refactor', pattern: /(?:리팩토링|리팩터|정리|개선|refactor|clean\s*up|improve|optimize|최적화)/i },
|
|
18
18
|
{ intent: 'explain', pattern: /(?:설명|알려|뭐야|뭔가요|어떻게|explain|what\s+is|how\s+does|why\s+does|tell\s+me)/i },
|
|
@@ -30,6 +30,29 @@ const INTENT_HINTS = {
|
|
|
30
30
|
design: 'Design task. Specify trade-offs explicitly.',
|
|
31
31
|
general: 'General request.',
|
|
32
32
|
};
|
|
33
|
+
/** Intent-specific context rules injected via additionalContext */
|
|
34
|
+
const INTENT_CONTEXT = {
|
|
35
|
+
implement: `[quality-rules]
|
|
36
|
+
- Write tests for new logic (branch coverage 83%+)
|
|
37
|
+
- Build + lint + type-check must pass before completion
|
|
38
|
+
- Prefer small incremental changes (<200 lines)
|
|
39
|
+
- Interfaces and type contracts before implementation`,
|
|
40
|
+
review: `[review-rules]
|
|
41
|
+
- Report format: [SEVERITY] file:line — issue
|
|
42
|
+
- Check: logic errors, security (OWASP), performance, maintainability
|
|
43
|
+
- Verify edge cases and error handling at system boundaries
|
|
44
|
+
- No empty catch blocks, no eslint-disable without justification`,
|
|
45
|
+
debug: `[debug-rules]
|
|
46
|
+
- Reproduce the bug first, then isolate the root cause
|
|
47
|
+
- Write a failing test that captures the bug before fixing
|
|
48
|
+
- Check for regression: does the fix break anything else?
|
|
49
|
+
- Read error messages carefully — they usually point to the cause`,
|
|
50
|
+
refactor: `[refactor-rules]
|
|
51
|
+
- Ensure all tests pass before AND after refactoring
|
|
52
|
+
- Make one structural change at a time, verify between each
|
|
53
|
+
- Preserve external behavior — refactoring changes structure, not function
|
|
54
|
+
- Avoid mixing refactoring with feature changes in the same pass`,
|
|
55
|
+
};
|
|
33
56
|
export function classifyIntent(prompt) {
|
|
34
57
|
for (const rule of INTENT_RULES) {
|
|
35
58
|
if (rule.pattern.test(prompt)) {
|
|
@@ -38,16 +61,6 @@ export function classifyIntent(prompt) {
|
|
|
38
61
|
}
|
|
39
62
|
return 'general';
|
|
40
63
|
}
|
|
41
|
-
/** 프롬프트에 매칭되는 모든 의도를 반환. 없으면 ['general']. */
|
|
42
|
-
export function classifyAllIntents(prompt) {
|
|
43
|
-
const matches = [];
|
|
44
|
-
for (const rule of INTENT_RULES) {
|
|
45
|
-
if (rule.pattern.test(prompt)) {
|
|
46
|
-
matches.push(rule.intent);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return matches.length > 0 ? matches : ['general'];
|
|
50
|
-
}
|
|
51
64
|
async function main() {
|
|
52
65
|
const input = await readStdinJSON();
|
|
53
66
|
if (!isHookEnabled('intent-classifier')) {
|
|
@@ -58,16 +71,17 @@ async function main() {
|
|
|
58
71
|
console.log(approve());
|
|
59
72
|
return;
|
|
60
73
|
}
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
74
|
+
const intent = classifyIntent(input.prompt);
|
|
75
|
+
if (intent === 'general') {
|
|
63
76
|
console.log(approve());
|
|
64
77
|
return;
|
|
65
78
|
}
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
79
|
+
const hint = INTENT_HINTS[intent];
|
|
80
|
+
const extra = INTENT_CONTEXT[intent] ?? '';
|
|
81
|
+
const context = extra ? `[intent: ${intent}] ${hint}\n${extra}` : `[intent: ${intent}] ${hint}`;
|
|
82
|
+
console.log(approveWithContext(context, 'UserPromptSubmit'));
|
|
69
83
|
}
|
|
70
84
|
main().catch((e) => {
|
|
71
85
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
72
|
-
console.log(
|
|
86
|
+
console.log(failOpenWithTracking('intent-classifier'));
|
|
73
87
|
});
|