@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.
Files changed (268) hide show
  1. package/.claude-plugin/plugin.json +20 -0
  2. package/CHANGELOG.md +353 -0
  3. package/CONTRIBUTING.md +98 -0
  4. package/LICENSE +21 -0
  5. package/README.ja.md +469 -0
  6. package/README.ko.md +469 -0
  7. package/README.md +483 -0
  8. package/README.zh.md +469 -0
  9. package/agents/analyst.md +98 -0
  10. package/agents/architect.md +62 -0
  11. package/agents/code-reviewer.md +120 -0
  12. package/agents/code-simplifier.md +197 -0
  13. package/agents/critic.md +70 -0
  14. package/agents/debugger.md +117 -0
  15. package/agents/designer.md +131 -0
  16. package/agents/executor.md +54 -0
  17. package/agents/explore.md +145 -0
  18. package/agents/git-master.md +212 -0
  19. package/agents/performance-reviewer.md +172 -0
  20. package/agents/planner.md +29 -0
  21. package/agents/qa-tester.md +158 -0
  22. package/agents/refactoring-expert.md +168 -0
  23. package/agents/scientist.md +144 -0
  24. package/agents/security-reviewer.md +137 -0
  25. package/agents/test-engineer.md +153 -0
  26. package/agents/verifier.md +133 -0
  27. package/agents/writer.md +184 -0
  28. package/commands/api-design.md +268 -0
  29. package/commands/architecture-decision.md +314 -0
  30. package/commands/ci-cd.md +270 -0
  31. package/commands/code-review.md +233 -0
  32. package/commands/compound.md +117 -0
  33. package/commands/database.md +263 -0
  34. package/commands/debug-detective.md +99 -0
  35. package/commands/docker.md +274 -0
  36. package/commands/documentation.md +276 -0
  37. package/commands/ecomode.md +51 -0
  38. package/commands/frontend.md +271 -0
  39. package/commands/git-master.md +90 -0
  40. package/commands/incident-response.md +292 -0
  41. package/commands/migrate.md +101 -0
  42. package/commands/performance.md +288 -0
  43. package/commands/refactor.md +105 -0
  44. package/commands/security-review.md +288 -0
  45. package/commands/tdd.md +183 -0
  46. package/commands/testing-strategy.md +265 -0
  47. package/dist/cli.d.ts +2 -0
  48. package/dist/cli.js +295 -0
  49. package/dist/core/auto-compound-runner.d.ts +12 -0
  50. package/dist/core/auto-compound-runner.js +460 -0
  51. package/dist/core/config-hooks.d.ts +10 -0
  52. package/dist/core/config-hooks.js +112 -0
  53. package/dist/core/config-injector.d.ts +50 -0
  54. package/dist/core/config-injector.js +455 -0
  55. package/dist/core/doctor.d.ts +1 -0
  56. package/dist/core/doctor.js +163 -0
  57. package/dist/core/errors.d.ts +81 -0
  58. package/dist/core/errors.js +133 -0
  59. package/dist/core/global-config.d.ts +43 -0
  60. package/dist/core/global-config.js +25 -0
  61. package/dist/core/harness.d.ts +24 -0
  62. package/dist/core/harness.js +621 -0
  63. package/dist/core/init.d.ts +7 -0
  64. package/dist/core/init.js +37 -0
  65. package/dist/core/inspect-cli.d.ts +7 -0
  66. package/dist/core/inspect-cli.js +47 -0
  67. package/dist/core/legacy-detector.d.ts +33 -0
  68. package/dist/core/legacy-detector.js +66 -0
  69. package/dist/core/logger.d.ts +34 -0
  70. package/dist/core/logger.js +121 -0
  71. package/dist/core/mcp-config.d.ts +44 -0
  72. package/dist/core/mcp-config.js +177 -0
  73. package/dist/core/notepad.d.ts +31 -0
  74. package/dist/core/notepad.js +88 -0
  75. package/dist/core/paths.d.ts +85 -0
  76. package/dist/core/paths.js +101 -0
  77. package/dist/core/plugin-detector.d.ts +44 -0
  78. package/dist/core/plugin-detector.js +226 -0
  79. package/dist/core/runtime-detector.d.ts +8 -0
  80. package/dist/core/runtime-detector.js +49 -0
  81. package/dist/core/scope-resolver.d.ts +8 -0
  82. package/dist/core/scope-resolver.js +45 -0
  83. package/dist/core/session-logger.d.ts +6 -0
  84. package/dist/core/session-logger.js +111 -0
  85. package/dist/core/session-store.d.ts +28 -0
  86. package/dist/core/session-store.js +218 -0
  87. package/dist/core/settings-lock.d.ts +18 -0
  88. package/dist/core/settings-lock.js +125 -0
  89. package/dist/core/spawn.d.ts +3 -0
  90. package/dist/core/spawn.js +135 -0
  91. package/dist/core/types.d.ts +108 -0
  92. package/dist/core/types.js +1 -0
  93. package/dist/core/uninstall.d.ts +4 -0
  94. package/dist/core/uninstall.js +307 -0
  95. package/dist/core/v1-bootstrap.d.ts +26 -0
  96. package/dist/core/v1-bootstrap.js +155 -0
  97. package/dist/engine/compound-cli.d.ts +24 -0
  98. package/dist/engine/compound-cli.js +250 -0
  99. package/dist/engine/compound-extractor.d.ts +68 -0
  100. package/dist/engine/compound-extractor.js +860 -0
  101. package/dist/engine/compound-lifecycle.d.ts +32 -0
  102. package/dist/engine/compound-lifecycle.js +305 -0
  103. package/dist/engine/compound-loop.d.ts +32 -0
  104. package/dist/engine/compound-loop.js +511 -0
  105. package/dist/engine/match-eval-log.d.ts +139 -0
  106. package/dist/engine/match-eval-log.js +270 -0
  107. package/dist/engine/phrase-blocklist.d.ts +119 -0
  108. package/dist/engine/phrase-blocklist.js +208 -0
  109. package/dist/engine/skill-promoter.d.ts +20 -0
  110. package/dist/engine/skill-promoter.js +115 -0
  111. package/dist/engine/solution-format.d.ts +160 -0
  112. package/dist/engine/solution-format.js +432 -0
  113. package/dist/engine/solution-index.d.ts +13 -0
  114. package/dist/engine/solution-index.js +252 -0
  115. package/dist/engine/solution-matcher.d.ts +364 -0
  116. package/dist/engine/solution-matcher.js +656 -0
  117. package/dist/engine/solution-writer.d.ts +76 -0
  118. package/dist/engine/solution-writer.js +157 -0
  119. package/dist/engine/term-matcher.d.ts +81 -0
  120. package/dist/engine/term-matcher.js +268 -0
  121. package/dist/engine/term-normalizer.d.ts +116 -0
  122. package/dist/engine/term-normalizer.js +171 -0
  123. package/dist/fgx.d.ts +6 -0
  124. package/dist/fgx.js +42 -0
  125. package/dist/forge/cli.d.ts +11 -0
  126. package/dist/forge/cli.js +100 -0
  127. package/dist/forge/evidence-processor.d.ts +21 -0
  128. package/dist/forge/evidence-processor.js +87 -0
  129. package/dist/forge/mismatch-detector.d.ts +44 -0
  130. package/dist/forge/mismatch-detector.js +83 -0
  131. package/dist/forge/onboarding-cli.d.ts +6 -0
  132. package/dist/forge/onboarding-cli.js +89 -0
  133. package/dist/forge/onboarding.d.ts +25 -0
  134. package/dist/forge/onboarding.js +122 -0
  135. package/dist/hooks/compound-reflection.d.ts +45 -0
  136. package/dist/hooks/compound-reflection.js +82 -0
  137. package/dist/hooks/context-guard.d.ts +24 -0
  138. package/dist/hooks/context-guard.js +156 -0
  139. package/dist/hooks/dangerous-patterns.json +18 -0
  140. package/dist/hooks/db-guard.d.ts +17 -0
  141. package/dist/hooks/db-guard.js +105 -0
  142. package/dist/hooks/hook-config.d.ts +29 -0
  143. package/dist/hooks/hook-config.js +92 -0
  144. package/dist/hooks/hook-registry.d.ts +43 -0
  145. package/dist/hooks/hook-registry.js +31 -0
  146. package/dist/hooks/hooks-generator.d.ts +49 -0
  147. package/dist/hooks/hooks-generator.js +99 -0
  148. package/dist/hooks/intent-classifier.d.ts +12 -0
  149. package/dist/hooks/intent-classifier.js +62 -0
  150. package/dist/hooks/keyword-detector.d.ts +25 -0
  151. package/dist/hooks/keyword-detector.js +389 -0
  152. package/dist/hooks/notepad-injector.d.ts +18 -0
  153. package/dist/hooks/notepad-injector.js +51 -0
  154. package/dist/hooks/permission-handler.d.ts +14 -0
  155. package/dist/hooks/permission-handler.js +114 -0
  156. package/dist/hooks/post-tool-failure.d.ts +11 -0
  157. package/dist/hooks/post-tool-failure.js +118 -0
  158. package/dist/hooks/post-tool-handlers.d.ts +17 -0
  159. package/dist/hooks/post-tool-handlers.js +115 -0
  160. package/dist/hooks/post-tool-use.d.ts +29 -0
  161. package/dist/hooks/post-tool-use.js +151 -0
  162. package/dist/hooks/pre-compact.d.ts +10 -0
  163. package/dist/hooks/pre-compact.js +165 -0
  164. package/dist/hooks/pre-tool-use.d.ts +31 -0
  165. package/dist/hooks/pre-tool-use.js +325 -0
  166. package/dist/hooks/prompt-injection-filter.d.ts +56 -0
  167. package/dist/hooks/prompt-injection-filter.js +287 -0
  168. package/dist/hooks/rate-limiter.d.ts +21 -0
  169. package/dist/hooks/rate-limiter.js +86 -0
  170. package/dist/hooks/secret-filter.d.ts +14 -0
  171. package/dist/hooks/secret-filter.js +65 -0
  172. package/dist/hooks/session-recovery.d.ts +27 -0
  173. package/dist/hooks/session-recovery.js +406 -0
  174. package/dist/hooks/shared/atomic-write.d.ts +41 -0
  175. package/dist/hooks/shared/atomic-write.js +148 -0
  176. package/dist/hooks/shared/context-budget.d.ts +37 -0
  177. package/dist/hooks/shared/context-budget.js +45 -0
  178. package/dist/hooks/shared/file-lock.d.ts +56 -0
  179. package/dist/hooks/shared/file-lock.js +253 -0
  180. package/dist/hooks/shared/hook-response.d.ts +33 -0
  181. package/dist/hooks/shared/hook-response.js +62 -0
  182. package/dist/hooks/shared/injection-caps.d.ts +39 -0
  183. package/dist/hooks/shared/injection-caps.js +52 -0
  184. package/dist/hooks/shared/plugin-signal.d.ts +23 -0
  185. package/dist/hooks/shared/plugin-signal.js +104 -0
  186. package/dist/hooks/shared/read-stdin.d.ts +8 -0
  187. package/dist/hooks/shared/read-stdin.js +63 -0
  188. package/dist/hooks/shared/sanitize-id.d.ts +7 -0
  189. package/dist/hooks/shared/sanitize-id.js +9 -0
  190. package/dist/hooks/shared/sanitize.d.ts +7 -0
  191. package/dist/hooks/shared/sanitize.js +22 -0
  192. package/dist/hooks/skill-injector.d.ts +38 -0
  193. package/dist/hooks/skill-injector.js +285 -0
  194. package/dist/hooks/slop-detector.d.ts +18 -0
  195. package/dist/hooks/slop-detector.js +93 -0
  196. package/dist/hooks/solution-injector.d.ts +58 -0
  197. package/dist/hooks/solution-injector.js +436 -0
  198. package/dist/hooks/subagent-tracker.d.ts +10 -0
  199. package/dist/hooks/subagent-tracker.js +90 -0
  200. package/dist/i18n/index.d.ts +43 -0
  201. package/dist/i18n/index.js +224 -0
  202. package/dist/lib.d.ts +14 -0
  203. package/dist/lib.js +14 -0
  204. package/dist/mcp/server.d.ts +8 -0
  205. package/dist/mcp/server.js +40 -0
  206. package/dist/mcp/solution-reader.d.ts +90 -0
  207. package/dist/mcp/solution-reader.js +273 -0
  208. package/dist/mcp/tools.d.ts +16 -0
  209. package/dist/mcp/tools.js +302 -0
  210. package/dist/preset/facet-catalog.d.ts +17 -0
  211. package/dist/preset/facet-catalog.js +46 -0
  212. package/dist/preset/preset-manager.d.ts +31 -0
  213. package/dist/preset/preset-manager.js +111 -0
  214. package/dist/renderer/inspect-renderer.d.ts +11 -0
  215. package/dist/renderer/inspect-renderer.js +123 -0
  216. package/dist/renderer/rule-renderer.d.ts +18 -0
  217. package/dist/renderer/rule-renderer.js +159 -0
  218. package/dist/store/evidence-store.d.ts +23 -0
  219. package/dist/store/evidence-store.js +58 -0
  220. package/dist/store/profile-store.d.ts +12 -0
  221. package/dist/store/profile-store.js +53 -0
  222. package/dist/store/recommendation-store.d.ts +22 -0
  223. package/dist/store/recommendation-store.js +64 -0
  224. package/dist/store/rule-store.d.ts +22 -0
  225. package/dist/store/rule-store.js +62 -0
  226. package/dist/store/session-state-store.d.ts +11 -0
  227. package/dist/store/session-state-store.js +44 -0
  228. package/dist/store/types.d.ts +159 -0
  229. package/dist/store/types.js +7 -0
  230. package/hooks/hook-registry.json +21 -0
  231. package/hooks/hooks.json +185 -0
  232. package/package.json +89 -0
  233. package/plugin.json +20 -0
  234. package/scripts/postinstall.js +826 -0
  235. package/skills/api-design/SKILL.md +262 -0
  236. package/skills/architecture-decision/SKILL.md +309 -0
  237. package/skills/ci-cd/SKILL.md +264 -0
  238. package/skills/code-review/SKILL.md +228 -0
  239. package/skills/compound/SKILL.md +101 -0
  240. package/skills/database/SKILL.md +257 -0
  241. package/skills/debug-detective/SKILL.md +95 -0
  242. package/skills/docker/SKILL.md +268 -0
  243. package/skills/documentation/SKILL.md +270 -0
  244. package/skills/ecomode/SKILL.md +46 -0
  245. package/skills/frontend/SKILL.md +265 -0
  246. package/skills/git-master/SKILL.md +86 -0
  247. package/skills/incident-response/SKILL.md +286 -0
  248. package/skills/migrate/SKILL.md +96 -0
  249. package/skills/performance/SKILL.md +282 -0
  250. package/skills/refactor/SKILL.md +100 -0
  251. package/skills/security-review/SKILL.md +282 -0
  252. package/skills/tdd/SKILL.md +178 -0
  253. package/skills/testing-strategy/SKILL.md +260 -0
  254. package/starter-pack/solutions/starter-api-error-responses.md +37 -0
  255. package/starter-pack/solutions/starter-async-patterns.md +40 -0
  256. package/starter-pack/solutions/starter-caching-strategy.md +40 -0
  257. package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
  258. package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
  259. package/starter-pack/solutions/starter-dependency-injection.md +40 -0
  260. package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
  261. package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
  262. package/starter-pack/solutions/starter-input-validation.md +40 -0
  263. package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
  264. package/starter-pack/solutions/starter-refactor-safely.md +38 -0
  265. package/starter-pack/solutions/starter-secret-management.md +37 -0
  266. package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
  267. package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
  268. 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
+ }