@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,460 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — Auto Compound Runner
|
|
4
|
+
*
|
|
5
|
+
* Detached process로 실행. 이전 세션의 transcript를 분석하여:
|
|
6
|
+
* 1. 재사용 가능한 솔루션 추출 (compound --solution)
|
|
7
|
+
* 2. 사용자 패턴을 USER.md에 축적
|
|
8
|
+
*
|
|
9
|
+
* 호출: session-recovery hook 또는 spawn.ts에서 detached spawn
|
|
10
|
+
* 인자: [cwd] [transcriptPath] [sessionId]
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
|
|
17
|
+
/** Auto-compound에 사용할 모델 — background 추출이므로 haiku로 충분 */
|
|
18
|
+
const COMPOUND_MODEL = 'haiku';
|
|
19
|
+
/** execFileSync wrapper: transient 에러(ETIMEDOUT 등) 시 1회 재시도 */
|
|
20
|
+
function execClaudeRetry(args, opts) {
|
|
21
|
+
const TRANSIENT = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE/;
|
|
22
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync('claude', args, opts);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
28
|
+
if (attempt === 0 && TRANSIENT.test(msg)) {
|
|
29
|
+
process.stderr.write(`[forgen-auto-compound] transient error, retrying in 3s...\n`);
|
|
30
|
+
// Blocking synchronous sleep: Atomics.wait on a zero-initialized
|
|
31
|
+
// SharedArrayBuffer is the Node.js idiom for blocking the event
|
|
32
|
+
// loop without spawning child processes. This file runs as a
|
|
33
|
+
// detached subprocess (`auto-compound-runner`) so blocking is
|
|
34
|
+
// both safe and intentional. The 3000ms matches the backoff
|
|
35
|
+
// before retry. Alternative setTimeout would require making this
|
|
36
|
+
// function async, which would ripple through the entire runner.
|
|
37
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error('unreachable');
|
|
44
|
+
}
|
|
45
|
+
const [, , cwd, transcriptPath, sessionId] = process.argv;
|
|
46
|
+
if (!cwd || !transcriptPath || !sessionId) {
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const FORGEN_HOME = path.join(os.homedir(), '.forgen');
|
|
50
|
+
const SOLUTIONS_DIR = path.join(FORGEN_HOME, 'me', 'solutions');
|
|
51
|
+
const BEHAVIOR_DIR = path.join(FORGEN_HOME, 'me', 'behavior');
|
|
52
|
+
/** Lightweight quality gate for auto-extracted solution files */
|
|
53
|
+
/** Toxicity patterns — code-context only to avoid false positives on prose */
|
|
54
|
+
const SOLUTION_TOXICITY_PATTERNS = [/@ts-ignore/i, /:\s*any\b/, /\/\/\s*TODO\b/];
|
|
55
|
+
/** Parse tags from solution frontmatter */
|
|
56
|
+
function parseTags(content) {
|
|
57
|
+
const match = content.match(/tags:\s*\[([^\]]*)\]/);
|
|
58
|
+
if (!match)
|
|
59
|
+
return [];
|
|
60
|
+
return match[1].split(',').map(t => t.trim().replace(/"/g, '').replace(/'/g, '')).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
/** Gate 3 (dedup): check tag overlap with existing solutions */
|
|
63
|
+
function isDuplicate(newContent, existingFiles) {
|
|
64
|
+
const newTags = parseTags(newContent);
|
|
65
|
+
if (newTags.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
for (const [, existingContent] of existingFiles) {
|
|
68
|
+
const existingTags = parseTags(existingContent);
|
|
69
|
+
if (existingTags.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
const overlap = newTags.filter(t => existingTags.includes(t));
|
|
72
|
+
const overlapRatio = overlap.length / Math.max(newTags.length, existingTags.length, 1);
|
|
73
|
+
if (overlapRatio >= 0.7)
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function validateSolutionFiles(dirBefore) {
|
|
79
|
+
let removed = 0;
|
|
80
|
+
if (!fs.existsSync(SOLUTIONS_DIR))
|
|
81
|
+
return removed;
|
|
82
|
+
try {
|
|
83
|
+
// Load existing solutions for dedup (gate 3)
|
|
84
|
+
const existingSolutions = new Map();
|
|
85
|
+
for (const file of dirBefore) {
|
|
86
|
+
try {
|
|
87
|
+
existingSolutions.set(file, fs.readFileSync(path.join(SOLUTIONS_DIR, file), 'utf-8'));
|
|
88
|
+
}
|
|
89
|
+
catch { /* skip unreadable */ }
|
|
90
|
+
}
|
|
91
|
+
const currentFiles = fs.readdirSync(SOLUTIONS_DIR).filter(f => f.endsWith('.md'));
|
|
92
|
+
for (const file of currentFiles) {
|
|
93
|
+
if (dirBefore.has(file))
|
|
94
|
+
continue; // existed before extraction — skip
|
|
95
|
+
const filePath = path.join(SOLUTIONS_DIR, file);
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
98
|
+
// Gate 1: file must be > 100 chars (not too short)
|
|
99
|
+
if (content.length <= 100) {
|
|
100
|
+
fs.unlinkSync(filePath);
|
|
101
|
+
removed++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Gate 2: first 500 chars must not contain toxicity patterns
|
|
105
|
+
const head = content.slice(0, 500);
|
|
106
|
+
if (SOLUTION_TOXICITY_PATTERNS.some(p => p.test(head))) {
|
|
107
|
+
fs.unlinkSync(filePath);
|
|
108
|
+
removed++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Gate 3: dedup — reject if 70%+ tag overlap with existing solutions
|
|
112
|
+
if (isDuplicate(content, existingSolutions)) {
|
|
113
|
+
fs.unlinkSync(filePath);
|
|
114
|
+
removed++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Accepted — add to existing pool so subsequent new files dedup against it too
|
|
118
|
+
existingSolutions.set(file, content);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
process.stderr.write(`[forgen-auto-compound] file validation failed: ${e.message}\n`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
process.stderr.write(`[forgen-auto-compound] solution dir scan failed: ${e.message}\n`);
|
|
127
|
+
}
|
|
128
|
+
return removed;
|
|
129
|
+
}
|
|
130
|
+
function extractText(c) {
|
|
131
|
+
if (typeof c === 'string')
|
|
132
|
+
return c;
|
|
133
|
+
if (Array.isArray(c))
|
|
134
|
+
return c.filter((x) => x?.type === 'text').map((x) => x.text ?? '').join('\n');
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
function extractSummary(filePath, maxChars = 8000) {
|
|
138
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
139
|
+
const lines = content.split('\n').filter(Boolean);
|
|
140
|
+
const messages = [];
|
|
141
|
+
let totalChars = 0;
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
try {
|
|
144
|
+
const entry = JSON.parse(line);
|
|
145
|
+
if (entry.type === 'user' || entry.type === 'queue-operation') {
|
|
146
|
+
const text = extractText(entry.content);
|
|
147
|
+
if (text) {
|
|
148
|
+
messages.push(`[User] ${text.slice(0, 500)}`);
|
|
149
|
+
totalChars += text.length;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (entry.type === 'assistant') {
|
|
153
|
+
const text = extractText(entry.content);
|
|
154
|
+
if (text) {
|
|
155
|
+
messages.push(`[Assistant] ${text.slice(0, 500)}`);
|
|
156
|
+
totalChars += text.length;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch { /* skip */ }
|
|
161
|
+
if (totalChars > maxChars)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
return messages.join('\n\n');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 기존 behavior 파일에 유사 패턴이 있으면 observedCount를 +1 증가.
|
|
168
|
+
* 유사도는 같은 kind + 내용 키워드 50%+ 겹침으로 판단.
|
|
169
|
+
* 누적했으면 true, 새 파일 필요하면 false 반환.
|
|
170
|
+
*/
|
|
171
|
+
function mergeOrCreateBehavior(dir, newContent, kind, today) {
|
|
172
|
+
if (!fs.existsSync(dir))
|
|
173
|
+
return false;
|
|
174
|
+
const newWords = new Set(newContent.toLowerCase().split(/\s+/).filter(w => w.length >= 3));
|
|
175
|
+
if (newWords.size === 0)
|
|
176
|
+
return false;
|
|
177
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const filePath = path.join(dir, file);
|
|
180
|
+
try {
|
|
181
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
182
|
+
// kind 매칭
|
|
183
|
+
const kindMatch = raw.match(/^kind:\s*["']?(\w+)["']?/m);
|
|
184
|
+
if (!kindMatch || kindMatch[1] !== kind)
|
|
185
|
+
continue;
|
|
186
|
+
// 내용 유사도 체크
|
|
187
|
+
const existingWords = new Set(raw.toLowerCase().split(/\s+/).filter(w => w.length >= 3));
|
|
188
|
+
let overlap = 0;
|
|
189
|
+
for (const w of newWords) {
|
|
190
|
+
if (existingWords.has(w))
|
|
191
|
+
overlap++;
|
|
192
|
+
}
|
|
193
|
+
const similarity = overlap / newWords.size;
|
|
194
|
+
if (similarity < 0.5)
|
|
195
|
+
continue;
|
|
196
|
+
// 유사 패턴 발견 — observedCount 증가
|
|
197
|
+
const countMatch = raw.match(/^observedCount:\s*(\d+)/m);
|
|
198
|
+
const currentCount = countMatch ? parseInt(countMatch[1], 10) : 1;
|
|
199
|
+
const updated = raw
|
|
200
|
+
.replace(/^observedCount:\s*\d+/m, `observedCount: ${currentCount + 1}`)
|
|
201
|
+
.replace(/^updated:\s*"[^"]*"/m, `updated: "${today}"`)
|
|
202
|
+
.replace(/^confidence:\s*[\d.]+/m, `confidence: ${Math.min(0.95, 0.6 + (currentCount * 0.1)).toFixed(2)}`);
|
|
203
|
+
fs.writeFileSync(filePath, updated);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const summary = extractSummary(transcriptPath);
|
|
214
|
+
if (summary.length < 200)
|
|
215
|
+
process.exit(0);
|
|
216
|
+
// 보안: 프롬프트 인젝션이 포함된 transcript는 분석하지 않음
|
|
217
|
+
if (containsPromptInjection(summary)) {
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
// 기존 솔루션 목록 (중복 방지)
|
|
221
|
+
let existingList = '';
|
|
222
|
+
const solDir = path.join(FORGEN_HOME, 'me', 'solutions');
|
|
223
|
+
if (fs.existsSync(solDir)) {
|
|
224
|
+
const names = fs.readdirSync(solDir).filter(f => f.endsWith('.md')).map(f => f.replace('.md', '')).slice(-30);
|
|
225
|
+
if (names.length > 0)
|
|
226
|
+
existingList = `\n\n이미 축적된 솔루션 (중복 추출 금지):\n${names.join(', ')}`;
|
|
227
|
+
}
|
|
228
|
+
// 기존 behavior 파일 목록 (중복 패턴 방지)
|
|
229
|
+
let existingBehaviorPatterns = '';
|
|
230
|
+
if (fs.existsSync(BEHAVIOR_DIR)) {
|
|
231
|
+
const behaviorFiles = fs.readdirSync(BEHAVIOR_DIR).filter(f => f.endsWith('.md')).slice(-10);
|
|
232
|
+
if (behaviorFiles.length > 0) {
|
|
233
|
+
const snippets = behaviorFiles.map(f => {
|
|
234
|
+
try {
|
|
235
|
+
return fs.readFileSync(path.join(BEHAVIOR_DIR, f), 'utf-8').slice(0, 200);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return '';
|
|
239
|
+
}
|
|
240
|
+
}).filter(Boolean);
|
|
241
|
+
existingBehaviorPatterns = `\n\n기존 behavior 패턴 (중복 추가 금지):\n${snippets.join('\n---\n')}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// 1단계: 솔루션 추출
|
|
245
|
+
// 보안: transcript 요약에 filterSolutionContent 적용하여 프롬프트 인젝션 방어
|
|
246
|
+
const scanResult = filterSolutionContent(summary);
|
|
247
|
+
if (scanResult.verdict === 'block') {
|
|
248
|
+
process.stderr.write('[forgen-auto-compound] transcript blocked by injection filter\n');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
if (scanResult.verdict === 'warn') {
|
|
252
|
+
process.stderr.write(`[forgen-auto-compound] injection warning: ${scanResult.findings.map(f => f.patternId).join(', ')}\n`);
|
|
253
|
+
}
|
|
254
|
+
const sanitizedSummary = scanResult.sanitized;
|
|
255
|
+
// Snapshot solution files before extraction (for post-extraction validation)
|
|
256
|
+
const solutionsBefore = new Set();
|
|
257
|
+
try {
|
|
258
|
+
if (fs.existsSync(SOLUTIONS_DIR)) {
|
|
259
|
+
for (const f of fs.readdirSync(SOLUTIONS_DIR)) {
|
|
260
|
+
if (f.endsWith('.md'))
|
|
261
|
+
solutionsBefore.add(f);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch { /* ignore */ }
|
|
266
|
+
const solutionPrompt = `다음은 이전 Claude Code 세션의 대화 요약입니다.
|
|
267
|
+
미래 세션에서 재사용할 수 있는 패턴, 해결책, 의사결정을 추출해주세요.
|
|
268
|
+
|
|
269
|
+
각 항목은 반드시 다음을 포함해야 합니다:
|
|
270
|
+
- **제목**: 구체적이고 검색 가능한 이름 (예: "vitest-mock-esm-pattern", "react-state-lifting-decision")
|
|
271
|
+
- **설명**: (1) 무엇을 했는지 (2) 왜 그렇게 했는지 (3) 어떻게 적용하는지
|
|
272
|
+
|
|
273
|
+
형식: forgen compound --solution "제목" "설명 (why + how to apply)"
|
|
274
|
+
추출할 것이 없으면 "추출할 패턴 없음"이라고만 답하세요.
|
|
275
|
+
최대 3개. 피상적인 관찰(예: "TypeScript를 사용함")은 제외. 기존 솔루션과 중복 금지.${existingList}
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
${sanitizedSummary.slice(0, 6000)}
|
|
279
|
+
---`;
|
|
280
|
+
try {
|
|
281
|
+
execClaudeRetry(['-p', solutionPrompt, '--allowedTools', 'Bash', '--model', COMPOUND_MODEL], {
|
|
282
|
+
cwd, timeout: 90_000, stdio: ['pipe', 'ignore', 'pipe'],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
process.stderr.write(`[forgen-auto-compound] solution extraction: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
287
|
+
}
|
|
288
|
+
// Post-extraction quality validation: remove files that fail lightweight gates
|
|
289
|
+
const removedCount = validateSolutionFiles(solutionsBefore);
|
|
290
|
+
if (removedCount > 0) {
|
|
291
|
+
process.stderr.write(`[forgen-auto-compound] quality gate removed ${removedCount} low-quality solution(s)\n`);
|
|
292
|
+
}
|
|
293
|
+
// 2단계: 사용자 패턴 추출 → USER.md 업데이트
|
|
294
|
+
const userPrompt = `다음 대화에서 사용자의 작업 습관, 커뮤니케이션 스타일, 기술 선호도를 분석해주세요.
|
|
295
|
+
|
|
296
|
+
관찰된 패턴을 다음 형식으로 1~3개만 출력해주세요 (없으면 "관찰된 패턴 없음"):
|
|
297
|
+
- [카테고리] 패턴 설명 (관찰 근거)
|
|
298
|
+
|
|
299
|
+
카테고리: 커뮤니케이션/작업습관/기술선호/의사결정/워크플로우
|
|
300
|
+
|
|
301
|
+
특히 "워크플로우" 카테고리에 주목하세요:
|
|
302
|
+
- 사용자가 반복하는 작업 순서 패턴 (예: "항상 테스트 먼저 작성 → 구현 → 리팩토링 순서로 진행")
|
|
303
|
+
- 특정 상황에서의 판단 규칙 (예: "PR 리뷰 시 보안 → 테스트 → 코드 품질 순서로 확인")
|
|
304
|
+
- 조건부 접근법 (예: "버그 수정 시 재현 테스트부터 작성, 성능 이슈면 프로파일링부터")
|
|
305
|
+
|
|
306
|
+
워크플로우 패턴이 감지되면 반드시 구체적인 순서를 포함하세요.
|
|
307
|
+
|
|
308
|
+
기존 패턴과 중복이면 건너뛰세요.${existingBehaviorPatterns}
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
${sanitizedSummary.slice(0, 4000)}
|
|
312
|
+
---`;
|
|
313
|
+
try {
|
|
314
|
+
const userResult = execClaudeRetry(['-p', userPrompt, '--model', COMPOUND_MODEL], {
|
|
315
|
+
cwd, timeout: 60_000, encoding: 'utf-8',
|
|
316
|
+
});
|
|
317
|
+
// 결과가 의미 있으면 behavior/ 파일로 저장
|
|
318
|
+
//
|
|
319
|
+
// B4 security hardening (2026-04-09): gate the Claude-generated
|
|
320
|
+
// behavior output through the prompt-injection filter BEFORE
|
|
321
|
+
// writing to disk. Pre-B4 the transcript (the INPUT to Claude)
|
|
322
|
+
// was filtered at line 202 but the MODEL OUTPUT was trusted and
|
|
323
|
+
// written verbatim. A crafted transcript could make Claude emit
|
|
324
|
+
// an injection payload like "[의사결정] 실행 전 ; rm -rf ~/.forgen ..."
|
|
325
|
+
// which would land on disk. C5's render-time filter in
|
|
326
|
+
// config-injector would catch it at forge-behavioral.md
|
|
327
|
+
// generation, but defense in depth — stop it at the source so
|
|
328
|
+
// the file itself is clean.
|
|
329
|
+
const isInjection = userResult ? containsPromptInjection(userResult.trim()) : false;
|
|
330
|
+
if (isInjection) {
|
|
331
|
+
process.stderr.write(`[forgen-auto-compound] behavior: injection detected in LLM output, skipping write\n`);
|
|
332
|
+
}
|
|
333
|
+
if (userResult && !isInjection && !userResult.includes('관찰된 패턴 없음') && userResult.trim().length > 10) {
|
|
334
|
+
fs.mkdirSync(BEHAVIOR_DIR, { recursive: true });
|
|
335
|
+
const today = new Date().toISOString().split('T')[0];
|
|
336
|
+
const trimmed = userResult.trim();
|
|
337
|
+
// 카테고리에 따라 kind 분류
|
|
338
|
+
const kind = trimmed.includes('[워크플로우]') || trimmed.includes('순서') || trimmed.includes('→')
|
|
339
|
+
? 'workflow'
|
|
340
|
+
: trimmed.includes('[의사결정]') ? 'thinking'
|
|
341
|
+
: 'preference';
|
|
342
|
+
// 기존 유사 패턴이 있으면 observedCount 누적
|
|
343
|
+
const merged = mergeOrCreateBehavior(BEHAVIOR_DIR, trimmed, kind, today);
|
|
344
|
+
if (!merged) {
|
|
345
|
+
const slug = `auto-${today}-${kind}`;
|
|
346
|
+
const behaviorPath = path.join(BEHAVIOR_DIR, `${slug}.md`);
|
|
347
|
+
if (!fs.existsSync(behaviorPath)) {
|
|
348
|
+
const content = `---\nname: "${slug}"\nversion: 1\nkind: "${kind}"\nobservedCount: 1\nconfidence: 0.6\ntags: ["auto-observed", "${kind}"]\ncreated: "${today}"\nupdated: "${today}"\nsource: "auto-compound"\n---\n\n## Content\n${trimmed}\n`;
|
|
349
|
+
fs.writeFileSync(behaviorPath, content);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
process.stderr.write(`[forgen-auto-compound] behavior update: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
356
|
+
}
|
|
357
|
+
// 3단계: 세션 학습 요약 (SessionLearningSummary) 생성
|
|
358
|
+
try {
|
|
359
|
+
const FORGEN_HOME = path.join(os.homedir(), '.forgen');
|
|
360
|
+
const V1_ME_DIR = path.join(FORGEN_HOME, 'me');
|
|
361
|
+
const V1_PROFILE = path.join(V1_ME_DIR, 'forge-profile.json');
|
|
362
|
+
const V1_EVIDENCE_DIR = path.join(V1_ME_DIR, 'behavior');
|
|
363
|
+
if (fs.existsSync(V1_PROFILE)) {
|
|
364
|
+
const learningSummaryPrompt = `다음 Claude Code 세션 대화를 분석하여 사용자의 개인화 학습 요약을 JSON으로 출력해주세요.
|
|
365
|
+
|
|
366
|
+
출력 형식 (JSON만, 설명 없이):
|
|
367
|
+
{
|
|
368
|
+
"corrections": ["사용자가 명시적으로 교정한 내용 목록"],
|
|
369
|
+
"observations": ["사용자의 반복 행동 패턴 목록"],
|
|
370
|
+
"pack_direction": null 또는 "opposite_quality" 또는 "opposite_autonomy",
|
|
371
|
+
"profile_delta": {
|
|
372
|
+
"quality_safety": { "verification_depth": 0.0, "stop_threshold": 0.0, "change_conservatism": 0.0 },
|
|
373
|
+
"autonomy": { "confirmation_independence": 0.0, "assumption_tolerance": 0.0, "scope_expansion_tolerance": 0.0, "approval_threshold": 0.0 }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
규칙:
|
|
378
|
+
- corrections: "하지마", "그렇게 말고", "앞으로는" 같은 명시 교정만. 없으면 빈 배열.
|
|
379
|
+
- observations: 3회 이상 반복된 행동만. 없으면 빈 배열.
|
|
380
|
+
- pack_direction: 사용자가 현재 pack과 반대 방향으로 일관되게 행동했으면 opposite_quality 또는 opposite_autonomy. 아니면 null.
|
|
381
|
+
- profile_delta: facet 조정 제안. -0.1~+0.1 범위. 변화 없으면 0.0.
|
|
382
|
+
- 학습할 것이 없으면 모든 값을 빈 배열/null/0.0으로.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
${sanitizedSummary.slice(0, 4000)}
|
|
386
|
+
---`;
|
|
387
|
+
const learningResult = execClaudeRetry(['-p', learningSummaryPrompt, '--model', COMPOUND_MODEL], {
|
|
388
|
+
cwd, timeout: 60_000, encoding: 'utf-8',
|
|
389
|
+
});
|
|
390
|
+
// JSON 파싱 시도
|
|
391
|
+
const jsonMatch = learningResult.match(/\{[\s\S]*\}/);
|
|
392
|
+
if (jsonMatch) {
|
|
393
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
394
|
+
// session_summary evidence 저장 (mismatch detector용)
|
|
395
|
+
if (parsed.pack_direction || parsed.corrections?.length > 0 || parsed.observations?.length > 0) {
|
|
396
|
+
const evidenceId = `sess-summary-${sessionId.slice(0, 8)}`;
|
|
397
|
+
const evidence = {
|
|
398
|
+
evidence_id: evidenceId,
|
|
399
|
+
type: 'session_summary',
|
|
400
|
+
session_id: sessionId,
|
|
401
|
+
timestamp: new Date().toISOString(),
|
|
402
|
+
source_component: 'auto-compound-runner',
|
|
403
|
+
summary: `corrections: ${parsed.corrections?.length ?? 0}, observations: ${parsed.observations?.length ?? 0}`,
|
|
404
|
+
axis_refs: parsed.pack_direction ? [parsed.pack_direction.includes('quality') ? 'quality_safety' : 'autonomy'] : [],
|
|
405
|
+
candidate_rule_refs: [],
|
|
406
|
+
confidence: 0.7,
|
|
407
|
+
raw_payload: {
|
|
408
|
+
pack_direction: parsed.pack_direction,
|
|
409
|
+
corrections: parsed.corrections,
|
|
410
|
+
observations: parsed.observations,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
fs.mkdirSync(V1_EVIDENCE_DIR, { recursive: true });
|
|
414
|
+
fs.writeFileSync(path.join(V1_EVIDENCE_DIR, `${evidenceId}.json`), JSON.stringify(evidence, null, 2));
|
|
415
|
+
}
|
|
416
|
+
// facet delta 적용
|
|
417
|
+
if (parsed.profile_delta) {
|
|
418
|
+
const profile = JSON.parse(fs.readFileSync(V1_PROFILE, 'utf-8'));
|
|
419
|
+
const clamp = (v) => Math.max(0.0, Math.min(1.0, v));
|
|
420
|
+
let changed = false;
|
|
421
|
+
if (parsed.profile_delta.quality_safety) {
|
|
422
|
+
const d = parsed.profile_delta.quality_safety;
|
|
423
|
+
const f = profile.axes.quality_safety.facets;
|
|
424
|
+
for (const [k, v] of Object.entries(d)) {
|
|
425
|
+
if (typeof v === 'number' && Math.abs(v) > 0.001 && k in f) {
|
|
426
|
+
f[k] = clamp(f[k] + v);
|
|
427
|
+
changed = true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (parsed.profile_delta.autonomy) {
|
|
432
|
+
const d = parsed.profile_delta.autonomy;
|
|
433
|
+
const f = profile.axes.autonomy.facets;
|
|
434
|
+
for (const [k, v] of Object.entries(d)) {
|
|
435
|
+
if (typeof v === 'number' && Math.abs(v) > 0.001 && k in f) {
|
|
436
|
+
f[k] = clamp(f[k] + v);
|
|
437
|
+
changed = true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (changed) {
|
|
442
|
+
profile.metadata.updated_at = new Date().toISOString();
|
|
443
|
+
fs.writeFileSync(V1_PROFILE, JSON.stringify(profile, null, 2));
|
|
444
|
+
process.stderr.write('[forgen-auto-compound] profile facets updated from session learning\n');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
process.stderr.write(`[forgen-auto-compound] session learning: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
452
|
+
}
|
|
453
|
+
// 완료 기록
|
|
454
|
+
const statePath = path.join(FORGEN_HOME, 'state', 'last-auto-compound.json');
|
|
455
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
456
|
+
fs.writeFileSync(statePath, JSON.stringify({ sessionId, completedAt: new Date().toISOString() }));
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
process.stderr.write(`[forgen-auto-compound] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
460
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — config hooks display
|
|
3
|
+
*
|
|
4
|
+
* `forgen config hooks` 명령 구현.
|
|
5
|
+
* 훅 상태, 감지된 플러그인, 컨텍스트 버짓을 ANSI 컬러로 출력합니다.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { HOOK_REGISTRY } from '../hooks/hook-registry.js';
|
|
9
|
+
import { FORGEN_HOME } from './paths.js';
|
|
10
|
+
import { detectInstalledPlugins } from './plugin-detector.js';
|
|
11
|
+
import { isHookEnabled } from '../hooks/hook-config.js';
|
|
12
|
+
import { calculateBudget } from '../hooks/shared/context-budget.js';
|
|
13
|
+
import { INJECTION_CAPS } from '../hooks/shared/injection-caps.js';
|
|
14
|
+
import { getHookConflicts } from './plugin-detector.js';
|
|
15
|
+
// ── ANSI helpers ──
|
|
16
|
+
const GREEN = '\x1b[32m';
|
|
17
|
+
const RED = '\x1b[31m';
|
|
18
|
+
const YELLOW = '\x1b[33m';
|
|
19
|
+
const DIM = '\x1b[2m';
|
|
20
|
+
const BOLD = '\x1b[1m';
|
|
21
|
+
const RESET = '\x1b[0m';
|
|
22
|
+
function green(s) { return `${GREEN}${s}${RESET}`; }
|
|
23
|
+
function red(s) { return `${RED}${s}${RESET}`; }
|
|
24
|
+
function yellow(s) { return `${YELLOW}${s}${RESET}`; }
|
|
25
|
+
function dim(s) { return `${DIM}${s}${RESET}`; }
|
|
26
|
+
function bold(s) { return `${BOLD}${s}${RESET}`; }
|
|
27
|
+
// ── 표시 로직 ──
|
|
28
|
+
const TIER_ORDER = ['compound-core', 'safety', 'workflow'];
|
|
29
|
+
const TIER_LABELS = {
|
|
30
|
+
'compound-core': 'compound-core (always active)',
|
|
31
|
+
'safety': 'safety',
|
|
32
|
+
'workflow': 'workflow',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* 훅 상태와 플러그인 감지 결과를 출력합니다.
|
|
36
|
+
*/
|
|
37
|
+
export async function displayHookStatus(cwd) {
|
|
38
|
+
const plugins = detectInstalledPlugins(cwd);
|
|
39
|
+
const budget = calculateBudget(cwd);
|
|
40
|
+
const hookConflicts = getHookConflicts(cwd);
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(bold(' Forgen — Hook Configuration'));
|
|
43
|
+
console.log();
|
|
44
|
+
// ── 감지된 플러그인 ──
|
|
45
|
+
if (plugins.length > 0) {
|
|
46
|
+
console.log(' Detected plugins:');
|
|
47
|
+
for (const p of plugins) {
|
|
48
|
+
const skillCount = p.overlappingSkills.length;
|
|
49
|
+
const detail = skillCount > 0 ? `(${skillCount} overlapping skills)` : '';
|
|
50
|
+
console.log(` ${green('●')} ${p.name.padEnd(20)} ${dim(detail)}`);
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
// ── 훅 상태 ──
|
|
55
|
+
// 전체 활성 수 계산
|
|
56
|
+
let activeCount = 0;
|
|
57
|
+
for (const h of HOOK_REGISTRY) {
|
|
58
|
+
if (isEffectivelyEnabled(h.name, h.tier, hookConflicts, plugins.length > 0))
|
|
59
|
+
activeCount++;
|
|
60
|
+
}
|
|
61
|
+
console.log(` Hook Status (${activeCount}/${HOOK_REGISTRY.length} active):`);
|
|
62
|
+
for (const tier of TIER_ORDER) {
|
|
63
|
+
const tierHooks = HOOK_REGISTRY.filter(h => h.tier === tier);
|
|
64
|
+
if (tierHooks.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
// workflow 티어가 자동 비활성화되었는지 확인
|
|
67
|
+
const tierAutoDisabled = tier === 'workflow' &&
|
|
68
|
+
plugins.length > 0 &&
|
|
69
|
+
tierHooks.some(h => hookConflicts.has(h.name));
|
|
70
|
+
const tierLabel = tierAutoDisabled
|
|
71
|
+
? `${TIER_LABELS[tier]} ${yellow(`(auto-disabled — ${getConflictingPluginName(hookConflicts)} detected)`)}`
|
|
72
|
+
: TIER_LABELS[tier];
|
|
73
|
+
console.log(` ${dim(tierLabel)}:`);
|
|
74
|
+
for (const hook of tierHooks) {
|
|
75
|
+
const enabled = isEffectivelyEnabled(hook.name, hook.tier, hookConflicts, plugins.length > 0);
|
|
76
|
+
const mark = enabled ? green('✓') : red('✗');
|
|
77
|
+
const nameCol = hook.name.padEnd(26);
|
|
78
|
+
const eventCol = hook.event.padEnd(20);
|
|
79
|
+
const timeoutStr = dim(`${hook.timeout}s`);
|
|
80
|
+
console.log(` ${mark} ${nameCol} ${dim(eventCol)} ${timeoutStr}`);
|
|
81
|
+
}
|
|
82
|
+
console.log();
|
|
83
|
+
}
|
|
84
|
+
// ── 컨텍스트 버짓 ──
|
|
85
|
+
console.log(' Context Budget:');
|
|
86
|
+
const factorStr = budget.otherPluginsDetected
|
|
87
|
+
? `${budget.factor} ${yellow('(reduced — other plugins detected)')}`
|
|
88
|
+
: `${budget.factor}`;
|
|
89
|
+
console.log(` Factor: ${factorStr}`);
|
|
90
|
+
console.log(` Solution injection: ${budget.solutionSessionMax} chars/session ${dim(`(default: ${INJECTION_CAPS.solutionSessionMax})`)}`);
|
|
91
|
+
console.log(` Notepad cap: ${budget.notepadMax} chars ${dim(`(default: ${INJECTION_CAPS.notepadMax})`)}`);
|
|
92
|
+
console.log();
|
|
93
|
+
// ── 경로 ──
|
|
94
|
+
const hooksJson = path.join(process.cwd(), 'hooks', 'hooks.json');
|
|
95
|
+
const configPath = path.join(FORGEN_HOME, 'hook-config.json');
|
|
96
|
+
console.log(` hooks.json: ${dim(hooksJson)} ${dim('(auto-generated)')}`);
|
|
97
|
+
console.log(` Config: ${dim(configPath)}`);
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
/** 충돌 맵에서 첫 번째 플러그인 이름 반환 */
|
|
101
|
+
function getConflictingPluginName(conflicts) {
|
|
102
|
+
const first = conflicts.values().next();
|
|
103
|
+
return first.done ? 'plugin' : first.value;
|
|
104
|
+
}
|
|
105
|
+
/** 실효 활성 여부 (plugin 감지 + tier + hook-config 모두 반영) */
|
|
106
|
+
function isEffectivelyEnabled(name, tier, hookConflicts, hasPlugins) {
|
|
107
|
+
if (!isHookEnabled(name))
|
|
108
|
+
return false;
|
|
109
|
+
if (hasPlugins && tier === 'workflow' && hookConflicts.has(name))
|
|
110
|
+
return false;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Config Injector
|
|
3
|
+
*
|
|
4
|
+
* v1 설계: Rule Renderer + Profile 기반 규칙 생성.
|
|
5
|
+
* philosophy/scope/pack ��반 직접 규칙 생성은 제거됨.
|
|
6
|
+
*
|
|
7
|
+
* Authoritative: docs/plans/2026-04-03-forgen-rule-renderer-spec.md
|
|
8
|
+
*/
|
|
9
|
+
/** 보안 규칙 (정적 — v1 GLOBAL_SAFETY_RULES와 동일 맥락) */
|
|
10
|
+
export declare function generateSecurityRules(): string;
|
|
11
|
+
/** 안티패턴 감지 규칙 (정적) */
|
|
12
|
+
export declare function generateAntiPatternRules(): string;
|
|
13
|
+
/** compound loop + 개인 규칙 (me/rules) 로드 */
|
|
14
|
+
export declare function generateCompoundRules(cwd: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Strip formatting that already exists in the source line BEFORE the
|
|
17
|
+
* renderer adds its own prefix/suffix. Without this, a behavior file
|
|
18
|
+
* whose content begins with `- **[의사결정]** ... (3회 관찰)` ends up
|
|
19
|
+
* rendered as `- - **[의사결정]** ... (3회 관찰) (1회 관찰)` — double
|
|
20
|
+
* bullet + double count observed in production.
|
|
21
|
+
*
|
|
22
|
+
* Exported under `__testOnly` below for C5 regression coverage.
|
|
23
|
+
*/
|
|
24
|
+
declare function normalizeDescription(raw: string): string;
|
|
25
|
+
/** 모든 규칙 파일을 생성하여 반환. v1RenderedRules가 있으면 포함. */
|
|
26
|
+
export declare function generateClaudeRuleFiles(cwd: string, v1RenderedRules?: string | null): Record<string, string>;
|
|
27
|
+
/** 하위 호환: 단일 규칙 문자열 생성 */
|
|
28
|
+
export declare function generateClaudeRules(cwd: string, v1RenderedRules?: string | null): string;
|
|
29
|
+
/** tmux 키바인딩 등록 */
|
|
30
|
+
export declare function registerTmuxBindings(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* B10 (2026-04-09): environment variables for the harness context.
|
|
33
|
+
*
|
|
34
|
+
* The canonical namespace is now `FORGEN_*`. The legacy `COMPOUND_*`
|
|
35
|
+
* names are set alongside for one transition period (third-party hooks
|
|
36
|
+
* or user scripts may still read them). When all consumers have been
|
|
37
|
+
* migrated and a major version ships, remove the `COMPOUND_*` lines.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildEnv(cwd: string, v1SessionId?: string): Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Test-only exports for the C5 rendering pipeline. The ergonomic choice
|
|
42
|
+
* over `export function normalizeDescription` is intentional: anything
|
|
43
|
+
* reached via `__testOnly` is explicitly flagged as "not for production
|
|
44
|
+
* callers" and easy to grep for in future refactors.
|
|
45
|
+
*/
|
|
46
|
+
export declare const __testOnly: {
|
|
47
|
+
normalizeDescription: typeof normalizeDescription;
|
|
48
|
+
SELF_REFERENTIAL_PATTERNS: readonly RegExp[];
|
|
49
|
+
};
|
|
50
|
+
export {};
|