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