@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,455 @@
|
|
|
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
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { ME_BEHAVIOR, ME_DIR, ME_RULES } from './paths.js';
|
|
12
|
+
import { createLogger } from './logger.js';
|
|
13
|
+
import { parseSolutionV3 } from '../engine/solution-format.js';
|
|
14
|
+
import { containsPromptInjection } from '../hooks/prompt-injection-filter.js';
|
|
15
|
+
import { RULE_FILE_CAPS, truncateContent } from '../hooks/shared/injection-caps.js';
|
|
16
|
+
const log = createLogger('config-injector');
|
|
17
|
+
/**
|
|
18
|
+
* 디렉토리의 .md 파일에서 규칙 첫 줄(요약)을 추출.
|
|
19
|
+
* trusted=false일 때 프롬프트 인젝션 스캔 적용.
|
|
20
|
+
*/
|
|
21
|
+
function loadRulesFromDir(dir, trusted = true) {
|
|
22
|
+
if (!fs.existsSync(dir))
|
|
23
|
+
return [];
|
|
24
|
+
try {
|
|
25
|
+
return fs.readdirSync(dir)
|
|
26
|
+
.filter(f => f.endsWith('.md'))
|
|
27
|
+
.map(f => {
|
|
28
|
+
const filePath = path.join(dir, f);
|
|
29
|
+
if (fs.lstatSync(filePath).isSymbolicLink())
|
|
30
|
+
return null;
|
|
31
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
32
|
+
const parsed = parseSolutionV3(content);
|
|
33
|
+
const body = parsed ? parsed.content : stripFrontmatter(content);
|
|
34
|
+
if (!trusted) {
|
|
35
|
+
if (containsPromptInjection(body)) {
|
|
36
|
+
log.debug(`규칙 파일 인젝션 감지 — 차단: ${filePath}`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const firstLine = firstMeaningfulLine(body);
|
|
41
|
+
return firstLine ?? f.replace('.md', '');
|
|
42
|
+
})
|
|
43
|
+
.filter((rule) => Boolean(rule));
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
log.debug(`규칙 디렉토리 읽기 실패: ${dir}`, e);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function stripFrontmatter(content) {
|
|
51
|
+
const trimmed = content.trimStart();
|
|
52
|
+
if (!trimmed.startsWith('---'))
|
|
53
|
+
return content;
|
|
54
|
+
const endIdx = trimmed.indexOf('---', 3);
|
|
55
|
+
if (endIdx === -1)
|
|
56
|
+
return content;
|
|
57
|
+
return trimmed.slice(endIdx + 3);
|
|
58
|
+
}
|
|
59
|
+
function firstMeaningfulLine(content) {
|
|
60
|
+
for (const rawLine of content.split('\n')) {
|
|
61
|
+
const line = rawLine.trim();
|
|
62
|
+
if (!line)
|
|
63
|
+
continue;
|
|
64
|
+
if (line === '## Context' || line === '## Content')
|
|
65
|
+
continue;
|
|
66
|
+
return line.replace(/^#+\s*/, '').trim();
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** 프로젝트 맵에서 에이전트용 요약 생성 */
|
|
71
|
+
function loadProjectMapSummary(cwd) {
|
|
72
|
+
const mapPath = path.join(cwd, '.compound', 'project-map.json');
|
|
73
|
+
if (!fs.existsSync(mapPath))
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const map = JSON.parse(fs.readFileSync(mapPath, 'utf-8'));
|
|
77
|
+
const { summary } = map;
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(`- Project: ${summary.name} (${summary.totalFiles} files, ${summary.totalLines.toLocaleString()} lines)`);
|
|
80
|
+
if (summary.framework)
|
|
81
|
+
lines.push(`- Framework: ${summary.framework}`);
|
|
82
|
+
if (summary.packageManager)
|
|
83
|
+
lines.push(`- Package manager: ${summary.packageManager}`);
|
|
84
|
+
const topLangs = Object.entries(summary.languages)
|
|
85
|
+
.sort((a, b) => b[1] - a[1])
|
|
86
|
+
.filter(([l]) => l !== 'other')
|
|
87
|
+
.slice(0, 3);
|
|
88
|
+
if (topLangs.length > 0) {
|
|
89
|
+
lines.push(`- Languages: ${topLangs.map(([l, n]) => `${l}(${n} lines)`).join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
if (map.entryPoints.length > 0) {
|
|
92
|
+
lines.push(`- Entry points: ${map.entryPoints.slice(0, 5).join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
const topDirs = map.directories
|
|
95
|
+
.filter(d => d.purpose && !d.path.includes('/'))
|
|
96
|
+
.slice(0, 8);
|
|
97
|
+
if (topDirs.length > 0) {
|
|
98
|
+
lines.push('- Directories:');
|
|
99
|
+
for (const dir of topDirs) {
|
|
100
|
+
lines.push(` - \`${dir.path}/\` — ${dir.purpose}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ── v1 Static Rules ──
|
|
110
|
+
/** 보안 규칙 (정적 — v1 GLOBAL_SAFETY_RULES와 동일 맥락) */
|
|
111
|
+
export function generateSecurityRules() {
|
|
112
|
+
return [
|
|
113
|
+
'# Forgen — Security Rules',
|
|
114
|
+
'',
|
|
115
|
+
'## Dangerous Command Warning',
|
|
116
|
+
'- Always confirm before executing destructive commands like `rm -rf`, `git push --force`, `DROP TABLE`',
|
|
117
|
+
'- Double confirmation required for production environment access',
|
|
118
|
+
'',
|
|
119
|
+
'## Secret Key Protection',
|
|
120
|
+
'- Do not commit sensitive information such as `.env`, `credentials.json`, API keys',
|
|
121
|
+
'- Manage through environment variables or a secrets manager',
|
|
122
|
+
'- Detect hardcoded secrets during code review',
|
|
123
|
+
'',
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
126
|
+
/** 안티패턴 감지 규칙 (정적) */
|
|
127
|
+
export function generateAntiPatternRules() {
|
|
128
|
+
return [
|
|
129
|
+
'# Forgen — Anti-Pattern Detection',
|
|
130
|
+
'',
|
|
131
|
+
'## Repeated Edit Warning',
|
|
132
|
+
'- Stop immediately when editing the same file 3+ times → full structure redesign required',
|
|
133
|
+
'- For 5+ edits, always check current state with Read before replacing with a single Write',
|
|
134
|
+
'',
|
|
135
|
+
'## Error Suppression Warning',
|
|
136
|
+
'- No empty catch blocks — at minimum log or re-throw',
|
|
137
|
+
'- Minimize suppression comments like eslint-disable, @ts-ignore',
|
|
138
|
+
'',
|
|
139
|
+
'## Excessive Complexity Warning',
|
|
140
|
+
'- Consider splitting single functions exceeding 50 lines',
|
|
141
|
+
'- Apply early return pattern when nesting depth exceeds 4',
|
|
142
|
+
'- No unnecessary abstraction — implement only what is currently needed',
|
|
143
|
+
'',
|
|
144
|
+
].join('\n');
|
|
145
|
+
}
|
|
146
|
+
/** compound loop + 개인 규칙 (me/rules) 로드 */
|
|
147
|
+
export function generateCompoundRules(cwd) {
|
|
148
|
+
const lines = [
|
|
149
|
+
'# Forgen — Compound Loop',
|
|
150
|
+
'',
|
|
151
|
+
];
|
|
152
|
+
// 프로젝트 맵 요약 주입
|
|
153
|
+
const mapSummary = loadProjectMapSummary(cwd);
|
|
154
|
+
if (mapSummary) {
|
|
155
|
+
lines.push('## Project Structure (auto-generated)');
|
|
156
|
+
lines.push(mapSummary);
|
|
157
|
+
lines.push('');
|
|
158
|
+
}
|
|
159
|
+
// 개인 규칙 로드
|
|
160
|
+
//
|
|
161
|
+
// B7 security hardening (2026-04-09): ME_RULES is user-owned but still
|
|
162
|
+
// writable by any process the user runs (including auto-compound and
|
|
163
|
+
// skill-injector). An attacker who can write a single file into
|
|
164
|
+
// `~/.forgen/me/rules/` via a crafted prompt/skill promotion can
|
|
165
|
+
// inject instructions into every Claude session. Run the same
|
|
166
|
+
// injection filter the behavior directory already uses for
|
|
167
|
+
// consistency. The previous `trusted=true` default was safe only
|
|
168
|
+
// under the assumption that ME_RULES was exclusively human-authored,
|
|
169
|
+
// which isn't the case in practice.
|
|
170
|
+
const meRules = loadRulesFromDir(ME_RULES, false);
|
|
171
|
+
if (meRules.length > 0) {
|
|
172
|
+
lines.push('## Personal Rules (Me)');
|
|
173
|
+
for (const rule of meRules) {
|
|
174
|
+
lines.push(`- ${rule}`);
|
|
175
|
+
}
|
|
176
|
+
lines.push('');
|
|
177
|
+
}
|
|
178
|
+
return lines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Phrases that indicate a "pattern" is actually echoing a Claude response
|
|
182
|
+
* rather than a genuine user-behavior signal. Observed in production:
|
|
183
|
+
* auto-compound was picking up snippets of its own output ("다음 대화에서
|
|
184
|
+
* 분석하겠습니다", "3개 패턴을 메모리에 추가했습니다", "Step 1 완료") and
|
|
185
|
+
* treating them as learned user patterns.
|
|
186
|
+
*
|
|
187
|
+
* C5 fix (2026-04-09): filter these at render time so they never reach
|
|
188
|
+
* `~/.claude/rules/forge-behavioral.md`. The source files under
|
|
189
|
+
* `~/.forgen/me/behavior/` are left in place — this is a display-time
|
|
190
|
+
* filter, not a data-mutation step, so a bad filter regex here can't
|
|
191
|
+
* destroy legitimate history.
|
|
192
|
+
*
|
|
193
|
+
* Anchoring rules (H-2 fix):
|
|
194
|
+
* 1. Every regex is either START-anchored (`^`) or requires a narrow
|
|
195
|
+
* prefix context. A bare `/분석하겠습니다/` would false-positive on
|
|
196
|
+
* a legit user pattern like "관련 문서를 분석하겠습니다" (the user
|
|
197
|
+
* stating their preference to analyze docs). Anchoring prevents
|
|
198
|
+
* this by requiring the phrase to be the *beginning* of the line,
|
|
199
|
+
* which is the actual Claude-response failure mode.
|
|
200
|
+
* 2. Self-reference to the tool itself (`forgen`/`compound`) is
|
|
201
|
+
* narrowed to meta-announcement shapes like "N개 패턴을 …에 추가"
|
|
202
|
+
* — a legit user rule like "use compound when refactoring" is
|
|
203
|
+
* NOT filtered. The earlier bare `/forgen|compound/i` would have
|
|
204
|
+
* dropped any user pattern that happened to name the tool.
|
|
205
|
+
* 3. English Claude-response templates are covered too. Auto-compound
|
|
206
|
+
* will eventually process mixed-language transcripts and the
|
|
207
|
+
* filter must catch English leakage as well as Korean.
|
|
208
|
+
*/
|
|
209
|
+
const SELF_REFERENTIAL_PATTERNS = Object.freeze([
|
|
210
|
+
// Korean — Claude-voice announcements at line start.
|
|
211
|
+
// Note: we deliberately DO NOT filter bare `/분석하겠습니다/` because
|
|
212
|
+
// a user rule like "관련 문서를 분석하겠습니다" is a legitimate
|
|
213
|
+
// user-voice statement. The "다음/이번/현재 (대화|세션|작업)에서"
|
|
214
|
+
// prefix + "분석하겠습니다" suffix is Claude-voice; the prefix alone
|
|
215
|
+
// is enough of a discriminator.
|
|
216
|
+
/^관찰된 새로운 패턴 없습니다/,
|
|
217
|
+
/^\d+개 패턴을.*(메모리|compound|forgen).*(추가|기록)/,
|
|
218
|
+
/^계획이 진행 중/,
|
|
219
|
+
/^(다음|이번|현재) (대화|세션|작업)에서/,
|
|
220
|
+
/^Step \d/,
|
|
221
|
+
// Claude permission/proceed flow markers — observed in auto-captured
|
|
222
|
+
// behavior file `auto-2026-04-07-preference.md`. These are specific
|
|
223
|
+
// Korean phrases an assistant uses when asking the user to approve
|
|
224
|
+
// an action. A user writing their own preference would not phrase
|
|
225
|
+
// it as "승인하면 다음을 확인합니다" or end with "진행할까요?".
|
|
226
|
+
/권한\s*(확인|요청)이?\s*필요합니다/,
|
|
227
|
+
/^승인하(면|시면)/,
|
|
228
|
+
/진행할까요\??/,
|
|
229
|
+
// English — Claude response templates at line start.
|
|
230
|
+
/^I['\u2019]?ll\s+(analyze|review|check|update|add|create|run|fix)/i,
|
|
231
|
+
/^Let me\s+(analyze|check|look|verify|update|add)/i,
|
|
232
|
+
/^I['\u2019]?ve\s+(added|updated|created|fixed|completed)/i,
|
|
233
|
+
// Object.freeze is defense-in-depth: the readonly type is compile-time
|
|
234
|
+
// only. Freezing prevents runtime mutation by any other module loaded
|
|
235
|
+
// in the same process from silently disabling the filter by pushing
|
|
236
|
+
// an over-broad pattern or emptying the array.
|
|
237
|
+
]);
|
|
238
|
+
/**
|
|
239
|
+
* Strip formatting that already exists in the source line BEFORE the
|
|
240
|
+
* renderer adds its own prefix/suffix. Without this, a behavior file
|
|
241
|
+
* whose content begins with `- **[의사결정]** ... (3회 관찰)` ends up
|
|
242
|
+
* rendered as `- - **[의사결정]** ... (3회 관찰) (1회 관찰)` — double
|
|
243
|
+
* bullet + double count observed in production.
|
|
244
|
+
*
|
|
245
|
+
* Exported under `__testOnly` below for C5 regression coverage.
|
|
246
|
+
*/
|
|
247
|
+
function normalizeDescription(raw) {
|
|
248
|
+
let desc = raw.trim();
|
|
249
|
+
// Strip any number of leading bullet markers: `- `, `* `, `• `
|
|
250
|
+
desc = desc.replace(/^(?:[-*•]\s+)+/, '');
|
|
251
|
+
// Strip trailing inline "N회 관찰" suffixes (can be chained from
|
|
252
|
+
// earlier render passes). Note the space before the paren.
|
|
253
|
+
desc = desc.replace(/(?:\s*\(\d+회 관찰\))+$/, '');
|
|
254
|
+
return desc.trim();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* 학습된 선호/사고 패턴을 규칙으로 변환.
|
|
258
|
+
*/
|
|
259
|
+
function generateBehavioralRules() {
|
|
260
|
+
const lines = ['# Forgen — Learned Patterns', '# auto-generated from observed interactions', ''];
|
|
261
|
+
try {
|
|
262
|
+
if (!fs.existsSync(ME_BEHAVIOR))
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
const files = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.md'));
|
|
265
|
+
const categories = {
|
|
266
|
+
'Thinking Style': [],
|
|
267
|
+
'Response Preferences': [],
|
|
268
|
+
'Workflow': [],
|
|
269
|
+
};
|
|
270
|
+
for (const file of files) {
|
|
271
|
+
const filePath = path.join(ME_BEHAVIOR, file);
|
|
272
|
+
if (fs.lstatSync(filePath).isSymbolicLink())
|
|
273
|
+
continue;
|
|
274
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
275
|
+
const trimmed = raw.trimStart();
|
|
276
|
+
if (!trimmed.startsWith('---'))
|
|
277
|
+
continue;
|
|
278
|
+
const endIdx = trimmed.indexOf('---', 3);
|
|
279
|
+
if (endIdx === -1)
|
|
280
|
+
continue;
|
|
281
|
+
const fm = trimmed.slice(3, endIdx);
|
|
282
|
+
const body = trimmed.slice(endIdx + 3).trim();
|
|
283
|
+
const kindMatch = fm.match(/^kind:\s*(.+)$/m);
|
|
284
|
+
const countMatch = fm.match(/^observedCount:\s*(\d+)/m);
|
|
285
|
+
const kind = kindMatch?.[1]?.trim().replace(/^["']|["']$/g, '') ?? '';
|
|
286
|
+
const observedCount = countMatch ? parseInt(countMatch[1], 10) : 0;
|
|
287
|
+
const contentIdx = body.indexOf('## Content');
|
|
288
|
+
const contentBody = contentIdx >= 0 ? body.slice(contentIdx + '## Content'.length) : body;
|
|
289
|
+
const rawDesc = contentBody.split('\n').find(l => {
|
|
290
|
+
const t = l.trim();
|
|
291
|
+
return t.length >= 5 && !t.startsWith('##');
|
|
292
|
+
});
|
|
293
|
+
if (!rawDesc)
|
|
294
|
+
continue;
|
|
295
|
+
// C5: strip any pre-existing bullet/count formatting so we don't
|
|
296
|
+
// stack `- -` and `(3회 관찰) (1회 관찰)` on re-render.
|
|
297
|
+
const desc = normalizeDescription(rawDesc);
|
|
298
|
+
if (desc.length < 5)
|
|
299
|
+
continue;
|
|
300
|
+
// C5 edge case (2026-04-09): if the description text already
|
|
301
|
+
// contains an inline "N회 관찰" marker ANYWHERE (not just at the
|
|
302
|
+
// trailing-suffix position normalizeDescription strips), don't
|
|
303
|
+
// append another count from frontmatter. Observed data: source
|
|
304
|
+
// files like `auto-2026-04-02.md` have descriptions ending in
|
|
305
|
+
// `(compound-engineering-plugin, ohmyopencode 등과 반복 비교 요청 — 3회 관찰)`,
|
|
306
|
+
// where the `3회 관찰` is embedded inside a long parenthetical —
|
|
307
|
+
// normalizeDescription's tail regex can't strip it because the
|
|
308
|
+
// outer paren is not right before the count. Without this check,
|
|
309
|
+
// the renderer appends its own `(1회 관찰)` (from frontmatter
|
|
310
|
+
// observedCount) and produces `... 3회 관찰) (1회 관찰)`.
|
|
311
|
+
const hasInlineCount = /\d+회 관찰/.test(desc);
|
|
312
|
+
// C5: filter self-referential noise (Claude's own responses
|
|
313
|
+
// captured as "user patterns").
|
|
314
|
+
if (SELF_REFERENTIAL_PATTERNS.some(re => re.test(desc)))
|
|
315
|
+
continue;
|
|
316
|
+
// C5 security hardening (MEDIUM-1 from review): reject any
|
|
317
|
+
// behavior-file content that looks like a prompt injection
|
|
318
|
+
// payload. `generateCompoundRules`'s `loadRulesFromDir` already
|
|
319
|
+
// runs this check with `trusted=false` — this mirrors it for
|
|
320
|
+
// the auto-compound-populated behavior directory, which is a
|
|
321
|
+
// higher-risk input source because payloads can be injected
|
|
322
|
+
// indirectly via transcripts/commit messages that auto-compound
|
|
323
|
+
// observes. Without this filter, a crafted user prompt could
|
|
324
|
+
// cause a malicious instruction to be written into
|
|
325
|
+
// `forge-behavioral.md` and re-injected on every session.
|
|
326
|
+
if (containsPromptInjection(desc))
|
|
327
|
+
continue;
|
|
328
|
+
const countStr = observedCount > 0 && !hasInlineCount
|
|
329
|
+
? ` (${observedCount}회 관찰)`
|
|
330
|
+
: '';
|
|
331
|
+
if (kind === 'thinking') {
|
|
332
|
+
categories['Thinking Style'].push(`- ${desc}${countStr}`);
|
|
333
|
+
}
|
|
334
|
+
else if (kind === 'workflow') {
|
|
335
|
+
// observedCount >= 3인 워크플로우는 directive 형태로 렌더링
|
|
336
|
+
if (observedCount >= 3) {
|
|
337
|
+
categories.Workflow.push(`- **[적용]** ${desc}${countStr}`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
categories.Workflow.push(`- ${desc}${countStr}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (kind === 'preference') {
|
|
344
|
+
categories['Response Preferences'].push(`- ${desc}${countStr}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const [cat, items] of Object.entries(categories)) {
|
|
348
|
+
if (items.length === 0)
|
|
349
|
+
continue;
|
|
350
|
+
lines.push(`## ${cat}`);
|
|
351
|
+
if (cat === 'Workflow') {
|
|
352
|
+
lines.push('> Items marked **[적용]** are confirmed patterns (3+ observations). Follow these as default workflow unless the user overrides.');
|
|
353
|
+
}
|
|
354
|
+
lines.push(...items);
|
|
355
|
+
lines.push('');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// 행동 디렉토리 접근 실패 시 빈 규칙
|
|
360
|
+
}
|
|
361
|
+
return lines.length <= 3 ? '' : lines.join('\n');
|
|
362
|
+
}
|
|
363
|
+
/** 모든 규칙 파일을 생성하여 반환. v1RenderedRules가 있으면 포함. */
|
|
364
|
+
export function generateClaudeRuleFiles(cwd, v1RenderedRules) {
|
|
365
|
+
const v1Rules = v1RenderedRules
|
|
366
|
+
? `# Forgen v1 — Rendered Rules\n# auto-generated from profile + rule store\n\n${v1RenderedRules}`
|
|
367
|
+
: null;
|
|
368
|
+
// 정적 규칙 + compound
|
|
369
|
+
const coreSections = [
|
|
370
|
+
generateSecurityRules(),
|
|
371
|
+
generateAntiPatternRules(),
|
|
372
|
+
generateCompoundRules(cwd),
|
|
373
|
+
].filter(s => s.trim().length > 0);
|
|
374
|
+
const rules = {
|
|
375
|
+
'project-context.md': coreSections.join('\n\n---\n\n'),
|
|
376
|
+
};
|
|
377
|
+
// v1 rendered rules (profile 기반 개인화 규칙)
|
|
378
|
+
if (v1Rules) {
|
|
379
|
+
rules['v1-rules.md'] = v1Rules;
|
|
380
|
+
}
|
|
381
|
+
// 학습된 행동 패턴
|
|
382
|
+
const behavioral = generateBehavioralRules();
|
|
383
|
+
if (behavioral) {
|
|
384
|
+
rules['forge-behavioral.md'] = behavioral;
|
|
385
|
+
}
|
|
386
|
+
// USER.md → 사용자 프로필 주입
|
|
387
|
+
const userMdPath = path.join(ME_DIR, 'USER.md');
|
|
388
|
+
try {
|
|
389
|
+
if (fs.existsSync(userMdPath) && !fs.lstatSync(userMdPath).isSymbolicLink()) {
|
|
390
|
+
const raw = fs.readFileSync(userMdPath, 'utf-8').trim();
|
|
391
|
+
if (raw.length > 0) {
|
|
392
|
+
const truncated = truncateContent(raw, RULE_FILE_CAPS.perRuleFile);
|
|
393
|
+
rules['user-profile.md'] = [
|
|
394
|
+
'# Forgen — User Profile',
|
|
395
|
+
'# auto-injected from ~/.forgen/me/USER.md',
|
|
396
|
+
'',
|
|
397
|
+
truncated,
|
|
398
|
+
'',
|
|
399
|
+
].join('\n');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
log.debug('USER.md 로드 실���', e);
|
|
405
|
+
}
|
|
406
|
+
return rules;
|
|
407
|
+
}
|
|
408
|
+
/** 하위 호환: 단일 규칙 문자열 생성 */
|
|
409
|
+
export function generateClaudeRules(cwd, v1RenderedRules) {
|
|
410
|
+
const files = generateClaudeRuleFiles(cwd, v1RenderedRules);
|
|
411
|
+
return Object.values(files).join('\n');
|
|
412
|
+
}
|
|
413
|
+
/** tmux 키바인딩 등록 */
|
|
414
|
+
export async function registerTmuxBindings() {
|
|
415
|
+
const { execFileSync } = await import('node:child_process');
|
|
416
|
+
try {
|
|
417
|
+
execFileSync('tmux', ['bind-key', 'T', 'run-shell', 'forgen me'], { stdio: 'ignore' });
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
log.debug('tmux 키바인딩 등��� 실패', e);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* B10 (2026-04-09): environment variables for the harness context.
|
|
425
|
+
*
|
|
426
|
+
* The canonical namespace is now `FORGEN_*`. The legacy `COMPOUND_*`
|
|
427
|
+
* names are set alongside for one transition period (third-party hooks
|
|
428
|
+
* or user scripts may still read them). When all consumers have been
|
|
429
|
+
* migrated and a major version ships, remove the `COMPOUND_*` lines.
|
|
430
|
+
*/
|
|
431
|
+
export function buildEnv(cwd, v1SessionId) {
|
|
432
|
+
const env = {
|
|
433
|
+
// New canonical names
|
|
434
|
+
FORGEN_HARNESS: '1',
|
|
435
|
+
FORGEN_CWD: cwd,
|
|
436
|
+
FORGEN_V1: '1',
|
|
437
|
+
// Legacy compat (remove in next major)
|
|
438
|
+
COMPOUND_HARNESS: '1',
|
|
439
|
+
COMPOUND_CWD: cwd,
|
|
440
|
+
};
|
|
441
|
+
if (v1SessionId) {
|
|
442
|
+
env.FORGEN_SESSION_ID = v1SessionId;
|
|
443
|
+
}
|
|
444
|
+
return env;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Test-only exports for the C5 rendering pipeline. The ergonomic choice
|
|
448
|
+
* over `export function normalizeDescription` is intentional: anything
|
|
449
|
+
* reached via `__testOnly` is explicitly flagged as "not for production
|
|
450
|
+
* callers" and easy to grep for in future refactors.
|
|
451
|
+
*/
|
|
452
|
+
export const __testOnly = {
|
|
453
|
+
normalizeDescription,
|
|
454
|
+
SELF_REFERENTIAL_PATTERNS,
|
|
455
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDoctor(): Promise<void>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_PHILOSOPHY, ME_SOLUTIONS, ME_RULES, PACKS_DIR, SESSIONS_DIR } from './paths.js';
|
|
6
|
+
/** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
|
|
7
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
8
|
+
function check(label, condition, hint) {
|
|
9
|
+
const icon = condition ? '✓' : '✗';
|
|
10
|
+
const hintStr = !condition && hint ? ` — ${hint}` : '';
|
|
11
|
+
console.log(` ${icon} ${label}${hintStr}`);
|
|
12
|
+
}
|
|
13
|
+
function exists(p) {
|
|
14
|
+
return fs.existsSync(p);
|
|
15
|
+
}
|
|
16
|
+
function commandExists(cmd) {
|
|
17
|
+
try {
|
|
18
|
+
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
19
|
+
execFileSync(checker, [cmd], { stdio: 'pipe' });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function runDoctor() {
|
|
27
|
+
console.log('\n Forgen — Diagnostics\n');
|
|
28
|
+
console.log(' [Tools]');
|
|
29
|
+
check('claude CLI', commandExists('claude'));
|
|
30
|
+
check('tmux', commandExists('tmux'));
|
|
31
|
+
check('git', commandExists('git'));
|
|
32
|
+
check('gh (GitHub CLI)', commandExists('gh'), 'Required for team PR features: brew install gh');
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(' [Plugins]');
|
|
35
|
+
const ralphLoopInstalled = exists(path.join(os.homedir(), '.claude', 'plugins', 'cache', 'claude-plugins-official', 'ralph-loop'));
|
|
36
|
+
check('ralph-loop plugin', ralphLoopInstalled, 'Required for ralph mode auto-iteration. Install: claude plugins install ralph-loop');
|
|
37
|
+
// forgen 플러그인 캐시 디렉토리 확인 — 훅 실행의 필수 전제
|
|
38
|
+
const pluginCacheBase = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'forgen-local', 'forgen');
|
|
39
|
+
let forgenPluginCacheOk = false;
|
|
40
|
+
if (exists(pluginCacheBase)) {
|
|
41
|
+
const versions = fs.readdirSync(pluginCacheBase).filter(f => {
|
|
42
|
+
try {
|
|
43
|
+
const lstat = fs.lstatSync(path.join(pluginCacheBase, f));
|
|
44
|
+
return lstat.isDirectory() || lstat.isSymbolicLink();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
forgenPluginCacheOk = versions.length > 0;
|
|
51
|
+
}
|
|
52
|
+
check('forgen plugin cache', forgenPluginCacheOk, 'Hook execution requires plugin cache. Fix: npm run build && node scripts/postinstall.js');
|
|
53
|
+
// installed_plugins.json 정합성 확인
|
|
54
|
+
const installedPluginsPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
55
|
+
let pluginRegistered = false;
|
|
56
|
+
if (exists(installedPluginsPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const installed = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
|
|
59
|
+
const entry = installed?.plugins?.['forgen@forgen-local'];
|
|
60
|
+
if (Array.isArray(entry) && entry.length > 0) {
|
|
61
|
+
const installPath = entry[0]?.installPath;
|
|
62
|
+
pluginRegistered = !!installPath && exists(installPath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
check('forgen plugin registered & installPath exists', pluginRegistered, 'Plugin registered but installPath missing on disk. Fix: npm run build && node scripts/postinstall.js');
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(' [Directories]');
|
|
70
|
+
check('~/.forgen/', exists(FORGEN_HOME));
|
|
71
|
+
check('~/.forgen/me/', exists(ME_DIR));
|
|
72
|
+
check('~/.forgen/me/solutions/', exists(ME_SOLUTIONS));
|
|
73
|
+
check('~/.forgen/me/behavior/', exists(ME_BEHAVIOR));
|
|
74
|
+
check('~/.forgen/me/rules/', exists(ME_RULES));
|
|
75
|
+
check('~/.forgen/packs/', exists(PACKS_DIR));
|
|
76
|
+
check('~/.forgen/sessions/', exists(SESSIONS_DIR));
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(' [Philosophy]');
|
|
79
|
+
check('philosophy.json', exists(ME_PHILOSOPHY));
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(' [Environment]');
|
|
82
|
+
check('Inside tmux session', !!process.env.TMUX);
|
|
83
|
+
check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1');
|
|
84
|
+
console.log();
|
|
85
|
+
// 솔루션/규칙 수
|
|
86
|
+
if (exists(ME_SOLUTIONS)) {
|
|
87
|
+
const solutions = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md')).length;
|
|
88
|
+
console.log(` Personal solutions: ${solutions}`);
|
|
89
|
+
}
|
|
90
|
+
if (exists(ME_BEHAVIOR)) {
|
|
91
|
+
const behavior = fs.readdirSync(ME_BEHAVIOR).filter((f) => f.endsWith('.md')).length;
|
|
92
|
+
console.log(` Behavioral patterns: ${behavior}`);
|
|
93
|
+
}
|
|
94
|
+
if (exists(ME_RULES)) {
|
|
95
|
+
const rules = fs.readdirSync(ME_RULES).filter((f) => f.endsWith('.md')).length;
|
|
96
|
+
console.log(` Personal rules: ${rules}`);
|
|
97
|
+
}
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(' [Log Locations]');
|
|
100
|
+
console.log(` Session logs: ${SESSIONS_DIR}`);
|
|
101
|
+
if (exists(SESSIONS_DIR)) {
|
|
102
|
+
const sessionCount = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json')).length;
|
|
103
|
+
console.log(` Saved sessions: ${sessionCount}`);
|
|
104
|
+
}
|
|
105
|
+
console.log(` Claude Code sessions: ${CLAUDE_PROJECTS_DIR}`);
|
|
106
|
+
console.log();
|
|
107
|
+
console.log();
|
|
108
|
+
// v1: 팀 팩 시스템 제거. 개인 모드만 지원.
|
|
109
|
+
console.log(' [Pack Connections]');
|
|
110
|
+
console.log(' v1: Personal mode only (team packs removed)');
|
|
111
|
+
console.log();
|
|
112
|
+
// Lab 데이터 정리
|
|
113
|
+
const labExpDir = path.join(LAB_DIR, 'experiments');
|
|
114
|
+
if (exists(labExpDir)) {
|
|
115
|
+
const expFiles = fs.readdirSync(labExpDir).filter(f => f.endsWith('.json'));
|
|
116
|
+
// 1차 필터: 0바이트 또는 50바이트 미만 파일 (빠른 stat 기반)
|
|
117
|
+
const emptyFiles = expFiles.filter(f => {
|
|
118
|
+
try {
|
|
119
|
+
const stat = fs.statSync(path.join(labExpDir, f));
|
|
120
|
+
if (stat.size < 50)
|
|
121
|
+
return true;
|
|
122
|
+
// --clean-experiments 플래그가 있을 때만 내용 파싱 (성능 보호)
|
|
123
|
+
if (!process.argv.includes('--clean-experiments'))
|
|
124
|
+
return false;
|
|
125
|
+
const content = JSON.parse(fs.readFileSync(path.join(labExpDir, f), 'utf-8'));
|
|
126
|
+
return content.variants?.every((v) => !v.sessionIds?.length);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (emptyFiles.length > 0) {
|
|
133
|
+
console.log(` [Lab Cleanup]`);
|
|
134
|
+
console.log(` Empty experiment files: ${emptyFiles.length} / ${expFiles.length}`);
|
|
135
|
+
if (process.argv.includes('--clean-experiments')) {
|
|
136
|
+
let cleaned = 0;
|
|
137
|
+
for (const f of emptyFiles) {
|
|
138
|
+
try {
|
|
139
|
+
fs.unlinkSync(path.join(labExpDir, f));
|
|
140
|
+
cleaned++;
|
|
141
|
+
}
|
|
142
|
+
catch { /* skip */ }
|
|
143
|
+
}
|
|
144
|
+
console.log(` → Cleaned ${cleaned} empty experiment files`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log(` Run \`forgen doctor --clean-experiments\` to remove them`);
|
|
148
|
+
}
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 현재 디렉토리 git 정보
|
|
153
|
+
console.log(' [Git]');
|
|
154
|
+
try {
|
|
155
|
+
const remote = execFileSync('git', ['remote', 'get-url', 'origin'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
156
|
+
console.log(` remote (origin): ${remote}`);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// git 저장소가 아니거나 origin이 없으면 표시하지 않음
|
|
160
|
+
console.log(' git remote: (none)');
|
|
161
|
+
}
|
|
162
|
+
console.log();
|
|
163
|
+
}
|