@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,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Keyword Detector Hook
|
|
4
|
+
*
|
|
5
|
+
* Claude Code UserPromptSubmit 훅으로 등록.
|
|
6
|
+
* 사용자 프롬프트에서 매직 키워드를 감지하여 해당 스킬을 주입합니다.
|
|
7
|
+
*
|
|
8
|
+
* stdin: JSON { prompt: string, ... }
|
|
9
|
+
* stdout: JSON { result: "block"|"approve", message?: string }
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { createLogger } from '../core/logger.js';
|
|
15
|
+
const log = createLogger('keyword-detector');
|
|
16
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
17
|
+
import { isHookEnabled } from './hook-config.js';
|
|
18
|
+
import { truncateContent, INJECTION_CAPS } from './shared/injection-caps.js';
|
|
19
|
+
import { sanitizeForDetection } from './shared/sanitize.js';
|
|
20
|
+
// v1: prompt-learner (regex 선호 감지) 제거 — Evidence 기반으로 전환
|
|
21
|
+
// v1: pack-config (레거시 팩) 제거 — quality/autonomy pack으로 전환
|
|
22
|
+
import { ALL_MODES, FORGEN_HOME, ME_DIR, PACKS_DIR, STATE_DIR } from '../core/paths.js';
|
|
23
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
24
|
+
import { escapeAllXmlTags } from './prompt-injection-filter.js';
|
|
25
|
+
import { getSkillConflicts } from '../core/plugin-detector.js';
|
|
26
|
+
import { approve, approveWithContext, failOpen } from './shared/hook-response.js';
|
|
27
|
+
/** Escape a string for safe use in XML attribute values */
|
|
28
|
+
function escapeXmlAttr(s) {
|
|
29
|
+
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
30
|
+
}
|
|
31
|
+
const WORKFLOW_TRACKED_INJECTS = new Set(['benchmark']);
|
|
32
|
+
export function shouldTrackWorkflowActivation(match) {
|
|
33
|
+
if (match.type === 'inject')
|
|
34
|
+
return WORKFLOW_TRACKED_INJECTS.has(match.keyword);
|
|
35
|
+
return match.type === 'skill';
|
|
36
|
+
}
|
|
37
|
+
// sanitizeForDetection은 shared/sanitize.ts에서 import
|
|
38
|
+
// ── 키워드 우선순위 (높은 것부터) ──
|
|
39
|
+
// "team", "analyze" 등 일상어와 겹치는 키워드는 명시적 접두어 필요
|
|
40
|
+
export const KEYWORD_PATTERNS = [
|
|
41
|
+
// 취소 — cancel-ralph 등 복합 취소를 단일 키워드보다 먼저 매칭
|
|
42
|
+
{ pattern: /\b(cancelforgen|stopforgen|cancel[- ]?compound)\b/i, keyword: 'cancel', type: 'cancel' },
|
|
43
|
+
{ pattern: /\bcancel[- ]?ralph\b|랄프\s*(?:취소|중단|종료|멈춰)/i, keyword: 'cancel-ralph', type: 'cancel' },
|
|
44
|
+
// 핵심 모드 — ralph는 명시적 모드 호출만 매칭 (false positive 방지)
|
|
45
|
+
{ pattern: /(?:^|\n)\s*ralph\s*$|ralph\s+(?:mode|모드|해|해줘|시작|실행)/im, keyword: 'ralph', type: 'skill', skill: 'ralph' },
|
|
46
|
+
{ pattern: /\bautopilot\b/i, keyword: 'autopilot', type: 'skill', skill: 'autopilot' },
|
|
47
|
+
{ pattern: /(?:\bteam[- ]?mode\b|(?:^|\s)--team\b)/i, keyword: 'team', type: 'skill', skill: 'team' },
|
|
48
|
+
// 확장 모드
|
|
49
|
+
{ pattern: /\b(ulw|ultrawork)\b/i, keyword: 'ultrawork', type: 'skill', skill: 'ultrawork' },
|
|
50
|
+
{ pattern: /\bccg\b/i, keyword: 'ccg', type: 'skill', skill: 'ccg' },
|
|
51
|
+
{ pattern: /\bralplan\b/i, keyword: 'ralplan', type: 'skill', skill: 'ralplan' },
|
|
52
|
+
{ pattern: /\bdeep[- ]?interview\b/i, keyword: 'deep-interview', type: 'skill', skill: 'deep-interview' },
|
|
53
|
+
{ pattern: /\bpipeline\b/i, keyword: 'pipeline', type: 'skill', skill: 'pipeline' },
|
|
54
|
+
{ pattern: /\b(ecomode|에코\s*모드|토큰\s*절약)\b/i, keyword: 'ecomode', type: 'skill', skill: 'ecomode' },
|
|
55
|
+
// 인젝션 모드
|
|
56
|
+
{ pattern: /\bultrathink\b/i, keyword: 'ultrathink', type: 'inject' },
|
|
57
|
+
{ pattern: /\bdeepsearch\b/i, keyword: 'deepsearch', type: 'inject' },
|
|
58
|
+
{ pattern: /(?:^|\s)tdd(?:\s+(?:모드|mode|방식|으로|해|해줘|시작|적용)|\s*$)/im, keyword: 'tdd', type: 'skill', skill: 'tdd' },
|
|
59
|
+
{ pattern: /(?:code[- ]?review|코드\s*리뷰)\s*(?:해|해줘|시작|해봐|부탁|mode|모드)/i, keyword: 'code-review', type: 'skill', skill: 'code-review' },
|
|
60
|
+
{ pattern: /(?:security[- ]?review|보안\s*리뷰|보안\s*검토)\s*(?:해|해줘|시작|해봐|부탁|mode|모드)/i, keyword: 'security-review', type: 'skill', skill: 'security-review' },
|
|
61
|
+
// 실용 스킬 — 명시적 모드 호출만 매칭 (일상 단어 false positive 방지)
|
|
62
|
+
{ pattern: /\bgit[- ]?master\b/i, keyword: 'git-master', type: 'skill', skill: 'git-master' },
|
|
63
|
+
{ pattern: /\b(benchmark|벤치마크)\s*(?:mode|모드|해|해줘|시작|실행|돌려)|성능\s*측정/i, keyword: 'benchmark', type: 'inject' },
|
|
64
|
+
{ pattern: /\b(migrate|마이그레이션)\s*(?:mode|모드|해|해줘|시작|실행|진행)/i, keyword: 'migrate', type: 'skill', skill: 'migrate' },
|
|
65
|
+
{ pattern: /\b(debug[- ]?detective|디버그\s*탐정|체계적\s*디버깅)\b/i, keyword: 'debug-detective', type: 'skill', skill: 'debug-detective' },
|
|
66
|
+
{ pattern: /\b(refactor|리팩토링|리팩터)\s*(?:mode|모드|해|해줘|시작|실행|진행)/i, keyword: 'refactor', type: 'skill', skill: 'refactor' },
|
|
67
|
+
];
|
|
68
|
+
// ── 인젝션 메시지 ──
|
|
69
|
+
const INJECT_MESSAGES = {
|
|
70
|
+
ultrathink: `<compound-think-mode>
|
|
71
|
+
EXTENDED THINKING MODE ACTIVATED.
|
|
72
|
+
Before responding, engage in deep, thorough reasoning. Consider multiple approaches,
|
|
73
|
+
evaluate trade-offs, and explore edge cases. Your thinking should be comprehensive
|
|
74
|
+
and rigorous. Take your time — quality over speed.
|
|
75
|
+
</compound-think-mode>`,
|
|
76
|
+
deepsearch: `<compound-deepsearch>
|
|
77
|
+
DEEP SEARCH MODE ACTIVATED.
|
|
78
|
+
Perform comprehensive codebase exploration before answering:
|
|
79
|
+
1. Use Glob to map the full directory structure
|
|
80
|
+
2. Use Grep to find all relevant patterns and references
|
|
81
|
+
3. Read key files to understand architecture
|
|
82
|
+
4. Cross-reference findings across files
|
|
83
|
+
5. Present a complete, evidence-based analysis
|
|
84
|
+
</compound-deepsearch>`,
|
|
85
|
+
tdd: `<compound-tdd>
|
|
86
|
+
TDD MODE ACTIVATED.
|
|
87
|
+
Follow strict Test-Driven Development:
|
|
88
|
+
1. Write the failing test FIRST (Red)
|
|
89
|
+
2. Write the minimum code to pass (Green)
|
|
90
|
+
3. Refactor while keeping tests green (Refactor)
|
|
91
|
+
4. Repeat for each requirement
|
|
92
|
+
Never write implementation before tests.
|
|
93
|
+
</compound-tdd>`,
|
|
94
|
+
'code-review': `<compound-code-review>
|
|
95
|
+
CODE REVIEW MODE ACTIVATED.
|
|
96
|
+
Perform thorough code review with severity ratings:
|
|
97
|
+
- 🔴 CRITICAL: Security vulnerabilities, data loss risks, crashes
|
|
98
|
+
- 🟡 MAJOR: Logic errors, performance issues, missing error handling
|
|
99
|
+
- 🔵 MINOR: Style, naming, documentation improvements
|
|
100
|
+
- 💡 SUGGESTION: Optional enhancements
|
|
101
|
+
Provide file:line references for every finding.
|
|
102
|
+
</compound-code-review>`,
|
|
103
|
+
'security-review': `<compound-security-review>
|
|
104
|
+
SECURITY REVIEW MODE ACTIVATED.
|
|
105
|
+
Check for OWASP Top 10 and common vulnerabilities:
|
|
106
|
+
1. Injection (SQL, XSS, Command)
|
|
107
|
+
2. Broken Authentication / Authorization
|
|
108
|
+
3. Sensitive Data Exposure
|
|
109
|
+
4. Security Misconfiguration
|
|
110
|
+
5. Insecure Dependencies
|
|
111
|
+
6. Secrets in code (API keys, tokens, passwords)
|
|
112
|
+
7. Input validation gaps
|
|
113
|
+
8. Unsafe deserialization
|
|
114
|
+
Rate each finding: CRITICAL / HIGH / MEDIUM / LOW
|
|
115
|
+
</compound-security-review>`,
|
|
116
|
+
'git-master': `<compound-git-master>
|
|
117
|
+
GIT MASTER MODE ACTIVATED.
|
|
118
|
+
Apply atomic commit strategy and clean history management:
|
|
119
|
+
1. One commit = one logical change (atomic)
|
|
120
|
+
2. Follow Conventional Commits: feat/fix/refactor/docs/chore(<scope>): <subject>
|
|
121
|
+
3. Use interactive rebase (git rebase -i) to clean up WIP commits before pushing
|
|
122
|
+
4. Never force-push to shared branches (main, develop)
|
|
123
|
+
5. Use git bisect for systematic bug hunt across commits
|
|
124
|
+
Commit message format: <type>(<scope>): <subject> — imperative, 50 chars max
|
|
125
|
+
</compound-git-master>`,
|
|
126
|
+
benchmark: `<compound-benchmark>
|
|
127
|
+
BENCHMARK MODE ACTIVATED.
|
|
128
|
+
Measure performance with statistical rigor:
|
|
129
|
+
1. Collect baseline metrics FIRST (before any changes)
|
|
130
|
+
2. Run minimum 30 iterations (skip first 5 as warmup)
|
|
131
|
+
3. Calculate: avg, p95, p99, min, max
|
|
132
|
+
4. Measure: execution time (performance.now()), memory (process.memoryUsage()), bundle size
|
|
133
|
+
5. Output before/after comparison table with delta percentages
|
|
134
|
+
6. Use same environment for both measurements to ensure validity
|
|
135
|
+
</compound-benchmark>`,
|
|
136
|
+
migrate: `<compound-migrate>
|
|
137
|
+
MIGRATION MODE ACTIVATED.
|
|
138
|
+
Follow the 5-phase safe migration workflow:
|
|
139
|
+
1. ANALYZE: Document current state, identify breaking changes, map affected files
|
|
140
|
+
2. PLAN: Decompose into atomic steps, define rollback triggers (error rate > N%)
|
|
141
|
+
3. BACKUP: Create DB dump + git tag as restore point before any changes
|
|
142
|
+
4. EXECUTE: Apply Expand-Contract pattern for zero-downtime DB changes
|
|
143
|
+
5. VERIFY: Run E2E tests, check data integrity, validate performance regression
|
|
144
|
+
Rollback criteria: error rate spike, latency > 2x baseline, data inconsistency
|
|
145
|
+
</compound-migrate>`,
|
|
146
|
+
'debug-detective': `<compound-debug-detective>
|
|
147
|
+
DEBUG DETECTIVE MODE ACTIVATED.
|
|
148
|
+
Follow the Reproduce → Isolate → Fix → Verify loop:
|
|
149
|
+
1. REPRODUCE: Document exact conditions, input, expected vs actual, reproduction rate
|
|
150
|
+
2. ISOLATE: Classify error type (runtime/type/logic/async), use git bisect for regression
|
|
151
|
+
3. FIX: Address root cause (not symptoms), minimize change scope
|
|
152
|
+
4. VERIFY: Add regression test, confirm fix in staging before production
|
|
153
|
+
Error classification:
|
|
154
|
+
- Runtime: TypeError/ReferenceError → trace stack
|
|
155
|
+
- Logic: wrong output → add intermediate logging
|
|
156
|
+
- Async: race condition → check Promise chain, event ordering
|
|
157
|
+
Never guess — always reproduce first.
|
|
158
|
+
</compound-debug-detective>`,
|
|
159
|
+
refactor: `<compound-refactor>
|
|
160
|
+
REFACTOR MODE ACTIVATED.
|
|
161
|
+
Safe refactoring with test-first approach:
|
|
162
|
+
1. SECURE TESTS: Characterization tests for untested code before touching anything
|
|
163
|
+
2. IDENTIFY SMELLS: Long functions (>50 lines), duplication, deep nesting (>3), magic numbers
|
|
164
|
+
3. APPLY SOLID: Single responsibility, Open-closed, Liskov, Interface segregation, Dependency inversion
|
|
165
|
+
4. REFACTOR CATALOG: Extract Method, Move Method, Replace Conditional with Polymorphism
|
|
166
|
+
5. VERIFY: Run full test suite after each refactoring step
|
|
167
|
+
Rules:
|
|
168
|
+
- Never mix refactoring + feature changes in the same commit
|
|
169
|
+
- One refactoring pattern per commit
|
|
170
|
+
- Keep tests green at all times
|
|
171
|
+
</compound-refactor>`,
|
|
172
|
+
};
|
|
173
|
+
// ── 스킬 파일 로드 ──
|
|
174
|
+
function loadSkillContent(skillName) {
|
|
175
|
+
// 스킬 파일 검색 순서: 프로젝트 > 연결된 팩 > 글로벌 팩 > 글로벌 > 패키지 내장
|
|
176
|
+
const searchPaths = [
|
|
177
|
+
path.join(process.cwd(), '.compound', 'skills', `${skillName}.md`),
|
|
178
|
+
path.join(process.cwd(), 'skills', `${skillName}.md`),
|
|
179
|
+
];
|
|
180
|
+
// v1: 레거시 팩 스킬 검색은 제거. PACKS_DIR 하위 스킬은 직접 탐색.
|
|
181
|
+
try {
|
|
182
|
+
if (fs.existsSync(PACKS_DIR)) {
|
|
183
|
+
for (const entry of fs.readdirSync(PACKS_DIR)) {
|
|
184
|
+
const packSkillPath = path.join(PACKS_DIR, entry, 'skills', `${skillName}.md`);
|
|
185
|
+
searchPaths.push(packSkillPath);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// 팩 디렉토리 접근 실패 시 무시
|
|
191
|
+
}
|
|
192
|
+
// 사용자 개인 스킬 경로
|
|
193
|
+
searchPaths.push(path.join(ME_DIR, 'skills', `${skillName}.md`));
|
|
194
|
+
// 글로벌 스킬 경로
|
|
195
|
+
searchPaths.push(path.join(FORGEN_HOME, 'skills', `${skillName}.md`));
|
|
196
|
+
// forgen 패키지 내장 스킬
|
|
197
|
+
const pkgSkillPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands', `${skillName}.md`);
|
|
198
|
+
searchPaths.push(pkgSkillPath);
|
|
199
|
+
for (const p of searchPaths) {
|
|
200
|
+
if (fs.existsSync(p)) {
|
|
201
|
+
// Security: symlink을 통한 임의 파일 읽기 방지
|
|
202
|
+
try {
|
|
203
|
+
if (fs.lstatSync(p).isSymbolicLink())
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
return fs.readFileSync(p, 'utf-8');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
// ── 키워드 감지 ──
|
|
215
|
+
export function detectKeyword(prompt) {
|
|
216
|
+
// 코드 블록, URL, XML 태그 등을 제거한 순수 텍스트에서만 감지
|
|
217
|
+
const sanitized = sanitizeForDetection(prompt);
|
|
218
|
+
const lower = sanitized.toLowerCase();
|
|
219
|
+
for (const entry of KEYWORD_PATTERNS) {
|
|
220
|
+
if (entry.pattern.test(lower)) {
|
|
221
|
+
// entry.keyword의 RegExp 특수문자를 이스케이프하여 안전하게 사용
|
|
222
|
+
const escapedKeyword = entry.keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
223
|
+
// g 플래그 제거: 첫 번째 매치만 제거하여 코드블록 내 동일 키워드 보존
|
|
224
|
+
const extractedPrompt = prompt.replace(new RegExp(`\\b${escapedKeyword}\\b`, 'i'), '').trim();
|
|
225
|
+
if (entry.type === 'cancel') {
|
|
226
|
+
return { type: 'cancel', keyword: entry.keyword, message: '[Forgen] Mode cancelled.' };
|
|
227
|
+
}
|
|
228
|
+
if (entry.type === 'inject') {
|
|
229
|
+
return {
|
|
230
|
+
type: 'inject',
|
|
231
|
+
keyword: entry.keyword,
|
|
232
|
+
message: INJECT_MESSAGES[entry.keyword] ?? '',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
type: 'skill',
|
|
237
|
+
keyword: entry.keyword,
|
|
238
|
+
skill: entry.skill,
|
|
239
|
+
prompt: extractedPrompt,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
// ── 상태 관리 ──
|
|
246
|
+
function saveState(key, data) {
|
|
247
|
+
atomicWriteJSON(path.join(STATE_DIR, `${key}.json`), data);
|
|
248
|
+
}
|
|
249
|
+
function clearState(key) {
|
|
250
|
+
const p = path.join(STATE_DIR, `${key}.json`);
|
|
251
|
+
if (fs.existsSync(p))
|
|
252
|
+
fs.unlinkSync(p);
|
|
253
|
+
}
|
|
254
|
+
/** skill-cache 파일 모두 정리 */
|
|
255
|
+
function cleanSkillCaches() {
|
|
256
|
+
if (!fs.existsSync(STATE_DIR))
|
|
257
|
+
return;
|
|
258
|
+
try {
|
|
259
|
+
for (const f of fs.readdirSync(STATE_DIR)) {
|
|
260
|
+
if (f.startsWith('skill-cache-')) {
|
|
261
|
+
fs.unlinkSync(path.join(STATE_DIR, f));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
log.debug('skill-cache 파일 삭제 실패', e);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ── 메인 ──
|
|
270
|
+
async function main() {
|
|
271
|
+
const input = await readStdinJSON();
|
|
272
|
+
if (!isHookEnabled('keyword-detector')) {
|
|
273
|
+
console.log(approve());
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!input?.prompt) {
|
|
277
|
+
console.log(approve());
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const match = detectKeyword(input.prompt);
|
|
281
|
+
const sessionId = input.session_id ?? 'unknown';
|
|
282
|
+
// v1: regex 기반 prompt 학습 제거. Evidence 기반으로 전환됨.
|
|
283
|
+
if (!match) {
|
|
284
|
+
console.log(approve());
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Cache conflict map once for the duration of this hook execution
|
|
288
|
+
const skillConflicts = getSkillConflicts(input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd());
|
|
289
|
+
if (match.type === 'cancel') {
|
|
290
|
+
const cancelCwd = input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
291
|
+
if (match.keyword === 'cancel-ralph') {
|
|
292
|
+
// ralph만 취소
|
|
293
|
+
clearState('ralph-state');
|
|
294
|
+
const ralphLoopState = path.join(cancelCwd, '.claude', 'ralph-loop.local.md');
|
|
295
|
+
try {
|
|
296
|
+
fs.unlinkSync(ralphLoopState);
|
|
297
|
+
}
|
|
298
|
+
catch { /* 파일 없으면 무시 */ }
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
// 모든 모드 상태 초기화 (ralplan, deep-interview 포함)
|
|
302
|
+
for (const mode of ALL_MODES) {
|
|
303
|
+
clearState(`${mode}-state`);
|
|
304
|
+
}
|
|
305
|
+
const ralphLoopState = path.join(cancelCwd, '.claude', 'ralph-loop.local.md');
|
|
306
|
+
try {
|
|
307
|
+
fs.unlinkSync(ralphLoopState);
|
|
308
|
+
}
|
|
309
|
+
catch { /* 파일 없으면 무시 */ }
|
|
310
|
+
}
|
|
311
|
+
// skill-cache 파일도 정리 (재주입 가능하도록)
|
|
312
|
+
cleanSkillCaches();
|
|
313
|
+
console.log(approveWithContext(match.message ?? '[Forgen] Mode cancelled.', 'UserPromptSubmit'));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (match.type === 'inject') {
|
|
317
|
+
// Plugin conflict check: inject 타입도 다른 플러그인과 충돌하면 스킵
|
|
318
|
+
// (tdd, code-review 등이 OMC/superpowers와 이중 실행되는 것을 방지)
|
|
319
|
+
const conflictPlugin = skillConflicts.get(match.keyword);
|
|
320
|
+
if (conflictPlugin) {
|
|
321
|
+
log.debug(`Skipping inject "${match.keyword}" — provided by ${conflictPlugin}`);
|
|
322
|
+
console.log(approve());
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (shouldTrackWorkflowActivation(match)) {
|
|
326
|
+
try { /* v1: recordModeUsage 제거 */ }
|
|
327
|
+
catch { /* noop */ }
|
|
328
|
+
}
|
|
329
|
+
console.log(approveWithContext(match.message ?? `[Forgen] ${match.keyword} mode activated.`, 'UserPromptSubmit'));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// 스킬 주입
|
|
333
|
+
if (match.skill) {
|
|
334
|
+
// Plugin conflict check: if a plugin already provides this skill, skip injection
|
|
335
|
+
const conflictPlugin = skillConflicts.get(match.skill);
|
|
336
|
+
if (conflictPlugin) {
|
|
337
|
+
log.debug(`Skipping keyword "${match.keyword}" — skill provided by ${conflictPlugin}`);
|
|
338
|
+
console.log(approve());
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Compound: mode usage 기록
|
|
342
|
+
// v1: recordModeUsage 제거
|
|
343
|
+
const skillContent = loadSkillContent(match.skill);
|
|
344
|
+
const effectiveCwd = input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
345
|
+
// 상태 저장
|
|
346
|
+
saveState(`${match.skill}-state`, {
|
|
347
|
+
active: true,
|
|
348
|
+
startedAt: new Date().toISOString(),
|
|
349
|
+
prompt: match.prompt,
|
|
350
|
+
sessionId: input.session_id,
|
|
351
|
+
});
|
|
352
|
+
// ralph 스킬 활성화 시 ralph-loop 플러그인 상태 파일도 생성
|
|
353
|
+
if (match.skill === 'ralph') {
|
|
354
|
+
const ralphLoopDir = path.join(effectiveCwd, '.claude');
|
|
355
|
+
const ralphLoopState = path.join(ralphLoopDir, 'ralph-loop.local.md');
|
|
356
|
+
fs.mkdirSync(ralphLoopDir, { recursive: true });
|
|
357
|
+
const frontmatter = [
|
|
358
|
+
'---',
|
|
359
|
+
'active: true',
|
|
360
|
+
'iteration: 1',
|
|
361
|
+
`session_id: ${input.session_id ?? ''}`,
|
|
362
|
+
'max_iterations: 0',
|
|
363
|
+
'completion_promise: "TASK COMPLETE"',
|
|
364
|
+
`started_at: "${new Date().toISOString()}"`,
|
|
365
|
+
'---',
|
|
366
|
+
'',
|
|
367
|
+
match.prompt ?? input.prompt,
|
|
368
|
+
].join('\n');
|
|
369
|
+
fs.writeFileSync(ralphLoopState, frontmatter);
|
|
370
|
+
}
|
|
371
|
+
if (skillContent) {
|
|
372
|
+
const truncatedContent = truncateContent(skillContent, INJECTION_CAPS.skillContentMax);
|
|
373
|
+
console.log(approveWithContext(`<compound-skill name="${escapeXmlAttr(match.skill)}">\n${escapeAllXmlTags(truncatedContent)}\n</compound-skill>\n\nUser request: ${match.prompt}`, 'UserPromptSubmit'));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
console.log(approveWithContext(`[Forgen] ${match.keyword} mode activated.\n\nUser request: ${match.prompt}`, 'UserPromptSubmit'));
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
console.log(approve());
|
|
381
|
+
}
|
|
382
|
+
// ESM main guard: 다른 모듈에서 import 시 main() 실행 방지
|
|
383
|
+
// realpathSync로 symlink 해석 (플러그인 캐시가 symlink일 때 경로 불일치 방지)
|
|
384
|
+
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
385
|
+
main().catch((e) => {
|
|
386
|
+
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
387
|
+
console.log(failOpen());
|
|
388
|
+
});
|
|
389
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Notepad Injector Hook
|
|
4
|
+
*
|
|
5
|
+
* Claude Code UserPromptSubmit 훅으로 등록.
|
|
6
|
+
* notepad.md에 저장된 영구 컨텍스트를 사용자 프롬프트 앞에 자동 주입합니다.
|
|
7
|
+
*
|
|
8
|
+
* compaction(컨텍스트 압축) 후에도 notepad의 내용은 매 프롬프트마다
|
|
9
|
+
* <forgen-notepad> 태그로 재주입되어 컨텍스트에서 사라지지 않습니다.
|
|
10
|
+
*
|
|
11
|
+
* stdin: JSON { prompt: string, ... }
|
|
12
|
+
* stdout: JSON { result: "approve", message?: string }
|
|
13
|
+
*
|
|
14
|
+
* notepad 경로 결정 우선순위:
|
|
15
|
+
* 1. COMPOUND_CWD 환경변수
|
|
16
|
+
* 2. process.cwd()
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Notepad Injector Hook
|
|
4
|
+
*
|
|
5
|
+
* Claude Code UserPromptSubmit 훅으로 등록.
|
|
6
|
+
* notepad.md에 저장된 영구 컨텍스트를 사용자 프롬프트 앞에 자동 주입합니다.
|
|
7
|
+
*
|
|
8
|
+
* compaction(컨텍스트 압축) 후에도 notepad의 내용은 매 프롬프트마다
|
|
9
|
+
* <forgen-notepad> 태그로 재주입되어 컨텍스트에서 사라지지 않습니다.
|
|
10
|
+
*
|
|
11
|
+
* stdin: JSON { prompt: string, ... }
|
|
12
|
+
* stdout: JSON { result: "approve", message?: string }
|
|
13
|
+
*
|
|
14
|
+
* notepad 경로 결정 우선순위:
|
|
15
|
+
* 1. COMPOUND_CWD 환경변수
|
|
16
|
+
* 2. process.cwd()
|
|
17
|
+
*/
|
|
18
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
19
|
+
import { readNotepad } from '../core/notepad.js';
|
|
20
|
+
import { isHookEnabled } from './hook-config.js';
|
|
21
|
+
import { truncateContent } from './shared/injection-caps.js';
|
|
22
|
+
import { calculateBudget } from './shared/context-budget.js';
|
|
23
|
+
import { approve, approveWithContext, failOpen } from './shared/hook-response.js';
|
|
24
|
+
// ── 메인 ──
|
|
25
|
+
async function main() {
|
|
26
|
+
const input = await readStdinJSON();
|
|
27
|
+
if (!isHookEnabled('notepad-injector')) {
|
|
28
|
+
console.log(approve());
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!input?.prompt) {
|
|
32
|
+
console.log(approve());
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const effectiveCwd = input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
36
|
+
const notepadContent = readNotepad(effectiveCwd);
|
|
37
|
+
if (!notepadContent.trim()) {
|
|
38
|
+
// notepad가 비어있으면 아무것도 주입하지 않음
|
|
39
|
+
console.log(approve());
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// 태그 이스케이프: notepad 내용 내의 닫는 태그를 안전하게 처리
|
|
43
|
+
const safeContent = truncateContent(notepadContent.trim(), calculateBudget(effectiveCwd).notepadMax)
|
|
44
|
+
.replace(/<\/forgen-notepad>/g, '</forgen-notepad>');
|
|
45
|
+
const injection = `<forgen-notepad>\n${safeContent}\n</forgen-notepad>`;
|
|
46
|
+
console.log(approveWithContext(injection, 'UserPromptSubmit'));
|
|
47
|
+
}
|
|
48
|
+
main().catch((e) => {
|
|
49
|
+
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
50
|
+
console.log(failOpen());
|
|
51
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PermissionRequest Hook
|
|
4
|
+
*
|
|
5
|
+
* 사용자 권한 요청 시 활성 모드에 따른 자동 승인/거부 정책 적용.
|
|
6
|
+
* - autopilot 모드: 안전한 도구는 자동 승인
|
|
7
|
+
* - 위험 패턴: 항상 사용자 확인 요구
|
|
8
|
+
*/
|
|
9
|
+
/** 자동 승인 가능한 안전 도구 목록 */
|
|
10
|
+
export declare const SAFE_TOOLS: Set<string>;
|
|
11
|
+
/** autopilot 모드에서도 수동 확인이 필요한 도구 */
|
|
12
|
+
export declare const ALWAYS_CONFIRM_TOOLS: Set<string>;
|
|
13
|
+
/** 도구 분류: 승인/확인/통과 결정 (순수 함수) */
|
|
14
|
+
export declare function classifyTool(toolName: string, isAutopilot: boolean): 'auto-approve-safe' | 'autopilot-confirm' | 'autopilot-approve' | 'pass-through';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PermissionRequest Hook
|
|
4
|
+
*
|
|
5
|
+
* 사용자 권한 요청 시 활성 모드에 따른 자동 승인/거부 정책 적용.
|
|
6
|
+
* - autopilot 모드: 안전한 도구는 자동 승인
|
|
7
|
+
* - 위험 패턴: 항상 사용자 확인 요구
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { createLogger } from '../core/logger.js';
|
|
12
|
+
const log = createLogger('permission-handler');
|
|
13
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
14
|
+
import { sanitizeId } from './shared/sanitize-id.js';
|
|
15
|
+
import { isHookEnabled } from './hook-config.js';
|
|
16
|
+
import { approve, approveWithWarning, failOpen } from './shared/hook-response.js';
|
|
17
|
+
import { STATE_DIR } from '../core/paths.js';
|
|
18
|
+
/** 자동 승인 가능한 안전 도구 목록 */
|
|
19
|
+
export const SAFE_TOOLS = new Set([
|
|
20
|
+
'Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch',
|
|
21
|
+
'Agent', 'LSP', 'TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList',
|
|
22
|
+
]);
|
|
23
|
+
/** autopilot 모드에서도 수동 확인이 필요한 도구 */
|
|
24
|
+
export const ALWAYS_CONFIRM_TOOLS = new Set([
|
|
25
|
+
'Bash', 'Write', 'Edit',
|
|
26
|
+
]);
|
|
27
|
+
/** 도구 분류: 승인/확인/통과 결정 (순수 함수) */
|
|
28
|
+
export function classifyTool(toolName, isAutopilot) {
|
|
29
|
+
if (SAFE_TOOLS.has(toolName))
|
|
30
|
+
return 'auto-approve-safe';
|
|
31
|
+
if (!isAutopilot)
|
|
32
|
+
return 'pass-through';
|
|
33
|
+
if (ALWAYS_CONFIRM_TOOLS.has(toolName))
|
|
34
|
+
return 'autopilot-confirm';
|
|
35
|
+
return 'autopilot-approve';
|
|
36
|
+
}
|
|
37
|
+
/** autopilot 모드 활성 여부 확인 */
|
|
38
|
+
function isAutopilotActive() {
|
|
39
|
+
const modes = ['autopilot', 'ralph', 'ultrawork'];
|
|
40
|
+
for (const mode of modes) {
|
|
41
|
+
const statePath = path.join(STATE_DIR, `${mode}-state.json`);
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(statePath)) {
|
|
44
|
+
const data = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
45
|
+
if (data.active)
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
log.debug(`mode state file parse failed: ${mode}`, e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/** 권한 요청 로그 기록 */
|
|
56
|
+
function logPermissionRequest(sessionId, toolName, decision) {
|
|
57
|
+
try {
|
|
58
|
+
const logPath = path.join(STATE_DIR, `permissions-${sanitizeId(sessionId)}.jsonl`);
|
|
59
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
60
|
+
const entry = JSON.stringify({
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
tool: toolName,
|
|
63
|
+
decision,
|
|
64
|
+
});
|
|
65
|
+
fs.appendFileSync(logPath, `${entry}\n`);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
log.debug('권한 로그 기록 실패', e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function main() {
|
|
72
|
+
const data = await readStdinJSON();
|
|
73
|
+
if (!data) {
|
|
74
|
+
console.log(approve());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!isHookEnabled('permission-handler')) {
|
|
78
|
+
console.log(approve());
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const toolName = data.tool_name ?? data.toolName ?? '';
|
|
82
|
+
const sessionId = data.session_id ?? 'default';
|
|
83
|
+
// 안전 도구는 항상 승인
|
|
84
|
+
if (SAFE_TOOLS.has(toolName)) {
|
|
85
|
+
logPermissionRequest(sessionId, toolName, 'auto-approve-safe');
|
|
86
|
+
console.log(approve());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// autopilot 모드가 아니면 기본 동작 (Claude Code 기본 권한 흐름)
|
|
90
|
+
if (!isAutopilotActive()) {
|
|
91
|
+
logPermissionRequest(sessionId, toolName, 'pass-through');
|
|
92
|
+
console.log(approve());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// autopilot 모드 (2차 방어선):
|
|
96
|
+
// pre-tool-use 훅이 위험 패턴(rm -rf, git push --force 등)을 이미 block/warn 처리함.
|
|
97
|
+
// 여기 도달하는 도구는 pre-tool-use를 통과한 것이므로, 승인하되 메시지로 추적 가능하게 함.
|
|
98
|
+
if (ALWAYS_CONFIRM_TOOLS.has(toolName)) {
|
|
99
|
+
logPermissionRequest(sessionId, toolName, 'autopilot-confirm');
|
|
100
|
+
// Bash는 pre-tool-use를 통과했더라도 경고 강도를 높임 (임의 셸 실행 위험)
|
|
101
|
+
const warningLevel = toolName === 'Bash'
|
|
102
|
+
? `[Forgen] ⚠ Autopilot: Bash tool auto-approved — passed pre-tool-use validation. Beware of unexpected commands.`
|
|
103
|
+
: `[Forgen] Autopilot: ${toolName} tool execution auto-approved.`;
|
|
104
|
+
console.log(approveWithWarning(`<compound-permission>\n${warningLevel}\n</compound-permission>`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// 기타 도구: autopilot 모드에서 자동 승인
|
|
108
|
+
logPermissionRequest(sessionId, toolName, 'autopilot-approve');
|
|
109
|
+
console.log(approve());
|
|
110
|
+
}
|
|
111
|
+
main().catch((e) => {
|
|
112
|
+
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
113
|
+
console.log(failOpen());
|
|
114
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PostToolUseFailure Hook
|
|
4
|
+
*
|
|
5
|
+
* 도구 실행 실패 시 자동 복구 안내 + 실패 패턴 분석.
|
|
6
|
+
* - 반복 실패 감지 (같은 도구 3회 이상)
|
|
7
|
+
* - 실패 원인별 복구 제안
|
|
8
|
+
* - 컨텍스트 신호에 실패 카운터 기록
|
|
9
|
+
*/
|
|
10
|
+
/** 에러 메시지 기반 복구 제안 */
|
|
11
|
+
export declare function getRecoverySuggestion(error: string, toolName: string): string;
|