@wooojin/forgen 0.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/.claude-plugin/plugin.json +20 -0
- package/CHANGELOG.md +353 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +21 -0
- package/README.ja.md +469 -0
- package/README.ko.md +469 -0
- package/README.md +483 -0
- package/README.zh.md +469 -0
- package/agents/analyst.md +98 -0
- package/agents/architect.md +62 -0
- package/agents/code-reviewer.md +120 -0
- package/agents/code-simplifier.md +197 -0
- package/agents/critic.md +70 -0
- package/agents/debugger.md +117 -0
- package/agents/designer.md +131 -0
- package/agents/executor.md +54 -0
- package/agents/explore.md +145 -0
- package/agents/git-master.md +212 -0
- package/agents/performance-reviewer.md +172 -0
- package/agents/planner.md +29 -0
- package/agents/qa-tester.md +158 -0
- package/agents/refactoring-expert.md +168 -0
- package/agents/scientist.md +144 -0
- package/agents/security-reviewer.md +137 -0
- package/agents/test-engineer.md +153 -0
- package/agents/verifier.md +133 -0
- package/agents/writer.md +184 -0
- package/commands/api-design.md +268 -0
- package/commands/architecture-decision.md +314 -0
- package/commands/ci-cd.md +270 -0
- package/commands/code-review.md +233 -0
- package/commands/compound.md +117 -0
- package/commands/database.md +263 -0
- package/commands/debug-detective.md +99 -0
- package/commands/docker.md +274 -0
- package/commands/documentation.md +276 -0
- package/commands/ecomode.md +51 -0
- package/commands/frontend.md +271 -0
- package/commands/git-master.md +90 -0
- package/commands/incident-response.md +292 -0
- package/commands/migrate.md +101 -0
- package/commands/performance.md +288 -0
- package/commands/refactor.md +105 -0
- package/commands/security-review.md +288 -0
- package/commands/tdd.md +183 -0
- package/commands/testing-strategy.md +265 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/core/auto-compound-runner.d.ts +12 -0
- package/dist/core/auto-compound-runner.js +460 -0
- package/dist/core/config-hooks.d.ts +10 -0
- package/dist/core/config-hooks.js +112 -0
- package/dist/core/config-injector.d.ts +50 -0
- package/dist/core/config-injector.js +455 -0
- package/dist/core/doctor.d.ts +1 -0
- package/dist/core/doctor.js +163 -0
- package/dist/core/errors.d.ts +81 -0
- package/dist/core/errors.js +133 -0
- package/dist/core/global-config.d.ts +43 -0
- package/dist/core/global-config.js +25 -0
- package/dist/core/harness.d.ts +24 -0
- package/dist/core/harness.js +621 -0
- package/dist/core/init.d.ts +7 -0
- package/dist/core/init.js +37 -0
- package/dist/core/inspect-cli.d.ts +7 -0
- package/dist/core/inspect-cli.js +47 -0
- package/dist/core/legacy-detector.d.ts +33 -0
- package/dist/core/legacy-detector.js +66 -0
- package/dist/core/logger.d.ts +34 -0
- package/dist/core/logger.js +121 -0
- package/dist/core/mcp-config.d.ts +44 -0
- package/dist/core/mcp-config.js +177 -0
- package/dist/core/notepad.d.ts +31 -0
- package/dist/core/notepad.js +88 -0
- package/dist/core/paths.d.ts +85 -0
- package/dist/core/paths.js +101 -0
- package/dist/core/plugin-detector.d.ts +44 -0
- package/dist/core/plugin-detector.js +226 -0
- package/dist/core/runtime-detector.d.ts +8 -0
- package/dist/core/runtime-detector.js +49 -0
- package/dist/core/scope-resolver.d.ts +8 -0
- package/dist/core/scope-resolver.js +45 -0
- package/dist/core/session-logger.d.ts +6 -0
- package/dist/core/session-logger.js +111 -0
- package/dist/core/session-store.d.ts +28 -0
- package/dist/core/session-store.js +218 -0
- package/dist/core/settings-lock.d.ts +18 -0
- package/dist/core/settings-lock.js +125 -0
- package/dist/core/spawn.d.ts +3 -0
- package/dist/core/spawn.js +135 -0
- package/dist/core/types.d.ts +108 -0
- package/dist/core/types.js +1 -0
- package/dist/core/uninstall.d.ts +4 -0
- package/dist/core/uninstall.js +307 -0
- package/dist/core/v1-bootstrap.d.ts +26 -0
- package/dist/core/v1-bootstrap.js +155 -0
- package/dist/engine/compound-cli.d.ts +24 -0
- package/dist/engine/compound-cli.js +250 -0
- package/dist/engine/compound-extractor.d.ts +68 -0
- package/dist/engine/compound-extractor.js +860 -0
- package/dist/engine/compound-lifecycle.d.ts +32 -0
- package/dist/engine/compound-lifecycle.js +305 -0
- package/dist/engine/compound-loop.d.ts +32 -0
- package/dist/engine/compound-loop.js +511 -0
- package/dist/engine/match-eval-log.d.ts +139 -0
- package/dist/engine/match-eval-log.js +270 -0
- package/dist/engine/phrase-blocklist.d.ts +119 -0
- package/dist/engine/phrase-blocklist.js +208 -0
- package/dist/engine/skill-promoter.d.ts +20 -0
- package/dist/engine/skill-promoter.js +115 -0
- package/dist/engine/solution-format.d.ts +160 -0
- package/dist/engine/solution-format.js +432 -0
- package/dist/engine/solution-index.d.ts +13 -0
- package/dist/engine/solution-index.js +252 -0
- package/dist/engine/solution-matcher.d.ts +364 -0
- package/dist/engine/solution-matcher.js +656 -0
- package/dist/engine/solution-writer.d.ts +76 -0
- package/dist/engine/solution-writer.js +157 -0
- package/dist/engine/term-matcher.d.ts +81 -0
- package/dist/engine/term-matcher.js +268 -0
- package/dist/engine/term-normalizer.d.ts +116 -0
- package/dist/engine/term-normalizer.js +171 -0
- package/dist/fgx.d.ts +6 -0
- package/dist/fgx.js +42 -0
- package/dist/forge/cli.d.ts +11 -0
- package/dist/forge/cli.js +100 -0
- package/dist/forge/evidence-processor.d.ts +21 -0
- package/dist/forge/evidence-processor.js +87 -0
- package/dist/forge/mismatch-detector.d.ts +44 -0
- package/dist/forge/mismatch-detector.js +83 -0
- package/dist/forge/onboarding-cli.d.ts +6 -0
- package/dist/forge/onboarding-cli.js +89 -0
- package/dist/forge/onboarding.d.ts +25 -0
- package/dist/forge/onboarding.js +122 -0
- package/dist/hooks/compound-reflection.d.ts +45 -0
- package/dist/hooks/compound-reflection.js +82 -0
- package/dist/hooks/context-guard.d.ts +24 -0
- package/dist/hooks/context-guard.js +156 -0
- package/dist/hooks/dangerous-patterns.json +18 -0
- package/dist/hooks/db-guard.d.ts +17 -0
- package/dist/hooks/db-guard.js +105 -0
- package/dist/hooks/hook-config.d.ts +29 -0
- package/dist/hooks/hook-config.js +92 -0
- package/dist/hooks/hook-registry.d.ts +43 -0
- package/dist/hooks/hook-registry.js +31 -0
- package/dist/hooks/hooks-generator.d.ts +49 -0
- package/dist/hooks/hooks-generator.js +99 -0
- package/dist/hooks/intent-classifier.d.ts +12 -0
- package/dist/hooks/intent-classifier.js +62 -0
- package/dist/hooks/keyword-detector.d.ts +25 -0
- package/dist/hooks/keyword-detector.js +389 -0
- package/dist/hooks/notepad-injector.d.ts +18 -0
- package/dist/hooks/notepad-injector.js +51 -0
- package/dist/hooks/permission-handler.d.ts +14 -0
- package/dist/hooks/permission-handler.js +114 -0
- package/dist/hooks/post-tool-failure.d.ts +11 -0
- package/dist/hooks/post-tool-failure.js +118 -0
- package/dist/hooks/post-tool-handlers.d.ts +17 -0
- package/dist/hooks/post-tool-handlers.js +115 -0
- package/dist/hooks/post-tool-use.d.ts +29 -0
- package/dist/hooks/post-tool-use.js +151 -0
- package/dist/hooks/pre-compact.d.ts +10 -0
- package/dist/hooks/pre-compact.js +165 -0
- package/dist/hooks/pre-tool-use.d.ts +31 -0
- package/dist/hooks/pre-tool-use.js +325 -0
- package/dist/hooks/prompt-injection-filter.d.ts +56 -0
- package/dist/hooks/prompt-injection-filter.js +287 -0
- package/dist/hooks/rate-limiter.d.ts +21 -0
- package/dist/hooks/rate-limiter.js +86 -0
- package/dist/hooks/secret-filter.d.ts +14 -0
- package/dist/hooks/secret-filter.js +65 -0
- package/dist/hooks/session-recovery.d.ts +27 -0
- package/dist/hooks/session-recovery.js +406 -0
- package/dist/hooks/shared/atomic-write.d.ts +41 -0
- package/dist/hooks/shared/atomic-write.js +148 -0
- package/dist/hooks/shared/context-budget.d.ts +37 -0
- package/dist/hooks/shared/context-budget.js +45 -0
- package/dist/hooks/shared/file-lock.d.ts +56 -0
- package/dist/hooks/shared/file-lock.js +253 -0
- package/dist/hooks/shared/hook-response.d.ts +33 -0
- package/dist/hooks/shared/hook-response.js +62 -0
- package/dist/hooks/shared/injection-caps.d.ts +39 -0
- package/dist/hooks/shared/injection-caps.js +52 -0
- package/dist/hooks/shared/plugin-signal.d.ts +23 -0
- package/dist/hooks/shared/plugin-signal.js +104 -0
- package/dist/hooks/shared/read-stdin.d.ts +8 -0
- package/dist/hooks/shared/read-stdin.js +63 -0
- package/dist/hooks/shared/sanitize-id.d.ts +7 -0
- package/dist/hooks/shared/sanitize-id.js +9 -0
- package/dist/hooks/shared/sanitize.d.ts +7 -0
- package/dist/hooks/shared/sanitize.js +22 -0
- package/dist/hooks/skill-injector.d.ts +38 -0
- package/dist/hooks/skill-injector.js +285 -0
- package/dist/hooks/slop-detector.d.ts +18 -0
- package/dist/hooks/slop-detector.js +93 -0
- package/dist/hooks/solution-injector.d.ts +58 -0
- package/dist/hooks/solution-injector.js +436 -0
- package/dist/hooks/subagent-tracker.d.ts +10 -0
- package/dist/hooks/subagent-tracker.js +90 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +224 -0
- package/dist/lib.d.ts +14 -0
- package/dist/lib.js +14 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +40 -0
- package/dist/mcp/solution-reader.d.ts +90 -0
- package/dist/mcp/solution-reader.js +273 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +302 -0
- package/dist/preset/facet-catalog.d.ts +17 -0
- package/dist/preset/facet-catalog.js +46 -0
- package/dist/preset/preset-manager.d.ts +31 -0
- package/dist/preset/preset-manager.js +111 -0
- package/dist/renderer/inspect-renderer.d.ts +11 -0
- package/dist/renderer/inspect-renderer.js +123 -0
- package/dist/renderer/rule-renderer.d.ts +18 -0
- package/dist/renderer/rule-renderer.js +159 -0
- package/dist/store/evidence-store.d.ts +23 -0
- package/dist/store/evidence-store.js +58 -0
- package/dist/store/profile-store.d.ts +12 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/recommendation-store.d.ts +22 -0
- package/dist/store/recommendation-store.js +64 -0
- package/dist/store/rule-store.d.ts +22 -0
- package/dist/store/rule-store.js +62 -0
- package/dist/store/session-state-store.d.ts +11 -0
- package/dist/store/session-state-store.js +44 -0
- package/dist/store/types.d.ts +159 -0
- package/dist/store/types.js +7 -0
- package/hooks/hook-registry.json +21 -0
- package/hooks/hooks.json +185 -0
- package/package.json +89 -0
- package/plugin.json +20 -0
- package/scripts/postinstall.js +826 -0
- package/skills/api-design/SKILL.md +262 -0
- package/skills/architecture-decision/SKILL.md +309 -0
- package/skills/ci-cd/SKILL.md +264 -0
- package/skills/code-review/SKILL.md +228 -0
- package/skills/compound/SKILL.md +101 -0
- package/skills/database/SKILL.md +257 -0
- package/skills/debug-detective/SKILL.md +95 -0
- package/skills/docker/SKILL.md +268 -0
- package/skills/documentation/SKILL.md +270 -0
- package/skills/ecomode/SKILL.md +46 -0
- package/skills/frontend/SKILL.md +265 -0
- package/skills/git-master/SKILL.md +86 -0
- package/skills/incident-response/SKILL.md +286 -0
- package/skills/migrate/SKILL.md +96 -0
- package/skills/performance/SKILL.md +282 -0
- package/skills/refactor/SKILL.md +100 -0
- package/skills/security-review/SKILL.md +282 -0
- package/skills/tdd/SKILL.md +178 -0
- package/skills/testing-strategy/SKILL.md +260 -0
- package/starter-pack/solutions/starter-api-error-responses.md +37 -0
- package/starter-pack/solutions/starter-async-patterns.md +40 -0
- package/starter-pack/solutions/starter-caching-strategy.md +40 -0
- package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
- package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
- package/starter-pack/solutions/starter-dependency-injection.md +40 -0
- package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
- package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
- package/starter-pack/solutions/starter-input-validation.md +40 -0
- package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
- package/starter-pack/solutions/starter-refactor-safely.md +38 -0
- package/starter-pack/solutions/starter-secret-management.md +37 -0
- package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
- package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
- package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PostToolUseFailure Hook
|
|
4
|
+
*
|
|
5
|
+
* 도구 실행 실패 시 자동 복구 안내 + 실패 패턴 분석.
|
|
6
|
+
* - 반복 실패 감지 (같은 도구 3회 이상)
|
|
7
|
+
* - 실패 원인별 복구 제안
|
|
8
|
+
* - 컨텍스트 신호에 실패 카운터 기록
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
// debugLog는 향후 확장용으로 import 유지 가능하지만, 현재 미사용
|
|
13
|
+
import { HookError } from '../core/errors.js';
|
|
14
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
15
|
+
import { isHookEnabled } from './hook-config.js';
|
|
16
|
+
import { sanitizeId } from './shared/sanitize-id.js';
|
|
17
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
18
|
+
import { approve, approveWithWarning, failOpen } from './shared/hook-response.js';
|
|
19
|
+
import { STATE_DIR } from '../core/paths.js';
|
|
20
|
+
function getFailureStatePath(sessionId) {
|
|
21
|
+
return path.join(STATE_DIR, `tool-failures-${sanitizeId(sessionId)}.json`);
|
|
22
|
+
}
|
|
23
|
+
function loadFailureState(sessionId) {
|
|
24
|
+
const p = getFailureStatePath(sessionId);
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(p)) {
|
|
27
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
28
|
+
if (data.sessionId === sessionId)
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { /* failure state parse failure — starting fresh, failure history for this session is lost */ }
|
|
33
|
+
return { sessionId, failures: {} };
|
|
34
|
+
}
|
|
35
|
+
function saveFailureState(state) {
|
|
36
|
+
atomicWriteJSON(getFailureStatePath(state.sessionId), state);
|
|
37
|
+
}
|
|
38
|
+
/** 실패 카운터 증가 (context-signals.json) */
|
|
39
|
+
function incrementFailureSignal(sessionId) {
|
|
40
|
+
const signalsPath = path.join(STATE_DIR, 'context-signals.json');
|
|
41
|
+
try {
|
|
42
|
+
let signals = {};
|
|
43
|
+
if (fs.existsSync(signalsPath)) {
|
|
44
|
+
signals = JSON.parse(fs.readFileSync(signalsPath, 'utf-8'));
|
|
45
|
+
if (signals.sessionId !== sessionId)
|
|
46
|
+
signals = {};
|
|
47
|
+
}
|
|
48
|
+
signals.sessionId = sessionId;
|
|
49
|
+
signals.previousFailures = (signals.previousFailures ?? 0) + 1;
|
|
50
|
+
signals.updatedAt = new Date().toISOString();
|
|
51
|
+
atomicWriteJSON(signalsPath, signals);
|
|
52
|
+
}
|
|
53
|
+
catch { /* context-signals.json write failure — failure counter lost, model routing escalation may not trigger */ }
|
|
54
|
+
}
|
|
55
|
+
/** 에러 메시지 기반 복구 제안 */
|
|
56
|
+
export function getRecoverySuggestion(error, toolName) {
|
|
57
|
+
const lower = error.toLowerCase();
|
|
58
|
+
if (/timeout|timed out/.test(lower)) {
|
|
59
|
+
return 'Timeout occurred. Split into smaller units and retry.';
|
|
60
|
+
}
|
|
61
|
+
if (/enoent|no such file|not found/.test(lower)) {
|
|
62
|
+
return 'File/path does not exist. Check the path.';
|
|
63
|
+
}
|
|
64
|
+
if (/eacces|permission denied/.test(lower)) {
|
|
65
|
+
return 'Permission denied. Check file permissions.';
|
|
66
|
+
}
|
|
67
|
+
if (/syntax error|syntaxerror/.test(lower)) {
|
|
68
|
+
return 'Syntax error. Review the code again.';
|
|
69
|
+
}
|
|
70
|
+
if (/enospc|no space/.test(lower)) {
|
|
71
|
+
return 'Disk space is insufficient.';
|
|
72
|
+
}
|
|
73
|
+
if (/old_string.*not found|not unique/i.test(lower)) {
|
|
74
|
+
return 'Edit tool old_string not found in file. Use Read to check the current file content and retry.';
|
|
75
|
+
}
|
|
76
|
+
return `${toolName} tool failed. Try a different approach.`;
|
|
77
|
+
}
|
|
78
|
+
async function main() {
|
|
79
|
+
const data = await readStdinJSON();
|
|
80
|
+
if (!isHookEnabled('post-tool-failure')) {
|
|
81
|
+
console.log(approve());
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!data) {
|
|
85
|
+
console.log(approve());
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const toolName = data.tool_name ?? data.toolName ?? 'Unknown';
|
|
89
|
+
const error = data.error ?? '';
|
|
90
|
+
const sessionId = data.session_id ?? 'default';
|
|
91
|
+
// 실패 카운터 업데이트
|
|
92
|
+
const state = loadFailureState(sessionId);
|
|
93
|
+
if (!state.failures[toolName]) {
|
|
94
|
+
state.failures[toolName] = { count: 0, lastError: '', lastAt: '' };
|
|
95
|
+
}
|
|
96
|
+
state.failures[toolName].count += 1;
|
|
97
|
+
state.failures[toolName].lastError = error.slice(0, 200);
|
|
98
|
+
state.failures[toolName].lastAt = new Date().toISOString();
|
|
99
|
+
saveFailureState(state);
|
|
100
|
+
// 컨텍스트 신호 업데이트
|
|
101
|
+
incrementFailureSignal(sessionId);
|
|
102
|
+
const failCount = state.failures[toolName].count;
|
|
103
|
+
const suggestion = getRecoverySuggestion(error, toolName);
|
|
104
|
+
// 3회 이상 반복 실패 시 강화된 경고
|
|
105
|
+
if (failCount >= 3) {
|
|
106
|
+
console.log(approveWithWarning(`<compound-failure-warning>\n[Forgen] ⚠ ${toolName} tool has failed ${failCount} times in this session.\nRecovery suggestion: ${suggestion}\nTry a different approach or analyze the issue before retrying.\n</compound-failure-warning>`));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// 일반 실패 안내
|
|
110
|
+
console.log(approveWithWarning(`<compound-failure-info>\n[Forgen] ${toolName} failed (${failCount} time(s)). ${suggestion}\n</compound-failure-info>`));
|
|
111
|
+
}
|
|
112
|
+
main().catch((e) => {
|
|
113
|
+
const hookErr = new HookError(e instanceof Error ? e.message : String(e), {
|
|
114
|
+
hookName: 'post-tool-failure', eventType: 'PostToolUseFailure', cause: e,
|
|
115
|
+
});
|
|
116
|
+
process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
|
|
117
|
+
console.log(failOpen());
|
|
118
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — PostToolUse Handlers (extracted from post-tool-use.ts)
|
|
3
|
+
*
|
|
4
|
+
* Compound negative/success 신호 감지, 컨텍스트 실패 카운터,
|
|
5
|
+
* 솔루션 negative evidence 업데이트 등 post-tool 분석 핸들러.
|
|
6
|
+
*/
|
|
7
|
+
/** 세션의 실패 카운터 증가 (컨텍스트 신호 수집) */
|
|
8
|
+
export declare function incrementFailureCounter(sessionId: string): void;
|
|
9
|
+
/** Compound v3: detect negative signals after tool execution */
|
|
10
|
+
export declare function checkCompoundNegative(toolName: string, toolResponse: string, sessionId: string): void;
|
|
11
|
+
/** Compound v3: Micro-extraction — detect success moments and return hint */
|
|
12
|
+
export declare function getCompoundSuccessHint(toolName: string, toolResponse: string, sessionId: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Update negative evidence counter in solution file.
|
|
15
|
+
* PR2b: solution-writer.incrementEvidence로 위임. lock + fresh re-read + atomic write.
|
|
16
|
+
*/
|
|
17
|
+
export declare function updateNegativeEvidence(solutionName: string): void;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — PostToolUse Handlers (extracted from post-tool-use.ts)
|
|
3
|
+
*
|
|
4
|
+
* Compound negative/success 신호 감지, 컨텍스트 실패 카운터,
|
|
5
|
+
* 솔루션 negative evidence 업데이트 등 post-tool 분석 핸들러.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { createLogger } from '../core/logger.js';
|
|
10
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
11
|
+
import { sanitizeId } from './shared/sanitize-id.js';
|
|
12
|
+
import { incrementEvidence } from '../engine/solution-writer.js';
|
|
13
|
+
import { classifyMatch, shouldAttribute } from '../engine/term-matcher.js';
|
|
14
|
+
import { detectErrorPattern } from './post-tool-use.js';
|
|
15
|
+
import { STATE_DIR } from '../core/paths.js';
|
|
16
|
+
const log = createLogger('post-tool-handlers');
|
|
17
|
+
const CONTEXT_SIGNALS_PATH = path.join(STATE_DIR, 'context-signals.json');
|
|
18
|
+
/** 세션의 실패 카운터 증가 (컨텍스트 신호 수집) */
|
|
19
|
+
export function incrementFailureCounter(sessionId) {
|
|
20
|
+
try {
|
|
21
|
+
let signals = {};
|
|
22
|
+
if (fs.existsSync(CONTEXT_SIGNALS_PATH)) {
|
|
23
|
+
signals = JSON.parse(fs.readFileSync(CONTEXT_SIGNALS_PATH, 'utf-8'));
|
|
24
|
+
if (signals.sessionId !== sessionId)
|
|
25
|
+
signals = {};
|
|
26
|
+
}
|
|
27
|
+
signals.sessionId = sessionId;
|
|
28
|
+
signals.previousFailures = (signals.previousFailures ?? 0) + 1;
|
|
29
|
+
signals.updatedAt = new Date().toISOString();
|
|
30
|
+
atomicWriteJSON(CONTEXT_SIGNALS_PATH, signals);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
log.debug('context signals write failed — failure count may be lost', e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Compound v3: detect negative signals after tool execution */
|
|
37
|
+
export function checkCompoundNegative(toolName, toolResponse, sessionId) {
|
|
38
|
+
if (toolName !== 'Bash')
|
|
39
|
+
return;
|
|
40
|
+
if (!toolResponse || toolResponse.length < 5)
|
|
41
|
+
return;
|
|
42
|
+
const negativePatterns = [
|
|
43
|
+
/error\s*TS\d+/i,
|
|
44
|
+
/BUILD FAILED/i,
|
|
45
|
+
/test.*fail/i,
|
|
46
|
+
/FAIL\s+tests?\//i,
|
|
47
|
+
/npm ERR!/i,
|
|
48
|
+
/exit code [1-9]/i,
|
|
49
|
+
/compilation error/i,
|
|
50
|
+
/SyntaxError/i,
|
|
51
|
+
];
|
|
52
|
+
const isNegative = negativePatterns.some(p => p.test(toolResponse));
|
|
53
|
+
if (!isNegative)
|
|
54
|
+
return;
|
|
55
|
+
const cachePath = path.join(STATE_DIR, `injection-cache-${sanitizeId(sessionId)}.json`);
|
|
56
|
+
if (!fs.existsSync(cachePath))
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
60
|
+
if (!Array.isArray(cache.solutions))
|
|
61
|
+
return;
|
|
62
|
+
// PR3: term-matcher로 word-boundary 매칭 + NEGATIVE_TERM_BLOCKLIST +
|
|
63
|
+
// match strength classification. 이전 substring 매칭은 `api` 솔루션이
|
|
64
|
+
// `rapid build failed`에 잘못 매칭되는 등 over-attribution이 심각했다.
|
|
65
|
+
const experiments = cache.solutions.filter((s) => s.status === 'experiment');
|
|
66
|
+
for (const sol of experiments) {
|
|
67
|
+
const classification = classifyMatch(toolResponse, Array.isArray(sol.identifiers) ? sol.identifiers : [], Array.isArray(sol.tags) ? sol.tags : []);
|
|
68
|
+
// 'strong' (identifier 매칭) 또는 'multi' (tag ≥2 매칭)만 attribute.
|
|
69
|
+
// 'weak' (tag 1개)와 'none'은 over-attribution 위험으로 무시.
|
|
70
|
+
if (!shouldAttribute(classification))
|
|
71
|
+
continue;
|
|
72
|
+
updateNegativeEvidence(sol.name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
log.debug('compound negative 체크 실패', e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Compound v3: Micro-extraction — detect success moments and return hint */
|
|
80
|
+
export function getCompoundSuccessHint(toolName, toolResponse, sessionId) {
|
|
81
|
+
if (toolName !== 'Bash' || !toolResponse)
|
|
82
|
+
return '';
|
|
83
|
+
if (detectErrorPattern(toolResponse))
|
|
84
|
+
return '';
|
|
85
|
+
const hints = [];
|
|
86
|
+
if (/\d+\s*(passed|tests?\s*passed)|all\s*tests?\s*pass/i.test(toolResponse) && !/fail|error/i.test(toolResponse)) {
|
|
87
|
+
hints.push('Tests passed — record effective patterns with /compound');
|
|
88
|
+
}
|
|
89
|
+
if (/build\s*(succeeded|success|done)|compiled?\s*successfully/i.test(toolResponse)) {
|
|
90
|
+
hints.push('Build success — record implementation patterns with /compound');
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(CONTEXT_SIGNALS_PATH)) {
|
|
94
|
+
const signals = JSON.parse(fs.readFileSync(CONTEXT_SIGNALS_PATH, 'utf-8'));
|
|
95
|
+
if (signals.sessionId === sessionId && (signals.previousFailures ?? 0) >= 2) {
|
|
96
|
+
hints.push('Error resolved after multiple failures — record the root cause and fix with /compound');
|
|
97
|
+
signals.previousFailures = 0;
|
|
98
|
+
atomicWriteJSON(CONTEXT_SIGNALS_PATH, signals);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
log.debug('error resolution detection failed', e);
|
|
104
|
+
}
|
|
105
|
+
if (hints.length === 0)
|
|
106
|
+
return '';
|
|
107
|
+
return `<compound-success-hint>\n${hints.map(h => `- ${h}`).join('\n')}\n</compound-success-hint>`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Update negative evidence counter in solution file.
|
|
111
|
+
* PR2b: solution-writer.incrementEvidence로 위임. lock + fresh re-read + atomic write.
|
|
112
|
+
*/
|
|
113
|
+
export function updateNegativeEvidence(solutionName) {
|
|
114
|
+
incrementEvidence(solutionName, 'negative');
|
|
115
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PostToolUse Hook
|
|
4
|
+
*
|
|
5
|
+
* 도구 실행 후 결과 검증 + 파일 변경 추적.
|
|
6
|
+
* Compound/workflow 핸들러는 ./post-tool-handlers.ts에 분리.
|
|
7
|
+
*/
|
|
8
|
+
interface ModifiedFilesState {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
files: Record<string, {
|
|
11
|
+
count: number;
|
|
12
|
+
lastModified: string;
|
|
13
|
+
tool: string;
|
|
14
|
+
}>;
|
|
15
|
+
toolCallCount: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const ERROR_PATTERNS: Array<{
|
|
18
|
+
pattern: RegExp;
|
|
19
|
+
description: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function detectErrorPattern(text: string): {
|
|
22
|
+
pattern: RegExp;
|
|
23
|
+
description: string;
|
|
24
|
+
} | null;
|
|
25
|
+
export declare function trackModifiedFile(state: ModifiedFilesState, filePath: string, toolName: string): {
|
|
26
|
+
state: ModifiedFilesState;
|
|
27
|
+
count: number;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PostToolUse Hook
|
|
4
|
+
*
|
|
5
|
+
* 도구 실행 후 결과 검증 + 파일 변경 추적.
|
|
6
|
+
* Compound/workflow 핸들러는 ./post-tool-handlers.ts에 분리.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { createLogger } from '../core/logger.js';
|
|
11
|
+
const log = createLogger('post-tool-use');
|
|
12
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
13
|
+
import { sanitizeId } from './shared/sanitize-id.js';
|
|
14
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
15
|
+
import { saveCheckpoint } from './session-recovery.js';
|
|
16
|
+
// v1: recordWriteContent (regex 선호 감지) 제거
|
|
17
|
+
import { incrementFailureCounter, checkCompoundNegative, getCompoundSuccessHint } from './post-tool-handlers.js';
|
|
18
|
+
import { isHookEnabled } from './hook-config.js';
|
|
19
|
+
import { approve, approveWithWarning, failOpen } from './shared/hook-response.js';
|
|
20
|
+
import { STATE_DIR } from '../core/paths.js';
|
|
21
|
+
// ── State management ──
|
|
22
|
+
function getModifiedFilesPath(sessionId) {
|
|
23
|
+
return path.join(STATE_DIR, `modified-files-${sanitizeId(sessionId)}.json`);
|
|
24
|
+
}
|
|
25
|
+
function loadModifiedFiles(sessionId) {
|
|
26
|
+
try {
|
|
27
|
+
const filePath = getModifiedFilesPath(sessionId);
|
|
28
|
+
if (fs.existsSync(filePath)) {
|
|
29
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
log.debug('modified files state load failed — starting fresh', e);
|
|
34
|
+
}
|
|
35
|
+
return { sessionId, files: {}, toolCallCount: 0 };
|
|
36
|
+
}
|
|
37
|
+
function saveModifiedFiles(state) {
|
|
38
|
+
atomicWriteJSON(getModifiedFilesPath(state.sessionId), state);
|
|
39
|
+
}
|
|
40
|
+
// ── Exported utilities ──
|
|
41
|
+
export const ERROR_PATTERNS = [
|
|
42
|
+
{ pattern: /ENOENT|no such file/i, description: 'file not found' },
|
|
43
|
+
{ pattern: /EACCES|permission denied/i, description: 'permission denied' },
|
|
44
|
+
{ pattern: /ENOSPC|no space left/i, description: 'disk space insufficient' },
|
|
45
|
+
{ pattern: /syntax error|SyntaxError/i, description: 'syntax error' },
|
|
46
|
+
{ pattern: /segmentation fault|SIGSEGV/i, description: 'segmentation fault' },
|
|
47
|
+
{ pattern: /out of memory|OOM/i, description: 'out of memory' },
|
|
48
|
+
];
|
|
49
|
+
export function detectErrorPattern(text) {
|
|
50
|
+
for (const entry of ERROR_PATTERNS) {
|
|
51
|
+
if (entry.pattern.test(text))
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export function trackModifiedFile(state, filePath, toolName) {
|
|
57
|
+
const existing = state.files[filePath];
|
|
58
|
+
const count = (existing?.count ?? 0) + 1;
|
|
59
|
+
state.files[filePath] = {
|
|
60
|
+
count,
|
|
61
|
+
lastModified: new Date().toISOString(),
|
|
62
|
+
tool: toolName,
|
|
63
|
+
};
|
|
64
|
+
return { state, count };
|
|
65
|
+
}
|
|
66
|
+
// ── Main flow ──
|
|
67
|
+
async function main() {
|
|
68
|
+
const data = await readStdinJSON();
|
|
69
|
+
if (!data) {
|
|
70
|
+
console.log(approve());
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!isHookEnabled('post-tool-use')) {
|
|
74
|
+
console.log(approve());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const toolName = data.tool_name ?? data.toolName ?? '';
|
|
78
|
+
const toolInput = data.tool_input ?? data.toolInput ?? {};
|
|
79
|
+
const toolResponse = data.tool_response ?? data.toolOutput ?? '';
|
|
80
|
+
const sessionId = data.session_id ?? 'default';
|
|
81
|
+
const modState = loadModifiedFiles(sessionId);
|
|
82
|
+
modState.toolCallCount = (modState.toolCallCount ?? 0) + 1;
|
|
83
|
+
const messages = [];
|
|
84
|
+
// 1. Checkpoint (every 5 calls)
|
|
85
|
+
if (modState.toolCallCount % 5 === 0) {
|
|
86
|
+
try {
|
|
87
|
+
saveCheckpoint({
|
|
88
|
+
sessionId, mode: 'active',
|
|
89
|
+
modifiedFiles: Object.keys(modState.files),
|
|
90
|
+
lastToolCall: toolName,
|
|
91
|
+
toolCallCount: modState.toolCallCount,
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
cwd: data.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
log.debug('체크포인트 저장 실패', e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 2. File change tracking (Write, Edit)
|
|
101
|
+
if (toolName === 'Write' || toolName === 'Edit') {
|
|
102
|
+
const filePath = toolInput.file_path ?? toolInput.filePath ?? '';
|
|
103
|
+
if (filePath) {
|
|
104
|
+
try {
|
|
105
|
+
const { count } = trackModifiedFile(modState, filePath, toolName);
|
|
106
|
+
if (count >= 5) {
|
|
107
|
+
messages.push(`<compound-tool-warning>\n[Forgen] ⚠ ${path.basename(filePath)} has been modified ${count} times.\nConsider redesigning the overall structure and restarting.\n</compound-tool-warning>`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
log.debug('파일 변경 추적 실패', e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// v1: regex 기반 write content 학습 제거. Evidence 기반으로 전환됨.
|
|
115
|
+
}
|
|
116
|
+
// 4. Bash error detection
|
|
117
|
+
if (toolName === 'Bash' && toolResponse) {
|
|
118
|
+
const errorMatch = detectErrorPattern(toolResponse);
|
|
119
|
+
if (errorMatch) {
|
|
120
|
+
incrementFailureCounter(sessionId);
|
|
121
|
+
messages.push(`<compound-tool-info>\n[Forgen] Error pattern detected in execution result: "${errorMatch.description}". Review may be needed.\n</compound-tool-info>`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 5. Compound negative signal (non-blocking)
|
|
125
|
+
try {
|
|
126
|
+
checkCompoundNegative(toolName, toolResponse, sessionId);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
log.debug('compound negative check 실패', e);
|
|
130
|
+
}
|
|
131
|
+
// 6. Compound success hint (non-blocking)
|
|
132
|
+
try {
|
|
133
|
+
const successHint = getCompoundSuccessHint(toolName, toolResponse, sessionId);
|
|
134
|
+
if (successHint)
|
|
135
|
+
messages.push(successHint);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
log.debug('success hint generation 실패', e);
|
|
139
|
+
}
|
|
140
|
+
saveModifiedFiles(modState);
|
|
141
|
+
if (messages.length > 0) {
|
|
142
|
+
console.log(approveWithWarning(messages.join('\n')));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(approve());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
main().catch((e) => {
|
|
149
|
+
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
150
|
+
console.log(failOpen());
|
|
151
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PreCompact Hook
|
|
4
|
+
*
|
|
5
|
+
* 컨텍스트 압축(compaction) 전 상태 보존.
|
|
6
|
+
* - 현재 활성 모드 상태 스냅샷
|
|
7
|
+
* - 진행 중인 작업 요약 저장
|
|
8
|
+
* - handoff 파일 생성 (압축 후 복구용)
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { createLogger } from '../core/logger.js';
|
|
13
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
14
|
+
import { isHookEnabled } from './hook-config.js';
|
|
15
|
+
import { approve, approveWithWarning, failOpen } from './shared/hook-response.js';
|
|
16
|
+
import { HANDOFFS_DIR, ME_BEHAVIOR, STATE_DIR } from '../core/paths.js';
|
|
17
|
+
const log = createLogger('pre-compact');
|
|
18
|
+
/** 활성 모드 상태 수집 */
|
|
19
|
+
function collectActiveStates() {
|
|
20
|
+
const active = [];
|
|
21
|
+
if (!fs.existsSync(STATE_DIR))
|
|
22
|
+
return active;
|
|
23
|
+
try {
|
|
24
|
+
for (const f of fs.readdirSync(STATE_DIR)) {
|
|
25
|
+
if (!f.endsWith('-state.json') || f.startsWith('context-guard') || f.startsWith('skill-cache'))
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(fs.readFileSync(path.join(STATE_DIR, f), 'utf-8'));
|
|
29
|
+
if (data.active) {
|
|
30
|
+
active.push({ mode: f.replace('-state.json', ''), data });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
log.debug(`상태 파일 파싱 실패 — skip`, e);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
log.debug('상태 디렉토리 읽기 실패', e);
|
|
40
|
+
}
|
|
41
|
+
return active;
|
|
42
|
+
}
|
|
43
|
+
/** compaction 전 스냅샷 저장 */
|
|
44
|
+
function saveCompactionSnapshot(sessionId) {
|
|
45
|
+
const activeStates = collectActiveStates();
|
|
46
|
+
if (activeStates.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
fs.mkdirSync(HANDOFFS_DIR, { recursive: true });
|
|
49
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
50
|
+
const snapshotPath = path.join(HANDOFFS_DIR, `${timestamp}-pre-compact.md`);
|
|
51
|
+
const lines = [
|
|
52
|
+
'# Pre-Compaction Snapshot',
|
|
53
|
+
`- Session: ${sessionId}`,
|
|
54
|
+
`- Time: ${new Date().toISOString()}`,
|
|
55
|
+
`- Reason: context compaction`,
|
|
56
|
+
'',
|
|
57
|
+
'## Active Modes',
|
|
58
|
+
];
|
|
59
|
+
for (const { mode, data } of activeStates) {
|
|
60
|
+
lines.push(`### ${mode}`);
|
|
61
|
+
lines.push(`- Prompt: ${data.prompt ?? 'N/A'}`);
|
|
62
|
+
lines.push(`- Started: ${data.startedAt ?? 'N/A'}`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
lines.push('## Recovery');
|
|
66
|
+
lines.push('This snapshot was automatically created before compaction.');
|
|
67
|
+
lines.push('Active modes are preserved in state files even after compaction.');
|
|
68
|
+
fs.writeFileSync(snapshotPath, lines.join('\n'));
|
|
69
|
+
return snapshotPath;
|
|
70
|
+
}
|
|
71
|
+
/** 7일 이상 된 handoff 파일 정리 */
|
|
72
|
+
function cleanOldHandoffs() {
|
|
73
|
+
if (!fs.existsSync(HANDOFFS_DIR))
|
|
74
|
+
return;
|
|
75
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
try {
|
|
78
|
+
for (const f of fs.readdirSync(HANDOFFS_DIR)) {
|
|
79
|
+
const p = path.join(HANDOFFS_DIR, f);
|
|
80
|
+
const stat = fs.statSync(p);
|
|
81
|
+
if (now - stat.mtimeMs > MAX_AGE_MS) {
|
|
82
|
+
fs.unlinkSync(p);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
log.debug('old handoff cleanup failed — stale files may remain in handoffs dir', e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function main() {
|
|
91
|
+
const data = await readStdinJSON() ?? {};
|
|
92
|
+
if (!isHookEnabled('pre-compact')) {
|
|
93
|
+
console.log(approve());
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const sessionId = data.session_id ?? 'default';
|
|
97
|
+
// 오래된 handoff 정리
|
|
98
|
+
cleanOldHandoffs();
|
|
99
|
+
// 기존 behavioral 패턴 목록 로드 (중복 방지)
|
|
100
|
+
let existingSolutions = [];
|
|
101
|
+
try {
|
|
102
|
+
const solDir = ME_BEHAVIOR;
|
|
103
|
+
if (fs.existsSync(solDir)) {
|
|
104
|
+
existingSolutions = fs.readdirSync(solDir).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { /* ignore */ }
|
|
108
|
+
const existingList = existingSolutions.length > 0
|
|
109
|
+
? `\nAlready captured (skip these): ${existingSolutions.slice(-10).join(', ')}`
|
|
110
|
+
: '';
|
|
111
|
+
const compoundHint = `
|
|
112
|
+
<forgen-compound-extract>
|
|
113
|
+
Context is about to be compacted. Before it's lost, analyze this conversation and extract the USER's behavioral patterns.
|
|
114
|
+
|
|
115
|
+
DO NOT extract code patterns or technical solutions. Extract HOW THE USER WORKS:
|
|
116
|
+
- Decision-making style (e.g., "always verifies before trusting", "prefers data over intuition")
|
|
117
|
+
- Communication preferences (e.g., "wants Korean responses", "hates long explanations")
|
|
118
|
+
- Workflow sequences — repeated action ORDER patterns (e.g., "always runs tests → checks coverage → then commits", "bug fix: reproduce first → write test → fix → verify")
|
|
119
|
+
- Conditional approaches (e.g., "for refactoring: ensures tests pass first, then restructures", "for PRs: security check → test review → code quality")
|
|
120
|
+
- Values/philosophy (e.g., "quality over speed", "pragmatic over theoretical")
|
|
121
|
+
|
|
122
|
+
For each pattern found, write a file to ~/.forgen/me/behavior/{slug}.md in this EXACT format:
|
|
123
|
+
\`\`\`
|
|
124
|
+
---
|
|
125
|
+
name: "{slug}"
|
|
126
|
+
version: 1
|
|
127
|
+
kind: "{thinking|preference|workflow}"
|
|
128
|
+
observedCount: 1
|
|
129
|
+
confidence: 0.6
|
|
130
|
+
tags: ["thinking", "{category}", "{specific-tag}"]
|
|
131
|
+
created: "${new Date().toISOString().split('T')[0]}"
|
|
132
|
+
updated: "${new Date().toISOString().split('T')[0]}"
|
|
133
|
+
source: "pre-compact"
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Context
|
|
137
|
+
{When and why this pattern was observed in this conversation}
|
|
138
|
+
|
|
139
|
+
## Content
|
|
140
|
+
{Concrete description of the behavioral pattern, with specific examples from this session}
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
Rules:
|
|
144
|
+
- Extract 0-3 patterns MAX (quality over quantity)
|
|
145
|
+
- Skip if nothing non-obvious was observed
|
|
146
|
+
- Skip patterns that are trivially obvious ("uses TypeScript")
|
|
147
|
+
- Each pattern must be specific enough to change Claude's behavior in future sessions${existingList}
|
|
148
|
+
</forgen-compound-extract>`;
|
|
149
|
+
// 스냅샷 저장
|
|
150
|
+
try {
|
|
151
|
+
const snapshotPath = saveCompactionSnapshot(sessionId);
|
|
152
|
+
if (snapshotPath) {
|
|
153
|
+
console.log(approveWithWarning(`<compound-compact-info>\n[Forgen] Pre-compaction state snapshot saved: ${path.basename(snapshotPath)}\nActive modes are preserved after compaction.\n</compound-compact-info>\n${compoundHint}`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
log.debug('스냅샷 저장 실패', e);
|
|
159
|
+
}
|
|
160
|
+
console.log(approveWithWarning(compoundHint));
|
|
161
|
+
}
|
|
162
|
+
main().catch((e) => {
|
|
163
|
+
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
164
|
+
console.log(failOpen());
|
|
165
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PreToolUse Hook
|
|
4
|
+
*
|
|
5
|
+
* 도구 실행 전 위험 명령어 차단 및 컨텍스트 리마인더 주입.
|
|
6
|
+
* - rm -rf, git push --force 등 위험 패턴 감지
|
|
7
|
+
* - 활성 모드 상태 리마인더 주입
|
|
8
|
+
*/
|
|
9
|
+
interface DangerousPatternEntry {
|
|
10
|
+
pattern: RegExp;
|
|
11
|
+
description: string;
|
|
12
|
+
severity: 'block' | 'warn';
|
|
13
|
+
}
|
|
14
|
+
/** 위험 Bash 명령어 패턴 (패키지 내장 + 사용자 커스텀 병합) */
|
|
15
|
+
export declare const DANGEROUS_PATTERNS: DangerousPatternEntry[];
|
|
16
|
+
/** 위험 명령어 검사 (순수 함수) */
|
|
17
|
+
export declare function checkDangerousCommand(toolName: string, toolInput: Record<string, unknown> | string): {
|
|
18
|
+
action: 'block' | 'warn' | 'pass';
|
|
19
|
+
description?: string;
|
|
20
|
+
command?: string;
|
|
21
|
+
};
|
|
22
|
+
/** 카운터 기반 리마인더 표시 여부 판정 (순수 함수 — I/O 없음) */
|
|
23
|
+
export declare function shouldShowReminder(count: number, interval?: number): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Update evidence counter in a solution file.
|
|
26
|
+
* PR2b: solution-writer.incrementEvidence로 위임. lock + fresh re-read + atomic write.
|
|
27
|
+
*
|
|
28
|
+
* Exported for use by solution-injector.
|
|
29
|
+
*/
|
|
30
|
+
export declare function updateSolutionEvidence(solutionName: string, field: 'reflected' | 'negative' | 'injected' | 'sessions' | 'reExtracted'): void;
|
|
31
|
+
export {};
|