@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
|
@@ -23,12 +23,13 @@ import { ALL_MODES, FORGEN_HOME, ME_DIR, PACKS_DIR, STATE_DIR } from '../core/pa
|
|
|
23
23
|
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
24
24
|
import { escapeAllXmlTags } from './prompt-injection-filter.js';
|
|
25
25
|
import { getSkillConflicts } from '../core/plugin-detector.js';
|
|
26
|
-
import { approve, approveWithContext,
|
|
26
|
+
import { approve, approveWithContext, failOpenWithTracking } from './shared/hook-response.js';
|
|
27
|
+
import { recordHookTiming } from './shared/hook-timing.js';
|
|
27
28
|
/** Escape a string for safe use in XML attribute values */
|
|
28
29
|
function escapeXmlAttr(s) {
|
|
29
30
|
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
30
31
|
}
|
|
31
|
-
const WORKFLOW_TRACKED_INJECTS = new Set(
|
|
32
|
+
const WORKFLOW_TRACKED_INJECTS = new Set();
|
|
32
33
|
export function shouldTrackWorkflowActivation(match) {
|
|
33
34
|
if (match.type === 'inject')
|
|
34
35
|
return WORKFLOW_TRACKED_INJECTS.has(match.keyword);
|
|
@@ -51,23 +52,19 @@ export const KEYWORD_PATTERNS = [
|
|
|
51
52
|
{ pattern: /\bralplan\b/i, keyword: 'ralplan', type: 'skill', skill: 'ralplan' },
|
|
52
53
|
{ pattern: /\bdeep[- ]?interview\b/i, keyword: 'deep-interview', type: 'skill', skill: 'deep-interview' },
|
|
53
54
|
{ 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
55
|
// 인젝션 모드
|
|
56
56
|
{ pattern: /\bultrathink\b/i, keyword: 'ultrathink', type: 'inject' },
|
|
57
57
|
{ pattern: /\bdeepsearch\b/i, keyword: 'deepsearch', type: 'inject' },
|
|
58
|
-
{ pattern: /(?:^|\s)tdd(?:\s+(?:모드|mode|방식|으로|해|해줘|시작|적용)|\s*$)/im, keyword: 'tdd', type: 'skill', skill: 'tdd' },
|
|
59
58
|
{ pattern: /(?:code[- ]?review|코드\s*리뷰)\s*(?:해|해줘|시작|해봐|부탁|mode|모드)/i, keyword: 'code-review', type: 'skill', skill: 'code-review' },
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{ pattern:
|
|
63
|
-
{ pattern: /\b(
|
|
64
|
-
{ pattern:
|
|
65
|
-
{ pattern: /\b(
|
|
66
|
-
{ pattern: /\b(refactor|리팩토링|리팩터)\s*(?:mode|모드|해|해줘|시작|실행|진행)/i, keyword: 'refactor', type: 'skill', skill: 'refactor' },
|
|
59
|
+
// forgen 핵심 스킬
|
|
60
|
+
{ pattern: /\b(forge[- ]?loop|포지[- ]?루프)\b|(?:^|\s)(끝까지|don'?t\s*stop)(?:\s|$)/im, keyword: 'forge-loop', type: 'skill', skill: 'forge-loop' },
|
|
61
|
+
{ pattern: /(?:^|\s)ship(?:\s|$)|(?:^|\s)(배포|릴리스)\s*(?:해|해줘|하자|시작|진행)/im, keyword: 'ship', type: 'skill', skill: 'ship' },
|
|
62
|
+
{ pattern: /\bretro\b|(?:^|\s)(회고|돌아보기)(?:\s|$)/im, keyword: 'retro', type: 'skill', skill: 'retro' },
|
|
63
|
+
{ pattern: /(?:^|\s)learn\s+(?:search|prune|stats|export)|(?:^|\s)(학습\s*관리|compound\s*정리|솔루션\s*정리)/im, keyword: 'learn', type: 'skill', skill: 'learn' },
|
|
64
|
+
{ pattern: /\bcalibrate\b|(?:^|\s)(캘리브|프로필\s*보정|프로필\s*조정|프로필\s*확인)(?:\s|$)/im, keyword: 'calibrate', type: 'skill', skill: 'calibrate' },
|
|
67
65
|
];
|
|
68
|
-
// ── 인젝션 메시지
|
|
69
|
-
|
|
70
|
-
const FALLBACK_INJECT_MESSAGES = {
|
|
66
|
+
// ── 인젝션 메시지 ──
|
|
67
|
+
const INJECT_MESSAGES = {
|
|
71
68
|
ultrathink: `<compound-think-mode>
|
|
72
69
|
EXTENDED THINKING MODE ACTIVATED.
|
|
73
70
|
Before responding, engage in deep, thorough reasoning. Consider multiple approaches,
|
|
@@ -83,98 +80,12 @@ Perform comprehensive codebase exploration before answering:
|
|
|
83
80
|
4. Cross-reference findings across files
|
|
84
81
|
5. Present a complete, evidence-based analysis
|
|
85
82
|
</compound-deepsearch>`,
|
|
86
|
-
tdd: `<compound-tdd>
|
|
87
|
-
TDD MODE ACTIVATED.
|
|
88
|
-
Follow strict Test-Driven Development:
|
|
89
|
-
1. Write the failing test FIRST (Red)
|
|
90
|
-
2. Write the minimum code to pass (Green)
|
|
91
|
-
3. Refactor while keeping tests green (Refactor)
|
|
92
|
-
4. Repeat for each requirement
|
|
93
|
-
Never write implementation before tests.
|
|
94
|
-
</compound-tdd>`,
|
|
95
|
-
'code-review': `<compound-code-review>
|
|
96
|
-
CODE REVIEW MODE ACTIVATED.
|
|
97
|
-
Perform thorough code review with severity ratings:
|
|
98
|
-
- 🔴 CRITICAL: Security vulnerabilities, data loss risks, crashes
|
|
99
|
-
- 🟡 MAJOR: Logic errors, performance issues, missing error handling
|
|
100
|
-
- 🔵 MINOR: Style, naming, documentation improvements
|
|
101
|
-
- 💡 SUGGESTION: Optional enhancements
|
|
102
|
-
Provide file:line references for every finding.
|
|
103
|
-
</compound-code-review>`,
|
|
104
|
-
'security-review': `<compound-security-review>
|
|
105
|
-
SECURITY REVIEW MODE ACTIVATED.
|
|
106
|
-
Check for OWASP Top 10 and common vulnerabilities:
|
|
107
|
-
1. Injection (SQL, XSS, Command)
|
|
108
|
-
2. Broken Authentication / Authorization
|
|
109
|
-
3. Sensitive Data Exposure
|
|
110
|
-
4. Security Misconfiguration
|
|
111
|
-
5. Insecure Dependencies
|
|
112
|
-
6. Secrets in code (API keys, tokens, passwords)
|
|
113
|
-
7. Input validation gaps
|
|
114
|
-
8. Unsafe deserialization
|
|
115
|
-
Rate each finding: CRITICAL / HIGH / MEDIUM / LOW
|
|
116
|
-
</compound-security-review>`,
|
|
117
|
-
'git-master': `<compound-git-master>
|
|
118
|
-
GIT MASTER MODE ACTIVATED.
|
|
119
|
-
Apply atomic commit strategy and clean history management:
|
|
120
|
-
1. One commit = one logical change (atomic)
|
|
121
|
-
2. Follow Conventional Commits: feat/fix/refactor/docs/chore(<scope>): <subject>
|
|
122
|
-
3. Use interactive rebase (git rebase -i) to clean up WIP commits before pushing
|
|
123
|
-
4. Never force-push to shared branches (main, develop)
|
|
124
|
-
5. Use git bisect for systematic bug hunt across commits
|
|
125
|
-
Commit message format: <type>(<scope>): <subject> — imperative, 50 chars max
|
|
126
|
-
</compound-git-master>`,
|
|
127
|
-
benchmark: `<compound-benchmark>
|
|
128
|
-
BENCHMARK MODE ACTIVATED.
|
|
129
|
-
Measure performance with statistical rigor:
|
|
130
|
-
1. Collect baseline metrics FIRST (before any changes)
|
|
131
|
-
2. Run minimum 30 iterations (skip first 5 as warmup)
|
|
132
|
-
3. Calculate: avg, p95, p99, min, max
|
|
133
|
-
4. Measure: execution time (performance.now()), memory (process.memoryUsage()), bundle size
|
|
134
|
-
5. Output before/after comparison table with delta percentages
|
|
135
|
-
6. Use same environment for both measurements to ensure validity
|
|
136
|
-
</compound-benchmark>`,
|
|
137
|
-
migrate: `<compound-migrate>
|
|
138
|
-
MIGRATION MODE ACTIVATED.
|
|
139
|
-
Follow the 5-phase safe migration workflow:
|
|
140
|
-
1. ANALYZE: Document current state, identify breaking changes, map affected files
|
|
141
|
-
2. PLAN: Decompose into atomic steps, define rollback triggers (error rate > N%)
|
|
142
|
-
3. BACKUP: Create DB dump + git tag as restore point before any changes
|
|
143
|
-
4. EXECUTE: Apply Expand-Contract pattern for zero-downtime DB changes
|
|
144
|
-
5. VERIFY: Run E2E tests, check data integrity, validate performance regression
|
|
145
|
-
Rollback criteria: error rate spike, latency > 2x baseline, data inconsistency
|
|
146
|
-
</compound-migrate>`,
|
|
147
|
-
'debug-detective': `<compound-debug-detective>
|
|
148
|
-
DEBUG DETECTIVE MODE ACTIVATED.
|
|
149
|
-
Follow the Reproduce → Isolate → Fix → Verify loop:
|
|
150
|
-
1. REPRODUCE: Document exact conditions, input, expected vs actual, reproduction rate
|
|
151
|
-
2. ISOLATE: Classify error type (runtime/type/logic/async), use git bisect for regression
|
|
152
|
-
3. FIX: Address root cause (not symptoms), minimize change scope
|
|
153
|
-
4. VERIFY: Add regression test, confirm fix in staging before production
|
|
154
|
-
Error classification:
|
|
155
|
-
- Runtime: TypeError/ReferenceError → trace stack
|
|
156
|
-
- Logic: wrong output → add intermediate logging
|
|
157
|
-
- Async: race condition → check Promise chain, event ordering
|
|
158
|
-
Never guess — always reproduce first.
|
|
159
|
-
</compound-debug-detective>`,
|
|
160
|
-
refactor: `<compound-refactor>
|
|
161
|
-
REFACTOR MODE ACTIVATED.
|
|
162
|
-
Safe refactoring with test-first approach:
|
|
163
|
-
1. SECURE TESTS: Characterization tests for untested code before touching anything
|
|
164
|
-
2. IDENTIFY SMELLS: Long functions (>50 lines), duplication, deep nesting (>3), magic numbers
|
|
165
|
-
3. APPLY SOLID: Single responsibility, Open-closed, Liskov, Interface segregation, Dependency inversion
|
|
166
|
-
4. REFACTOR CATALOG: Extract Method, Move Method, Replace Conditional with Polymorphism
|
|
167
|
-
5. VERIFY: Run full test suite after each refactoring step
|
|
168
|
-
Rules:
|
|
169
|
-
- Never mix refactoring + feature changes in the same commit
|
|
170
|
-
- One refactoring pattern per commit
|
|
171
|
-
- Keep tests green at all times
|
|
172
|
-
</compound-refactor>`,
|
|
173
83
|
};
|
|
174
84
|
// ── 스킬 파일 로드 ──
|
|
175
85
|
function loadSkillContent(skillName) {
|
|
176
|
-
// 스킬 파일 검색 순서: 프로젝트 >
|
|
86
|
+
// 스킬 파일 검색 순서: 프로젝트 .forgen > 프로젝트 .compound > 팩 > 개인 > 글로벌 > 패키지 내장
|
|
177
87
|
const searchPaths = [
|
|
88
|
+
path.join(process.cwd(), '.forgen', 'skills', `${skillName}.md`),
|
|
178
89
|
path.join(process.cwd(), '.compound', 'skills', `${skillName}.md`),
|
|
179
90
|
path.join(process.cwd(), 'skills', `${skillName}.md`),
|
|
180
91
|
];
|
|
@@ -227,11 +138,10 @@ export function detectKeyword(prompt) {
|
|
|
227
138
|
return { type: 'cancel', keyword: entry.keyword, message: '[Forgen] Mode cancelled.' };
|
|
228
139
|
}
|
|
229
140
|
if (entry.type === 'inject') {
|
|
230
|
-
const fileContent = loadSkillContent(entry.keyword);
|
|
231
141
|
return {
|
|
232
142
|
type: 'inject',
|
|
233
143
|
keyword: entry.keyword,
|
|
234
|
-
message:
|
|
144
|
+
message: INJECT_MESSAGES[entry.keyword] ?? '',
|
|
235
145
|
};
|
|
236
146
|
}
|
|
237
147
|
return {
|
|
@@ -270,122 +180,134 @@ function cleanSkillCaches() {
|
|
|
270
180
|
}
|
|
271
181
|
// ── 메인 ──
|
|
272
182
|
async function main() {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
if (!input?.prompt) {
|
|
279
|
-
console.log(approve());
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
const match = detectKeyword(input.prompt);
|
|
283
|
-
const sessionId = input.session_id ?? 'unknown';
|
|
284
|
-
// v1: regex 기반 prompt 학습 제거. Evidence 기반으로 전환됨.
|
|
285
|
-
if (!match) {
|
|
286
|
-
console.log(approve());
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
// Cache conflict map once for the duration of this hook execution
|
|
290
|
-
const skillConflicts = getSkillConflicts(input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd());
|
|
291
|
-
if (match.type === 'cancel') {
|
|
292
|
-
const cancelCwd = input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
293
|
-
if (match.keyword === 'cancel-ralph') {
|
|
294
|
-
// ralph만 취소
|
|
295
|
-
clearState('ralph-state');
|
|
296
|
-
const ralphLoopState = path.join(cancelCwd, '.claude', 'ralph-loop.local.md');
|
|
297
|
-
try {
|
|
298
|
-
fs.unlinkSync(ralphLoopState);
|
|
299
|
-
}
|
|
300
|
-
catch { /* 파일 없으면 무시 */ }
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
// 모든 모드 상태 초기화 (ralplan, deep-interview 포함)
|
|
304
|
-
for (const mode of ALL_MODES) {
|
|
305
|
-
clearState(`${mode}-state`);
|
|
306
|
-
}
|
|
307
|
-
const ralphLoopState = path.join(cancelCwd, '.claude', 'ralph-loop.local.md');
|
|
308
|
-
try {
|
|
309
|
-
fs.unlinkSync(ralphLoopState);
|
|
310
|
-
}
|
|
311
|
-
catch { /* 파일 없으면 무시 */ }
|
|
312
|
-
}
|
|
313
|
-
// skill-cache 파일도 정리 (재주입 가능하도록)
|
|
314
|
-
cleanSkillCaches();
|
|
315
|
-
console.log(approveWithContext(match.message ?? '[Forgen] Mode cancelled.', 'UserPromptSubmit'));
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (match.type === 'inject') {
|
|
319
|
-
// Plugin conflict check: inject 타입도 다른 플러그인과 충돌하면 스킵
|
|
320
|
-
// (tdd, code-review 등이 OMC/superpowers와 이중 실행되는 것을 방지)
|
|
321
|
-
const conflictPlugin = skillConflicts.get(match.keyword);
|
|
322
|
-
if (conflictPlugin) {
|
|
323
|
-
log.debug(`Skipping inject "${match.keyword}" — provided by ${conflictPlugin}`);
|
|
183
|
+
const _hookStart = Date.now();
|
|
184
|
+
try {
|
|
185
|
+
const input = await readStdinJSON();
|
|
186
|
+
if (!isHookEnabled('keyword-detector')) {
|
|
324
187
|
console.log(approve());
|
|
325
188
|
return;
|
|
326
189
|
}
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
190
|
+
if (!input?.prompt) {
|
|
191
|
+
console.log(approve());
|
|
192
|
+
return;
|
|
330
193
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (match.skill) {
|
|
336
|
-
// Plugin conflict check: if a plugin already provides this skill, skip injection
|
|
337
|
-
const conflictPlugin = skillConflicts.get(match.skill);
|
|
338
|
-
if (conflictPlugin) {
|
|
339
|
-
log.debug(`Skipping keyword "${match.keyword}" — skill provided by ${conflictPlugin}`);
|
|
194
|
+
const match = detectKeyword(input.prompt);
|
|
195
|
+
const sessionId = input.session_id ?? 'unknown';
|
|
196
|
+
// v1: regex 기반 prompt 학습 제거. Evidence 기반으로 전환됨.
|
|
197
|
+
if (!match) {
|
|
340
198
|
console.log(approve());
|
|
341
199
|
return;
|
|
342
200
|
}
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
'
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
'
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
201
|
+
// Cache conflict map once for the duration of this hook execution
|
|
202
|
+
const skillConflicts = getSkillConflicts(input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd());
|
|
203
|
+
if (match.type === 'cancel') {
|
|
204
|
+
const cancelCwd = input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
205
|
+
if (match.keyword === 'cancel-ralph') {
|
|
206
|
+
// ralph만 취소
|
|
207
|
+
clearState('ralph-state');
|
|
208
|
+
const ralphLoopState = path.join(cancelCwd, '.claude', 'ralph-loop.local.md');
|
|
209
|
+
try {
|
|
210
|
+
fs.unlinkSync(ralphLoopState);
|
|
211
|
+
}
|
|
212
|
+
catch { /* 파일 없으면 무시 */ }
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// 모든 모드 상태 초기화 (ralplan, deep-interview, forge-loop 등 포함)
|
|
216
|
+
for (const mode of ALL_MODES) {
|
|
217
|
+
clearState(`${mode}-state`);
|
|
218
|
+
}
|
|
219
|
+
const ralphLoopState = path.join(cancelCwd, '.claude', 'ralph-loop.local.md');
|
|
220
|
+
try {
|
|
221
|
+
fs.unlinkSync(ralphLoopState);
|
|
222
|
+
}
|
|
223
|
+
catch { /* 파일 없으면 무시 */ }
|
|
224
|
+
// forge-loop 상태 파일도 명시적으로 삭제 (Stop 훅 차단 해제)
|
|
225
|
+
const forgeLoopState = path.join(STATE_DIR, 'forge-loop.json');
|
|
226
|
+
try {
|
|
227
|
+
fs.unlinkSync(forgeLoopState);
|
|
228
|
+
}
|
|
229
|
+
catch { /* 파일 없으면 무시 */ }
|
|
230
|
+
}
|
|
231
|
+
// skill-cache 파일도 정리 (재주입 가능하도록)
|
|
232
|
+
cleanSkillCaches();
|
|
233
|
+
console.log(approveWithContext(match.message ?? '[Forgen] Mode cancelled.', 'UserPromptSubmit'));
|
|
234
|
+
return;
|
|
372
235
|
}
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
236
|
+
if (match.type === 'inject') {
|
|
237
|
+
// Plugin conflict check: inject 타입도 다른 플러그인과 충돌하면 스킵
|
|
238
|
+
// (tdd, code-review 등이 OMC/superpowers와 이중 실행되는 것을 방지)
|
|
239
|
+
const conflictPlugin = skillConflicts.get(match.keyword);
|
|
240
|
+
if (conflictPlugin) {
|
|
241
|
+
log.debug(`Skipping inject "${match.keyword}" — provided by ${conflictPlugin}`);
|
|
242
|
+
console.log(approve());
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (shouldTrackWorkflowActivation(match)) {
|
|
246
|
+
try { /* v1: recordModeUsage 제거 */ }
|
|
247
|
+
catch { /* noop */ }
|
|
248
|
+
}
|
|
249
|
+
console.log(approveWithContext(match.message ?? `[Forgen] ${match.keyword} mode activated.`, 'UserPromptSubmit'));
|
|
250
|
+
return;
|
|
376
251
|
}
|
|
377
|
-
|
|
378
|
-
|
|
252
|
+
// 스킬 주입
|
|
253
|
+
if (match.skill) {
|
|
254
|
+
// Plugin conflict check: if a plugin already provides this skill, skip injection
|
|
255
|
+
const conflictPlugin = skillConflicts.get(match.skill);
|
|
256
|
+
if (conflictPlugin) {
|
|
257
|
+
log.debug(`Skipping keyword "${match.keyword}" — skill provided by ${conflictPlugin}`);
|
|
258
|
+
console.log(approve());
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Compound: mode usage 기록
|
|
262
|
+
// v1: recordModeUsage 제거
|
|
263
|
+
const skillContent = loadSkillContent(match.skill);
|
|
264
|
+
const effectiveCwd = input.cwd ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
265
|
+
// 상태 저장
|
|
266
|
+
saveState(`${match.skill}-state`, {
|
|
267
|
+
active: true,
|
|
268
|
+
startedAt: new Date().toISOString(),
|
|
269
|
+
prompt: match.prompt,
|
|
270
|
+
sessionId: input.session_id,
|
|
271
|
+
});
|
|
272
|
+
// ralph 스킬 활성화 시 ralph-loop 플러그인 상태 파일도 생성
|
|
273
|
+
if (match.skill === 'ralph') {
|
|
274
|
+
const ralphLoopDir = path.join(effectiveCwd, '.claude');
|
|
275
|
+
const ralphLoopState = path.join(ralphLoopDir, 'ralph-loop.local.md');
|
|
276
|
+
fs.mkdirSync(ralphLoopDir, { recursive: true });
|
|
277
|
+
const frontmatter = [
|
|
278
|
+
'---',
|
|
279
|
+
'active: true',
|
|
280
|
+
'iteration: 1',
|
|
281
|
+
`session_id: ${input.session_id ?? ''}`,
|
|
282
|
+
'max_iterations: 0',
|
|
283
|
+
'completion_promise: "TASK COMPLETE"',
|
|
284
|
+
`started_at: "${new Date().toISOString()}"`,
|
|
285
|
+
'---',
|
|
286
|
+
'',
|
|
287
|
+
match.prompt ?? input.prompt,
|
|
288
|
+
].join('\n');
|
|
289
|
+
fs.writeFileSync(ralphLoopState, frontmatter);
|
|
290
|
+
}
|
|
291
|
+
if (skillContent) {
|
|
292
|
+
const truncatedContent = truncateContent(skillContent, INJECTION_CAPS.skillContentMax);
|
|
293
|
+
console.log(approveWithContext(`<compound-skill name="${escapeXmlAttr(match.skill)}">\n${escapeAllXmlTags(truncatedContent)}\n</compound-skill>\n\nUser request: ${match.prompt}`, 'UserPromptSubmit'));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
console.log(approveWithContext(`[Forgen] ${match.keyword} mode activated.\n\nUser request: ${match.prompt}`, 'UserPromptSubmit'));
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
379
299
|
}
|
|
380
|
-
|
|
300
|
+
console.log(approve());
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
recordHookTiming('keyword-detector', Date.now() - _hookStart, 'UserPromptSubmit');
|
|
381
304
|
}
|
|
382
|
-
console.log(approve());
|
|
383
305
|
}
|
|
384
306
|
// ESM main guard: 다른 모듈에서 import 시 main() 실행 방지
|
|
385
307
|
// realpathSync로 symlink 해석 (플러그인 캐시가 symlink일 때 경로 불일치 방지)
|
|
386
308
|
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
387
309
|
main().catch((e) => {
|
|
388
310
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
389
|
-
console.log(
|
|
311
|
+
console.log(failOpenWithTracking('keyword-detector'));
|
|
390
312
|
});
|
|
391
313
|
}
|
|
@@ -20,7 +20,7 @@ import { readNotepad } from '../core/notepad.js';
|
|
|
20
20
|
import { isHookEnabled } from './hook-config.js';
|
|
21
21
|
import { truncateContent } from './shared/injection-caps.js';
|
|
22
22
|
import { calculateBudget } from './shared/context-budget.js';
|
|
23
|
-
import { approve, approveWithContext,
|
|
23
|
+
import { approve, approveWithContext, failOpenWithTracking } from './shared/hook-response.js';
|
|
24
24
|
// ── 메인 ──
|
|
25
25
|
async function main() {
|
|
26
26
|
const input = await readStdinJSON();
|
|
@@ -47,5 +47,5 @@ async function main() {
|
|
|
47
47
|
}
|
|
48
48
|
main().catch((e) => {
|
|
49
49
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
50
|
-
console.log(
|
|
50
|
+
console.log(failOpenWithTracking('notepad-injector'));
|
|
51
51
|
});
|
|
@@ -13,7 +13,7 @@ const log = createLogger('permission-handler');
|
|
|
13
13
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
14
14
|
import { sanitizeId } from './shared/sanitize-id.js';
|
|
15
15
|
import { isHookEnabled } from './hook-config.js';
|
|
16
|
-
import { approve, approveWithWarning,
|
|
16
|
+
import { approve, approveWithWarning, failOpenWithTracking } from './shared/hook-response.js';
|
|
17
17
|
import { STATE_DIR } from '../core/paths.js';
|
|
18
18
|
/** 자동 승인 가능한 안전 도구 목록 */
|
|
19
19
|
export const SAFE_TOOLS = new Set([
|
|
@@ -110,5 +110,5 @@ async function main() {
|
|
|
110
110
|
}
|
|
111
111
|
main().catch((e) => {
|
|
112
112
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
113
|
-
console.log(
|
|
113
|
+
console.log(failOpenWithTracking('permission-handler'));
|
|
114
114
|
});
|
|
@@ -15,7 +15,7 @@ import { readStdinJSON } from './shared/read-stdin.js';
|
|
|
15
15
|
import { isHookEnabled } from './hook-config.js';
|
|
16
16
|
import { sanitizeId } from './shared/sanitize-id.js';
|
|
17
17
|
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
18
|
-
import { approve, approveWithWarning,
|
|
18
|
+
import { approve, approveWithWarning, failOpenWithTracking } from './shared/hook-response.js';
|
|
19
19
|
import { STATE_DIR } from '../core/paths.js';
|
|
20
20
|
function getFailureStatePath(sessionId) {
|
|
21
21
|
return path.join(STATE_DIR, `tool-failures-${sanitizeId(sessionId)}.json`);
|
|
@@ -58,11 +58,14 @@ export function getRecoverySuggestion(error, toolName) {
|
|
|
58
58
|
if (/timeout|timed out/.test(lower)) {
|
|
59
59
|
return 'Timeout occurred. Split into smaller units and retry.';
|
|
60
60
|
}
|
|
61
|
+
if (/old_string.*not found|not found in file|not unique|multiple matches/i.test(lower)) {
|
|
62
|
+
return 'The old_string matched multiple locations. Include more surrounding context to make it unique, or use replace_all: true if all occurrences should change.';
|
|
63
|
+
}
|
|
61
64
|
if (/enoent|no such file|not found/.test(lower)) {
|
|
62
|
-
return 'File/path does not exist.
|
|
65
|
+
return 'File/path does not exist. Use Glob to search for similar file names, then retry with the correct path.';
|
|
63
66
|
}
|
|
64
67
|
if (/eacces|permission denied/.test(lower)) {
|
|
65
|
-
return 'Permission denied. Check file permissions.';
|
|
68
|
+
return 'Permission denied. Check file permissions with `ls -la` and fix with `chmod` if needed.';
|
|
66
69
|
}
|
|
67
70
|
if (/syntax error|syntaxerror/.test(lower)) {
|
|
68
71
|
return 'Syntax error. Review the code again.';
|
|
@@ -70,8 +73,11 @@ export function getRecoverySuggestion(error, toolName) {
|
|
|
70
73
|
if (/enospc|no space/.test(lower)) {
|
|
71
74
|
return 'Disk space is insufficient.';
|
|
72
75
|
}
|
|
73
|
-
if (/
|
|
74
|
-
return '
|
|
76
|
+
if (/stale|file has been modified|changed since/i.test(lower)) {
|
|
77
|
+
return 'File content has changed since last read. Use Read to get the current content, then retry the edit with updated old_string.';
|
|
78
|
+
}
|
|
79
|
+
if (/binary|encoding|invalid utf|ucs-2/i.test(lower)) {
|
|
80
|
+
return "File may be binary or use non-UTF-8 encoding. Verify encoding with 'file <path>' command.";
|
|
75
81
|
}
|
|
76
82
|
return `${toolName} tool failed. Try a different approach.`;
|
|
77
83
|
}
|
|
@@ -114,5 +120,5 @@ main().catch((e) => {
|
|
|
114
120
|
hookName: 'post-tool-failure', eventType: 'PostToolUseFailure', cause: e,
|
|
115
121
|
});
|
|
116
122
|
process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
|
|
117
|
-
console.log(
|
|
123
|
+
console.log(failOpenWithTracking('post-tool-failure'));
|
|
118
124
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Compound negative/success 신호 감지, 컨텍스트 실패 카운터,
|
|
5
5
|
* 솔루션 negative evidence 업데이트 등 post-tool 분석 핸들러.
|
|
6
6
|
*/
|
|
7
|
-
/** 세션의 실패 카운터 증가 (컨텍스트 신호
|
|
7
|
+
/** 세션의 실패 카운터 증가 (컨텍스트 신호 수집, lock 보호) */
|
|
8
8
|
export declare function incrementFailureCounter(sessionId: string): void;
|
|
9
9
|
/** Compound v3: detect negative signals after tool execution */
|
|
10
10
|
export declare function checkCompoundNegative(toolName: string, toolResponse: string, sessionId: string): void;
|
|
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { createLogger } from '../core/logger.js';
|
|
10
10
|
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
11
|
+
import { withFileLockSync } from './shared/file-lock.js';
|
|
11
12
|
import { sanitizeId } from './shared/sanitize-id.js';
|
|
12
13
|
import { incrementEvidence } from '../engine/solution-writer.js';
|
|
13
14
|
import { classifyMatch, shouldAttribute } from '../engine/term-matcher.js';
|
|
@@ -15,19 +16,21 @@ import { detectErrorPattern } from './post-tool-use.js';
|
|
|
15
16
|
import { STATE_DIR } from '../core/paths.js';
|
|
16
17
|
const log = createLogger('post-tool-handlers');
|
|
17
18
|
const CONTEXT_SIGNALS_PATH = path.join(STATE_DIR, 'context-signals.json');
|
|
18
|
-
/** 세션의 실패 카운터 증가 (컨텍스트 신호
|
|
19
|
+
/** 세션의 실패 카운터 증가 (컨텍스트 신호 수집, lock 보호) */
|
|
19
20
|
export function incrementFailureCounter(sessionId) {
|
|
20
21
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
signals
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
withFileLockSync(CONTEXT_SIGNALS_PATH, () => {
|
|
23
|
+
let signals = {};
|
|
24
|
+
if (fs.existsSync(CONTEXT_SIGNALS_PATH)) {
|
|
25
|
+
signals = JSON.parse(fs.readFileSync(CONTEXT_SIGNALS_PATH, 'utf-8'));
|
|
26
|
+
if (signals.sessionId !== sessionId)
|
|
27
|
+
signals = {};
|
|
28
|
+
}
|
|
29
|
+
signals.sessionId = sessionId;
|
|
30
|
+
signals.previousFailures = (signals.previousFailures ?? 0) + 1;
|
|
31
|
+
signals.updatedAt = new Date().toISOString();
|
|
32
|
+
atomicWriteJSON(CONTEXT_SIGNALS_PATH, signals);
|
|
33
|
+
});
|
|
31
34
|
}
|
|
32
35
|
catch (e) {
|
|
33
36
|
log.debug('context signals write failed — failure count may be lost', e);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 도구 실행 후 결과 검증 + 파일 변경 추적.
|
|
6
6
|
* Compound/workflow 핸들러는 ./post-tool-handlers.ts에 분리.
|
|
7
7
|
*/
|
|
8
|
+
import { type DriftState } from '../core/drift-score.js';
|
|
8
9
|
interface ModifiedFilesState {
|
|
9
10
|
sessionId: string;
|
|
10
11
|
files: Record<string, {
|
|
@@ -13,6 +14,10 @@ interface ModifiedFilesState {
|
|
|
13
14
|
tool: string;
|
|
14
15
|
}>;
|
|
15
16
|
toolCallCount: number;
|
|
17
|
+
/** Track recent write content hashes for revert detection */
|
|
18
|
+
recentWrites?: Record<string, string[]>;
|
|
19
|
+
/** Drift detection state */
|
|
20
|
+
drift?: DriftState;
|
|
16
21
|
}
|
|
17
22
|
export declare const ERROR_PATTERNS: Array<{
|
|
18
23
|
pattern: RegExp;
|
|
@@ -22,6 +27,12 @@ export declare function detectErrorPattern(text: string): {
|
|
|
22
27
|
pattern: RegExp;
|
|
23
28
|
description: string;
|
|
24
29
|
} | null;
|
|
30
|
+
export interface AgentValidationResult {
|
|
31
|
+
signal: string;
|
|
32
|
+
severity: 'info' | 'warning' | 'error';
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function validateAgentOutput(toolResponse: string): AgentValidationResult | null;
|
|
25
36
|
export declare function trackModifiedFile(state: ModifiedFilesState, filePath: string, toolName: string): {
|
|
26
37
|
state: ModifiedFilesState;
|
|
27
38
|
count: number;
|