@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,511 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { ME_SOLUTIONS, ME_RULES } from '../core/paths.js';
4
+ import { resolveScope } from '../core/scope-resolver.js';
5
+ import { serializeSolutionV3, extractTags, DEFAULT_EVIDENCE, slugify } from './solution-format.js';
6
+ /** 키워드 기반으로 인사이트를 개인/팀으로 자동 분류 */
7
+ export function classifyInsight(title, content) {
8
+ const teamKeywords = [
9
+ 'API', 'DB', 'database', 'migration', 'schema', 'deploy', 'CI', 'CD',
10
+ 'security', 'auth', 'permission', 'error handling', 'logging', 'monitoring',
11
+ 'convention', 'standard', 'guideline', 'rule', 'pattern', 'architecture',
12
+ 'naming', 'structure', 'review', 'test strategy', 'documentation',
13
+ '에러 처리', '네이밍', '규칙', '규약', '표준', '패턴', '보안', '인증',
14
+ '배포', '마이그레이션', '아키텍처', '로깅', '모니터링', '구조',
15
+ ];
16
+ const personalKeywords = [
17
+ 'shortcut', 'preference', 'my style', 'editor', 'workflow tip',
18
+ 'vim', 'vscode', 'alias', 'snippet', 'dotfile',
19
+ '단축키', '내 스타일', '편의', '습관',
20
+ ];
21
+ const text = `${title} ${content}`.toLowerCase();
22
+ const teamScore = teamKeywords.filter(kw => text.includes(kw.toLowerCase())).length;
23
+ const personalScore = personalKeywords.filter(kw => text.includes(kw.toLowerCase())).length;
24
+ if (teamScore > personalScore) {
25
+ return { classification: 'team', reason: `team pattern (${teamScore} keyword matches)` };
26
+ }
27
+ if (personalScore > teamScore) {
28
+ return { classification: 'personal', reason: `personal style (${personalScore} keyword matches)` };
29
+ }
30
+ return { classification: 'personal', reason: 'default (personal)' };
31
+ }
32
+ /**
33
+ * Compound Loop — 이미 추출된 인사이트를 저장
34
+ */
35
+ export async function runCompoundLoop(cwd, insights) {
36
+ const saved = [];
37
+ const skipped = [];
38
+ const scope = resolveScope(cwd);
39
+ for (const insight of insights) {
40
+ try {
41
+ const destPath = getDestPath(insight, scope.team?.name);
42
+ if (!destPath) {
43
+ skipped.push(`${insight.title}: cannot determine save path`);
44
+ continue;
45
+ }
46
+ // 중복 체크
47
+ if (fs.existsSync(destPath)) {
48
+ skipped.push(`${insight.title}: already exists`);
49
+ continue;
50
+ }
51
+ // 디렉토리 생성
52
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
53
+ // 파일 저장
54
+ const fileContent = formatInsight(insight);
55
+ fs.writeFileSync(destPath, fileContent);
56
+ saved.push(`${insight.scope}/${insight.type}: ${insight.title}`);
57
+ }
58
+ catch (err) {
59
+ skipped.push(`${insight.title}: ${err.message}`);
60
+ }
61
+ }
62
+ return { saved, skipped };
63
+ }
64
+ function getDestPath(insight, _teamPackName) {
65
+ const fileName = `${slugify(insight.title)}.md`;
66
+ if (insight.scope === 'me') {
67
+ const dir = insight.type === 'rule' || insight.type === 'convention'
68
+ ? ME_RULES
69
+ : ME_SOLUTIONS;
70
+ return path.join(dir, fileName);
71
+ }
72
+ // v1: 팀 scope 제거 — 모든 인사이트를 개인으로 저장
73
+ const dir = insight.type === 'rule' || insight.type === 'convention'
74
+ ? ME_RULES
75
+ : ME_SOLUTIONS;
76
+ return path.join(dir, fileName);
77
+ }
78
+ /** Map v1 CompoundInsight type to v3 SolutionType */
79
+ function mapInsightType(type) {
80
+ switch (type) {
81
+ case 'solution': return 'pattern';
82
+ case 'pattern': return 'pattern';
83
+ case 'rule': return 'decision';
84
+ case 'convention': return 'decision';
85
+ default: return 'pattern';
86
+ }
87
+ }
88
+ /** Infer identifiers from title and content for Code Reflection matching */
89
+ function inferIdentifiers(title, content) {
90
+ const text = `${title} ${content}`;
91
+ // Extract PascalCase words (likely class/component names)
92
+ const pascalCase = text.match(/\b[A-Z][a-zA-Z0-9]{3,}\b/g) ?? [];
93
+ // Extract camelCase words starting with lowercase (likely function names)
94
+ const camelCase = text.match(/\b[a-z][a-zA-Z0-9]{3,}(?=[A-Z])\w*/g) ?? [];
95
+ // Extract quoted strings that look like identifiers
96
+ const quoted = text.match(/['"`]([a-zA-Z][a-zA-Z0-9-]{3,})['"`]/g)?.map(s => s.slice(1, -1)) ?? [];
97
+ const all = [...new Set([...pascalCase, ...camelCase, ...quoted])]
98
+ .filter(id => id.length >= 4 && id.length <= 50);
99
+ return all.slice(0, 10); // max 10 identifiers
100
+ }
101
+ function formatInsight(insight) {
102
+ const today = new Date().toISOString().split('T')[0];
103
+ const solution = {
104
+ frontmatter: {
105
+ name: slugify(insight.title),
106
+ version: 1,
107
+ status: 'candidate',
108
+ confidence: 0.5,
109
+ type: mapInsightType(insight.type),
110
+ scope: insight.scope,
111
+ tags: extractTags(`${insight.title} ${insight.content}`),
112
+ identifiers: inferIdentifiers(insight.title, insight.content),
113
+ evidence: { ...DEFAULT_EVIDENCE },
114
+ created: today,
115
+ updated: today,
116
+ supersedes: null,
117
+ extractedBy: insight.source === 'manual' ? 'manual' : 'auto',
118
+ },
119
+ context: '',
120
+ content: insight.content,
121
+ };
122
+ return serializeSolutionV3(solution);
123
+ }
124
+ // slugify is imported from solution-format.ts (single source of truth)
125
+ /** 팀 제안으로 저장 (.compound/proposals/) */
126
+ export function saveTeamProposals(insights, cwd) {
127
+ const proposalsDir = path.join(cwd, '.compound', 'proposals');
128
+ fs.mkdirSync(proposalsDir, { recursive: true });
129
+ const date = new Date().toISOString().split('T')[0];
130
+ const filename = `${date}-${Date.now()}.json`;
131
+ fs.writeFileSync(path.join(proposalsDir, filename), JSON.stringify(insights, null, 2));
132
+ }
133
+ /** .compound/proposals/ 에서 제안 파일 로드 */
134
+ export function loadProposals(proposalsDir) {
135
+ if (!fs.existsSync(proposalsDir))
136
+ return [];
137
+ const files = fs.readdirSync(proposalsDir).filter(f => f.endsWith('.json'));
138
+ const all = [];
139
+ for (const file of files) {
140
+ try {
141
+ const content = fs.readFileSync(path.join(proposalsDir, file), 'utf-8');
142
+ const parsed = JSON.parse(content);
143
+ if (Array.isArray(parsed)) {
144
+ all.push(...parsed);
145
+ }
146
+ }
147
+ catch {
148
+ // skip malformed files
149
+ }
150
+ }
151
+ return all;
152
+ }
153
+ /** 제안 파일 정리 */
154
+ export function cleanProposals(proposalsDir) {
155
+ if (!fs.existsSync(proposalsDir))
156
+ return;
157
+ const files = fs.readdirSync(proposalsDir).filter(f => f.endsWith('.json'));
158
+ for (const file of files) {
159
+ fs.unlinkSync(path.join(proposalsDir, file));
160
+ }
161
+ }
162
+ /** CLI 핸들러: forgen compound */
163
+ export async function handleCompound(args) {
164
+ const cwd = process.cwd();
165
+ const scope = resolveScope(cwd);
166
+ // --help 처리
167
+ if (args.includes('--help') || args.includes('-h')) {
168
+ console.log(`
169
+ Usage: forgen compound [options]
170
+
171
+ Default:
172
+ forgen compound Preview auto analysis from recent session/code changes
173
+ forgen compound --save Persist previewed insights
174
+
175
+ Manual add:
176
+ forgen compound --solution "title" "content"
177
+ forgen compound --rule "title" "content"
178
+ forgen compound --convention "title" "content"
179
+ forgen compound --to team Save to team scope
180
+
181
+ Inspect & manage:
182
+ forgen compound list List saved entries (solutions and rules)
183
+ forgen compound inspect <name> Show saved entry details
184
+ forgen compound remove <name> Remove a saved entry
185
+ forgen compound clean-stale Retire solutions from removed extractors (C4 cleanup)
186
+ forgen compound rollback --since 2026-03-20
187
+ Rollback unused auto-extracted solutions since date
188
+
189
+ Lifecycle:
190
+ forgen compound --lifecycle Run promotion/demotion/circuit-breaker check
191
+ forgen compound --verify <name> Manually promote solution to verified
192
+
193
+ Auto-extraction:
194
+ forgen compound --pause-auto Pause auto-extraction
195
+ forgen compound --resume-auto Resume auto-extraction
196
+
197
+ Interactive:
198
+ forgen compound interactive
199
+ `);
200
+ return;
201
+ }
202
+ // --pause-auto / --resume-auto
203
+ if (args.includes('--pause-auto') || args.includes('pause-auto')) {
204
+ const { pauseExtraction } = await import('./compound-extractor.js');
205
+ pauseExtraction();
206
+ console.log(' 자동 추출이 중단되었습니다. resume-auto로 재개할 수 있습니다.\n');
207
+ return;
208
+ }
209
+ if (args.includes('--resume-auto') || args.includes('resume-auto')) {
210
+ const { resumeExtraction } = await import('./compound-extractor.js');
211
+ resumeExtraction();
212
+ console.log(' 자동 추출이 재개되었습니다.\n');
213
+ return;
214
+ }
215
+ // --- lifecycle command ---
216
+ if (args.includes('--lifecycle') || args.includes('lifecycle')) {
217
+ const { runLifecycleCheck } = await import('./compound-lifecycle.js');
218
+ const result = runLifecycleCheck();
219
+ console.log('\n Compound Lifecycle Check\n');
220
+ if (result.promoted.length) {
221
+ console.log(' Promoted:');
222
+ for (const p of result.promoted)
223
+ console.log(` ↑ ${p}`);
224
+ }
225
+ if (result.demoted.length) {
226
+ console.log(' Demoted:');
227
+ for (const d of result.demoted)
228
+ console.log(` ↓ ${d}`);
229
+ }
230
+ if (result.retired.length) {
231
+ console.log(' Retired:');
232
+ for (const r of result.retired)
233
+ console.log(` ✗ ${r}`);
234
+ }
235
+ if (result.contradictions.length) {
236
+ console.log(' Contradictions:');
237
+ for (const c of result.contradictions)
238
+ console.log(` ⚠ ${c}`);
239
+ }
240
+ if (!result.promoted.length && !result.demoted.length && !result.retired.length && !result.contradictions.length) {
241
+ console.log(' No lifecycle changes needed.\n');
242
+ }
243
+ console.log();
244
+ return;
245
+ }
246
+ // --- verify command ---
247
+ if (args.includes('--verify')) {
248
+ const nameIdx = args.indexOf('--verify') + 1;
249
+ const name = args[nameIdx];
250
+ if (!name || name.startsWith('--')) {
251
+ console.log(' Usage: forgen compound --verify <solution-name>\n');
252
+ return;
253
+ }
254
+ const { verifySolution } = await import('./compound-lifecycle.js');
255
+ if (verifySolution(name)) {
256
+ console.log(` ✓ "${name}" verified 상태로 승격됨\n`);
257
+ }
258
+ else {
259
+ console.log(` ✗ "${name}" 솔루션을 찾을 수 없거나 업데이트 실패\n`);
260
+ }
261
+ return;
262
+ }
263
+ // --- list command ---
264
+ if (args.includes('list') || args.includes('--list')) {
265
+ const { listSolutions } = await import('./compound-cli.js');
266
+ listSolutions();
267
+ return;
268
+ }
269
+ // --- inspect command ---
270
+ if (args.includes('inspect') || args.includes('--inspect')) {
271
+ const nameIdx = Math.max(args.indexOf('inspect'), args.indexOf('--inspect')) + 1;
272
+ const name = args[nameIdx];
273
+ if (!name || name.startsWith('--')) {
274
+ console.log(' Usage: forgen compound inspect <solution-name>\n');
275
+ return;
276
+ }
277
+ const { inspectSolution } = await import('./compound-cli.js');
278
+ inspectSolution(name);
279
+ return;
280
+ }
281
+ // --- clean-stale command (M-3 migration) ---
282
+ if (args.includes('clean-stale') || args.includes('--clean-stale')) {
283
+ const { cleanStaleSolutions } = await import('./compound-cli.js');
284
+ cleanStaleSolutions();
285
+ return;
286
+ }
287
+ // --- remove command ---
288
+ if (args.includes('remove') || args.includes('--remove')) {
289
+ const nameIdx = Math.max(args.indexOf('remove'), args.indexOf('--remove')) + 1;
290
+ const name = args[nameIdx];
291
+ if (!name || name.startsWith('--')) {
292
+ console.log(' Usage: forgen compound remove <solution-name>\n');
293
+ return;
294
+ }
295
+ const { removeSolution } = await import('./compound-cli.js');
296
+ removeSolution(name);
297
+ return;
298
+ }
299
+ // --- retag command ---
300
+ if (args.includes('retag') || args.includes('--retag')) {
301
+ const { retagSolutions } = await import('./compound-cli.js');
302
+ retagSolutions();
303
+ return;
304
+ }
305
+ // --- rollback command ---
306
+ if (args.includes('rollback') || args.includes('--rollback')) {
307
+ const sinceIdx = args.indexOf('--since');
308
+ const since = sinceIdx !== -1 ? args[sinceIdx + 1] : undefined;
309
+ if (!since) {
310
+ console.log(' Usage: forgen compound rollback --since 2026-03-20\n');
311
+ return;
312
+ }
313
+ const { rollbackSolutions } = await import('./compound-cli.js');
314
+ rollbackSolutions(since);
315
+ return;
316
+ }
317
+ // --- explicit interactive command ---
318
+ if (args.includes('interactive') || args.includes('--interactive')) {
319
+ await interactiveCompound(cwd, scope);
320
+ return;
321
+ }
322
+ // --- preview-first default mode ---
323
+ if (args.length === 0) {
324
+ const { previewExtraction } = await import('./compound-extractor.js');
325
+ const result = await previewExtraction(cwd);
326
+ console.log('\n Compound Preview\n');
327
+ console.log(` Scope: ${scope.summary}`);
328
+ console.log();
329
+ if (result.preview.length === 0) {
330
+ console.log(` No auto-analysis preview available${result.reason ? `: ${result.reason}` : '.'}`);
331
+ console.log(' Run `forgen compound --save` after meaningful code changes, or `forgen compound interactive` for manual capture.\n');
332
+ return;
333
+ }
334
+ console.log(' Preview only — nothing was saved.\n');
335
+ for (const [index, insight] of result.preview.entries()) {
336
+ console.log(` ${index + 1}. [${insight.type}] ${insight.name}`);
337
+ console.log(` ${insight.content.split('\n')[0]}`);
338
+ }
339
+ if (result.skipped.length > 0) {
340
+ console.log('\n Skipped:');
341
+ for (const entry of result.skipped.slice(0, 5)) {
342
+ console.log(` - ${entry}`);
343
+ }
344
+ }
345
+ console.log('\n Run `forgen compound --save` to persist this preview.\n');
346
+ return;
347
+ }
348
+ // --- auto save mode ---
349
+ if (args.includes('--save')) {
350
+ const { runExtraction } = await import('./compound-extractor.js');
351
+ const sessionId = `compound-cli-${Date.now()}`;
352
+ const result = await runExtraction(cwd, sessionId);
353
+ console.log('\n Compound Save\n');
354
+ console.log(` Scope: ${scope.summary}`);
355
+ console.log();
356
+ if (result.extracted.length === 0 && result.skipped.length === 0) {
357
+ console.log(` No insights saved${result.reason ? `: ${result.reason}` : '.'}\n`);
358
+ return;
359
+ }
360
+ for (const saved of result.extracted) {
361
+ console.log(` ✓ Saved: ${saved}`);
362
+ }
363
+ for (const skipped of result.skipped) {
364
+ console.log(` ─ Skipped: ${skipped}`);
365
+ }
366
+ if (result.reason) {
367
+ console.log(` Reason: ${result.reason}`);
368
+ }
369
+ console.log();
370
+ return;
371
+ }
372
+ // 인자가 없거나 알 수 없는 플래그만 있으면 수동 추가/interactive가 아닌 것으로 간주
373
+ const knownFlags = [
374
+ '--solution', '--rule', '--convention', '--pattern', '--to', '--pause-auto', '--resume-auto',
375
+ '--lifecycle', '--verify', '--save', '--interactive',
376
+ 'list', 'inspect', 'remove', 'rollback', 'retag', 'lifecycle',
377
+ '--list', '--inspect', '--remove', '--rollback', '--retag', '--since', 'interactive',
378
+ ];
379
+ const hasTypeFlag = knownFlags.some(f => args.includes(f));
380
+ if (!hasTypeFlag) {
381
+ console.log(' Unknown compound arguments. Run `forgen compound --help` for usage.\n');
382
+ return;
383
+ }
384
+ console.log('\n Compound Loop — Accumulating insights\n');
385
+ console.log(` Scope: ${scope.summary}`);
386
+ console.log();
387
+ // 수동 인사이트 추가
388
+ const type = args.includes('--solution') ? 'solution'
389
+ : args.includes('--rule') ? 'rule'
390
+ : args.includes('--convention') ? 'convention'
391
+ : 'pattern';
392
+ const scopeTarget = args.includes('--to')
393
+ ? (args[args.indexOf('--to') + 1] === 'team' ? 'team' : 'me')
394
+ : 'me';
395
+ // --solution/--rule 다음 인자들이 제목과 내용 (-- 접두사 인자 필터)
396
+ const typeFlag = `--${type}`;
397
+ const flagIdx = args.indexOf(typeFlag);
398
+ const positionalArgs = args.slice(flagIdx + 1).filter(a => !a.startsWith('--'));
399
+ const title = positionalArgs[0];
400
+ const content = positionalArgs.slice(1).join(' ');
401
+ if (!title) {
402
+ console.log(' A title is required.');
403
+ console.log(' Usage: forgen compound --solution "title" "content"');
404
+ return;
405
+ }
406
+ const { classification, reason } = classifyInsight(title, content || title);
407
+ const insight = {
408
+ id: `c-${Date.now()}`,
409
+ type,
410
+ title,
411
+ content: content || title,
412
+ scope: scopeTarget,
413
+ classification,
414
+ reason,
415
+ source: 'manual',
416
+ };
417
+ const result = await runCompoundLoop(cwd, [insight]);
418
+ for (const s of result.saved) {
419
+ console.log(` ✓ Saved: ${s}`);
420
+ }
421
+ for (const s of result.skipped) {
422
+ console.log(` ─ Skipped: ${s}`);
423
+ }
424
+ console.log();
425
+ }
426
+ async function interactiveCompound(cwd, scope) {
427
+ console.log("\n Forgen Compound — Today's insights\n");
428
+ console.log(` Scope: ${scope.summary}`);
429
+ console.log();
430
+ // Non-interactive mode: 대화 없이 안내만 출력
431
+ if (!process.stdin.isTTY) {
432
+ console.log(' Non-interactive environment. Add insights via manual mode.\n');
433
+ console.log(' Usage:');
434
+ console.log(' forgen compound --solution "title" "content"');
435
+ console.log(' forgen compound --rule "title" "content"');
436
+ console.log(' forgen compound --convention "title" "content"');
437
+ console.log(' forgen compound --to team Save to team scope\n');
438
+ console.log(' Interactive mode: run `forgen compound interactive` in a TTY environment\n');
439
+ return;
440
+ }
441
+ const readline = await import('node:readline');
442
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
443
+ const prompt = (q) => new Promise(resolve => rl.question(q, resolve));
444
+ const insights = [];
445
+ console.log(' Enter insights. Press enter on empty line to finish.\n');
446
+ let idx = 1;
447
+ while (true) {
448
+ const title = await prompt(` [${idx}] Title (empty=quit): `);
449
+ if (!title.trim())
450
+ break;
451
+ const content = await prompt(' Content: ');
452
+ const typeChoiceStr = await prompt(' Type (1=solution 2=rule 3=convention 4=pattern) [1]: ');
453
+ const typeMap = { '1': 'solution', '2': 'rule', '3': 'convention', '4': 'pattern' };
454
+ const insightType = typeMap[typeChoiceStr.trim()] ?? 'solution';
455
+ const { classification, reason } = classifyInsight(title, content);
456
+ const insight = {
457
+ id: `c-${Date.now()}-${idx}`,
458
+ type: insightType,
459
+ title: title.trim(),
460
+ content: content.trim(),
461
+ classification,
462
+ reason,
463
+ scope: classification === 'team' ? 'team' : 'me',
464
+ source: 'manual',
465
+ };
466
+ insights.push(insight);
467
+ const icon = classification === 'team' ? '👥' : '👤';
468
+ console.log(` → ${icon} ${classification} (${reason})\n`);
469
+ idx++;
470
+ }
471
+ if (insights.length === 0) {
472
+ console.log(' No insights.\n');
473
+ rl.close();
474
+ return;
475
+ }
476
+ // Show summary and let user adjust
477
+ console.log('\n ── Classification results ──\n');
478
+ for (let i = 0; i < insights.length; i++) {
479
+ const ins = insights[i];
480
+ const icon = ins.classification === 'team' ? '👥 Team' : '👤 Personal';
481
+ console.log(` ${i + 1}. [${icon}] ${ins.title}`);
482
+ }
483
+ console.log('\n Enter number to toggle classification (e.g. 2=team→personal), enter to confirm');
484
+ const changes = await prompt(' > ');
485
+ if (changes.trim()) {
486
+ for (const num of changes.split(/[,\s]+/)) {
487
+ const changeIdx = parseInt(num, 10) - 1;
488
+ if (changeIdx >= 0 && changeIdx < insights.length) {
489
+ insights[changeIdx].classification = insights[changeIdx].classification === 'team' ? 'personal' : 'team';
490
+ insights[changeIdx].scope = insights[changeIdx].classification === 'team' ? 'team' : 'me';
491
+ }
492
+ }
493
+ }
494
+ // Save — runCompoundLoop을 통해 타입별 올바른 경로에 저장
495
+ const personal = insights.filter(i => i.classification === 'personal');
496
+ const team = insights.filter(i => i.classification === 'team');
497
+ if (personal.length > 0) {
498
+ const result = await runCompoundLoop(cwd, personal);
499
+ for (const s of result.saved)
500
+ console.log(`\n ✓ Saved: ${s}`);
501
+ for (const s of result.skipped)
502
+ console.log(` ─ Skipped: ${s}`);
503
+ }
504
+ // Save team to .compound/proposals/ (for later propose)
505
+ if (team.length > 0) {
506
+ saveTeamProposals(team, cwd);
507
+ console.log(` ✓ ${team.length} team rule candidate(s) saved (.compound/proposals/)`);
508
+ console.log(' → Run forgen propose to share with the team.\n');
509
+ }
510
+ rl.close();
511
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Match eval log — JSONL ranking-decision writer (T3 of the Round 3 plan).
3
+ *
4
+ * Why this module exists:
5
+ * The bootstrap evaluator (`evaluateSolutionMatcher`) measures matcher
6
+ * quality against a labeled fixture, but production traffic is open-ended.
7
+ * T2 hoisted query normalization out of the per-solution loop, which is
8
+ * fast, but it also hid the "what did we actually rank, and why?" signal
9
+ * from offline review. This module appends a single JSONL line per matcher
10
+ * call capturing the normalized query, the top candidates with their
11
+ * matched terms, and which ones the caller ultimately surfaced.
12
+ *
13
+ * The target consumer is offline analysis: a reviewer can tail or grep
14
+ * the file to spot systematic recall misses or spurious matches without
15
+ * instrumenting production.
16
+ *
17
+ * Privacy posture (T3 security review fix):
18
+ * The raw user prompt is NEVER written to disk. Instead, we store a
19
+ * short SHA-256 prefix (`rawQueryHash`) plus character length
20
+ * (`rawQueryLen`). This keeps dedup and "was the prompt substantial"
21
+ * signals available for offline analysis while eliminating the PII /
22
+ * API-key / credential leakage risk of persisting raw prompts in
23
+ * `~/.forgen/state/match-eval-log.jsonl`. The `normalizedQuery` array
24
+ * already carries the matching-signal payload and is safe to persist
25
+ * because it only contains short tag tokens (never the full prompt).
26
+ *
27
+ * Operational principles:
28
+ * 1. **Off the critical path.** Never throw; never block. A failed write
29
+ * is silently swallowed — the hook must continue to return its
30
+ * solutions even if the log is misconfigured, read-only, or full.
31
+ * 2. **Bounded record size.** Candidates are capped at 5 (the matcher's
32
+ * own top-5 cap). `normalizedQuery` is capped at 64 terms. Each
33
+ * candidate's `matchedTerms` is capped at 16. Worst-case record ≈
34
+ * 2KB, which stays under Linux PIPE_BUF=4096 for safe concurrent
35
+ * appends on local filesystems.
36
+ * 3. **Symlink defense.** `fs.openSync` with `O_NOFOLLOW` refuses to
37
+ * follow a symlink at the log path. Without this guard, an attacker
38
+ * with write access to `~/.forgen/state/` could redirect appends to
39
+ * `~/.ssh/authorized_keys`, `~/.bashrc`, or other sensitive files.
40
+ * 4. **File-lock for concurrency.** Uses `withFileLockSync` to serialize
41
+ * concurrent writers. macOS PIPE_BUF=512 is smaller than the worst-
42
+ * case record size so POSIX atomic append alone isn't enough.
43
+ * 5. **Opt-out via env, fail-closed on invalid config.**
44
+ * `FORGEN_MATCH_EVAL_LOG=off|disabled|0|false|no` disables entirely.
45
+ * `FORGEN_MATCH_EVAL_LOG_SAMPLE=<float 0..1>` samples. An invalid
46
+ * sample value (NaN, out of range, whitespace) falls back to 0
47
+ * (skip) rather than 1 (log everything) — fail-closed for privacy.
48
+ * 6. **File size cap.** `readMatchEvalLog` refuses to parse files
49
+ * larger than 50 MB to prevent OOM in the offline analyzer. Callers
50
+ * are responsible for rotating the log externally.
51
+ */
52
+ /** Environment variable controlling log enable/disable. */
53
+ export declare const MATCH_EVAL_LOG_ENV = "FORGEN_MATCH_EVAL_LOG";
54
+ /** Environment variable controlling sample rate (0.0 – 1.0). */
55
+ export declare const MATCH_EVAL_LOG_SAMPLE_ENV = "FORGEN_MATCH_EVAL_LOG_SAMPLE";
56
+ /**
57
+ * Single ranking decision captured at matcher call time.
58
+ *
59
+ * Rationale for each field:
60
+ * - `source`: distinguishes the hook path (`solution-injector`) from the
61
+ * MCP path (`solution-reader.searchSolutions`). They have different
62
+ * query shapes and the log should support filtering by origin.
63
+ * - `rawQueryHash`: first 16 hex chars of SHA-256 over the user prompt.
64
+ * Enables dedup ("this query shape recurred") without persisting the
65
+ * prompt text. NOT cryptographically reversible — only useful for
66
+ * grouping identical queries in offline analysis.
67
+ * - `rawQueryLen`: character count of the original prompt. A rough
68
+ * "was this a substantial query?" signal that helps triage.
69
+ * - `normalizedQuery`: the output of `defaultNormalizer.normalizeTerms`
70
+ * over `extractTags(rawQuery)`. This is what actually drove matching,
71
+ * so it's the most important piece for debugging ranking surprises.
72
+ * Only short tag tokens — safe to persist.
73
+ * - `candidates`: top-N ranked solutions with relevance and matched
74
+ * terms. Bounded by `MAX_CANDIDATES_LOGGED`.
75
+ * - `rankedTopN`: the names of the top-N solutions the CALLER RECEIVED
76
+ * from the matcher at the time of logging. This is the pre-filter top
77
+ * (hook path) or post-`limit` top (MCP path). Caller-side budget /
78
+ * experiment / disjoint filtering happens AFTER logging and is not
79
+ * captured here — by design, this field records what the matcher
80
+ * returned, not what the hook ultimately injected.
81
+ * - `ts`: ISO 8601 timestamp. Always set by the logger, never by the
82
+ * caller — prevents clock injection from polluting the log.
83
+ */
84
+ export interface MatchEvalLogRecord {
85
+ source: 'hook' | 'mcp';
86
+ rawQueryHash: string;
87
+ rawQueryLen: number;
88
+ normalizedQuery: string[];
89
+ candidates: Array<{
90
+ name: string;
91
+ relevance: number;
92
+ matchedTerms: string[];
93
+ }>;
94
+ rankedTopN: string[];
95
+ ts: string;
96
+ }
97
+ /**
98
+ * Caller payload. `ts` and `rawQueryHash`/`rawQueryLen` are derived by
99
+ * the logger from the caller-supplied `rawQuery`. `rawQuery` itself is
100
+ * consumed in-process only and never written to disk.
101
+ */
102
+ export interface MatchEvalLogInput {
103
+ source: 'hook' | 'mcp';
104
+ /** Raw user prompt. Hashed + length-captured, never persisted. */
105
+ rawQuery: string;
106
+ normalizedQuery: string[];
107
+ candidates: Array<{
108
+ name: string;
109
+ relevance: number;
110
+ matchedTerms: string[];
111
+ }>;
112
+ /**
113
+ * Top-N by relevance that the matcher returned to the caller at log
114
+ * time. See `MatchEvalLogRecord.rankedTopN` for semantics — this is
115
+ * NOT the post-filter "actually injected" set.
116
+ */
117
+ rankedTopN: string[];
118
+ }
119
+ /**
120
+ * Append a single ranking decision to the match-eval-log JSONL file.
121
+ *
122
+ * Fail-open: any error is caught and debug-logged. Callers can invoke
123
+ * this without guarding — the logger will never bubble an exception into
124
+ * the hook critical path.
125
+ */
126
+ export declare function logMatchDecision(input: MatchEvalLogInput): void;
127
+ /**
128
+ * Read all records from the match-eval-log file. Intended for tests and
129
+ * offline analysis tools; NOT for hot-path use.
130
+ *
131
+ * Malformed lines (non-JSON, missing required fields, wrong shape) are
132
+ * silently skipped — preserves the debug value of the rest of the file
133
+ * if one entry gets corrupted by a partial write or tool error.
134
+ *
135
+ * DoS guard: refuses to read files larger than `MAX_LOG_FILE_SIZE_BYTES`
136
+ * to prevent OOM when a long-running log grows unbounded. Returns [] in
137
+ * that case and debug-logs the skip.
138
+ */
139
+ export declare function readMatchEvalLog(): MatchEvalLogRecord[];