@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,148 @@
1
+ /**
2
+ * 훅 공유 유틸: 원자적 파일 쓰기
3
+ *
4
+ * write → rename 패턴으로 동시 세션에서의 상태 파일 손상을 방지합니다.
5
+ *
6
+ * 보안 모델:
7
+ * - mode 옵션이 0o600인 파일은 같은 호스트의 다른 user로부터 보호.
8
+ * - tmp 파일은 PID + randomBytes(6) suffix → 같은 process가 동시 atomic write를
9
+ * 하더라도 tmp 충돌 없음. symlink TOCTOU도 방지.
10
+ *
11
+ * Windows 한계:
12
+ * - fchmodSync/chmodSync는 Windows에서 read-only 비트만 영향. 0o600 같은
13
+ * POSIX 권한은 의미가 없으며, 보안은 OS-level ACL과 사용자 home 격리에 의존.
14
+ * - 민감 데이터는 Windows에서 추가 보호가 필요할 수 있다.
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import { randomBytes } from 'node:crypto';
19
+ import { STATE_DIR } from '../../core/paths.js';
20
+ // L1 hardening: O_NOFOLLOW로 symlink 공격 차단 (POSIX only).
21
+ // Windows에는 정의되지 않으므로 0으로 fallback (no-op flag).
22
+ const O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;
23
+ /**
24
+ * 부모 디렉터리 생성. 민감한 cache 디렉터리는 0o700으로 강제.
25
+ *
26
+ * H13 fix (security review): caller가 dirMode를 명시 안 해도, STATE_DIR
27
+ * 하위 경로는 자동으로 0o700으로 강제한다. 50+ caller가 dirMode를 매번
28
+ * 명시할 필요 없이 STATE_DIR 권한 보장.
29
+ *
30
+ * 호출자가 명시한 mode가 있으면 그 값을 우선한다.
31
+ */
32
+ function ensureDir(dir, mode) {
33
+ // STATE_DIR 자동 감지: caller가 mode 미지정 시에만 0o700 default.
34
+ // STATE_DIR이 undefined인 환경(테스트 mock 등)에선 자동 감지를 skip한다.
35
+ const isStateDir = typeof STATE_DIR === 'string'
36
+ && (dir === STATE_DIR || dir.startsWith(STATE_DIR + path.sep));
37
+ const effectiveMode = mode ?? (isStateDir ? 0o700 : undefined);
38
+ fs.mkdirSync(dir, { recursive: true, ...(effectiveMode !== undefined ? { mode: effectiveMode } : {}) });
39
+ if (effectiveMode !== undefined && process.platform !== 'win32') {
40
+ // mkdirSync mode는 umask 영향을 받으므로 chmod로 강제 (POSIX only)
41
+ try {
42
+ fs.chmodSync(dir, effectiveMode);
43
+ }
44
+ catch { /* non-fatal — ACL 환경 등 */ }
45
+ }
46
+ }
47
+ /**
48
+ * tmp 파일 경로 생성. PID + random suffix로 충돌 방지 (M17 fix).
49
+ * 같은 process가 같은 디렉터리에 동시 atomic write 해도 안전.
50
+ */
51
+ function makeTmpPath(filePath) {
52
+ const suffix = randomBytes(6).toString('hex');
53
+ return `${filePath}.tmp.${process.pid}.${suffix}`;
54
+ }
55
+ /**
56
+ * JSON 데이터를 원자적으로 파일에 기록 (tmp → rename)
57
+ *
58
+ * @param options.pretty 들여쓴 포맷으로 직렬화
59
+ * @param options.mode 생성될 파일의 권한 (예: 0o600). 기본값은 umask 의존(보통 0o644).
60
+ * 민감한 캐시(컨텍스트 식별자/태그 포함)에 대해서는 0o600 권장.
61
+ * @param options.dirMode 부모 디렉터리 mode. cache는 0o700 권장.
62
+ *
63
+ * 권한 보장 (M6+M16+M17 fix):
64
+ * 1. tmp 파일이 random suffix → 다른 fd 충돌 없음 (Promise.all 안전).
65
+ * 2. fd 단위 fchmodSync로 새 inode 권한 강제.
66
+ * 3. post-rename chmodSync 실패는 throw — 느슨한 권한 침묵 방지.
67
+ * 4. Windows에서는 위 mode가 무효일 수 있으나, OS-level ACL이 일반적으로 user 격리.
68
+ */
69
+ export function atomicWriteJSON(filePath, data, options) {
70
+ const dir = path.dirname(filePath);
71
+ ensureDir(dir, options?.dirMode);
72
+ const tmpFile = makeTmpPath(filePath);
73
+ try {
74
+ const json = options?.pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
75
+ // O_EXCL: 새 inode 생성 보장 / O_NOFOLLOW: symlink 공격 차단 (POSIX only)
76
+ const fd = fs.openSync(tmpFile, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, options?.mode ?? 0o644);
77
+ try {
78
+ fs.writeFileSync(fd, json);
79
+ if (options?.mode !== undefined) {
80
+ // fd 단위 fchmod로 새 inode 권한 강제 (POSIX only — Windows no-op)
81
+ try {
82
+ fs.fchmodSync(fd, options.mode);
83
+ }
84
+ catch { /* Windows fallback */ }
85
+ }
86
+ }
87
+ finally {
88
+ fs.closeSync(fd);
89
+ }
90
+ fs.renameSync(tmpFile, filePath);
91
+ if (options?.mode !== undefined && process.platform !== 'win32') {
92
+ // POSIX: rename 후에도 명시적 chmod로 보장. 실패 시 throw (M6).
93
+ // Windows: chmodSync가 read-only 비트만 영향. 의미 없으므로 skip.
94
+ fs.chmodSync(filePath, options.mode);
95
+ }
96
+ }
97
+ catch (e) {
98
+ // rename 실패 시 tmp 파일 정리
99
+ try {
100
+ fs.unlinkSync(tmpFile);
101
+ }
102
+ catch { /* tmp file cleanup — leftover .tmp file is harmless if unlink fails */ }
103
+ throw e;
104
+ }
105
+ }
106
+ /** 텍스트를 원자적으로 파일에 기록 (tmp → rename) */
107
+ export function atomicWriteText(filePath, content, options) {
108
+ const dir = path.dirname(filePath);
109
+ ensureDir(dir, options?.dirMode);
110
+ const tmpFile = makeTmpPath(filePath);
111
+ try {
112
+ // O_EXCL: 새 inode 생성 보장 / O_NOFOLLOW: symlink 공격 차단 (POSIX only)
113
+ const fd = fs.openSync(tmpFile, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, options?.mode ?? 0o644);
114
+ try {
115
+ fs.writeFileSync(fd, content, 'utf-8');
116
+ if (options?.mode !== undefined) {
117
+ try {
118
+ fs.fchmodSync(fd, options.mode);
119
+ }
120
+ catch { /* Windows fallback */ }
121
+ }
122
+ }
123
+ finally {
124
+ fs.closeSync(fd);
125
+ }
126
+ fs.renameSync(tmpFile, filePath);
127
+ if (options?.mode !== undefined && process.platform !== 'win32') {
128
+ fs.chmodSync(filePath, options.mode);
129
+ }
130
+ }
131
+ catch (e) {
132
+ try {
133
+ fs.unlinkSync(tmpFile);
134
+ }
135
+ catch { /* cleanup */ }
136
+ throw e;
137
+ }
138
+ }
139
+ /** JSON 파일을 안전하게 읽기 (파싱 실패 시 fallback 반환) */
140
+ export function safeReadJSON(filePath, fallback) {
141
+ try {
142
+ if (fs.existsSync(filePath)) {
143
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
144
+ }
145
+ }
146
+ catch { /* JSON parse failure — return fallback */ }
147
+ return fallback;
148
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Forgen — Adaptive Context Budget
3
+ *
4
+ * 다른 플러그인이 감지되면 forgen의 컨텍스트 주입량을 동적으로 조절합니다.
5
+ * "양보 원칙": 컨텍스트 경쟁 시 forgen가 먼저 축소합니다.
6
+ *
7
+ * 버짓 계산:
8
+ * - 다른 플러그인 없음: factor = 1.0 (INJECTION_CAPS 그대로)
9
+ * - 훅 기반 메모리 플러그인 감지 (OMC, claude-mem 등): factor = 0.5 (50% 축소)
10
+ *
11
+ * 설계 결정:
12
+ * - 감지 결과는 plugin-detector에서 캐시 (1시간 TTL)
13
+ * - 하드캡(INJECTION_CAPS)을 초과하지 않음
14
+ * - MCP 기반 플러그인(Playwright, Context7 등)은 온디맨드 호출이므로
15
+ * 상시 컨텍스트를 점유하지 않아 factor에 영향 없음
16
+ */
17
+ export interface ContextBudget {
18
+ /** solution-injector 세션 총 주입 상한 (chars) */
19
+ solutionSessionMax: number;
20
+ /** solution 하나당 최대 글자 수 */
21
+ solutionMax: number;
22
+ /** 프롬프트당 최대 솔루션 수 */
23
+ solutionsPerPrompt: number;
24
+ /** notepad-injector 최대 글자 수 */
25
+ notepadMax: number;
26
+ /** skill-injector 스킬당 최대 글자 수 */
27
+ skillContentMax: number;
28
+ /** 축소 계수 (1.0 = 전체, 0.5 = 반) */
29
+ factor: number;
30
+ /** 다른 플러그인 감지 여부 */
31
+ otherPluginsDetected: boolean;
32
+ }
33
+ /**
34
+ * 현재 환경에 맞는 컨텍스트 버짓을 계산합니다.
35
+ * 다른 플러그인이 감지되면 주입량을 축소합니다.
36
+ */
37
+ export declare function calculateBudget(cwd?: string): ContextBudget;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Forgen — Adaptive Context Budget
3
+ *
4
+ * 다른 플러그인이 감지되면 forgen의 컨텍스트 주입량을 동적으로 조절합니다.
5
+ * "양보 원칙": 컨텍스트 경쟁 시 forgen가 먼저 축소합니다.
6
+ *
7
+ * 버짓 계산:
8
+ * - 다른 플러그인 없음: factor = 1.0 (INJECTION_CAPS 그대로)
9
+ * - 훅 기반 메모리 플러그인 감지 (OMC, claude-mem 등): factor = 0.5 (50% 축소)
10
+ *
11
+ * 설계 결정:
12
+ * - 감지 결과는 plugin-detector에서 캐시 (1시간 TTL)
13
+ * - 하드캡(INJECTION_CAPS)을 초과하지 않음
14
+ * - MCP 기반 플러그인(Playwright, Context7 등)은 온디맨드 호출이므로
15
+ * 상시 컨텍스트를 점유하지 않아 factor에 영향 없음
16
+ */
17
+ import { INJECTION_CAPS } from './injection-caps.js';
18
+ import { hasContextInjectingPlugins } from '../../core/plugin-detector.js';
19
+ // ── 버짓 계산 ──
20
+ /**
21
+ * 현재 환경에 맞는 컨텍스트 버짓을 계산합니다.
22
+ * 다른 플러그인이 감지되면 주입량을 축소합니다.
23
+ */
24
+ export function calculateBudget(cwd) {
25
+ let otherPluginsDetected = false;
26
+ let factor = 1.0;
27
+ try {
28
+ otherPluginsDetected = hasContextInjectingPlugins(cwd);
29
+ if (otherPluginsDetected)
30
+ factor = 0.5;
31
+ }
32
+ catch {
33
+ // 감지 실패 시 보수적 기본값 (충돌 없음으로 간주하면 위험)
34
+ factor = 0.7;
35
+ }
36
+ return {
37
+ solutionSessionMax: Math.floor(INJECTION_CAPS.solutionSessionMax * factor),
38
+ solutionMax: factor < 1.0 ? 800 : INJECTION_CAPS.solutionMax,
39
+ solutionsPerPrompt: factor < 1.0 ? 2 : 3,
40
+ notepadMax: Math.floor(INJECTION_CAPS.notepadMax * factor),
41
+ skillContentMax: Math.floor(INJECTION_CAPS.skillContentMax * factor),
42
+ factor,
43
+ otherPluginsDetected,
44
+ };
45
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 파일 단위 advisory lock — read-modify-write race 방지
3
+ *
4
+ * 문제:
5
+ * - injection cache를 여러 hook(solution-injector / pre-tool-use / backfill)이
6
+ * read → modify → write 패턴으로 갱신하지만 락이 없어서 last-writer-wins.
7
+ * - rename atomicity는 찢어진 JSON만 막을 뿐, 동시 mutator의 변경을 보존하지 못한다.
8
+ *
9
+ * 해결:
10
+ * - O_EXCL로 `${target}.lock` 파일 생성 → exclusive lock.
11
+ * - withFileLock() 안에서 호출되는 fn은 fresh re-read 후 mutate해야 함.
12
+ * - lock holder가 죽어 stale lock이 남으면 mtime + PID 검증으로 안전 회수.
13
+ * - lock 파일에 randomBytes token 기록, release 시 token 일치할 때만 unlink
14
+ * → cascade lock loss 방지 (H4+H7 fix).
15
+ *
16
+ * 외부 의존성 없음. 다중 OS 호환 (POSIX + Windows).
17
+ *
18
+ * Windows 한계:
19
+ * - file-lock 자체는 동작하지만, 같은 process의 다른 fd가 lock 파일을 read하지
20
+ * 못하게 막지는 않는다 (advisory lock).
21
+ * - lock 파일 mode 0o600은 POSIX에서만 의미. Windows는 ACL 기반.
22
+ */
23
+ export interface FileLockOptions {
24
+ /** 락 획득 최대 대기 시간 (ms). 초과 시 throw. */
25
+ timeoutMs?: number;
26
+ /** 이만큼 오래된 lock 파일은 stale로 간주하고 강제 회수 (ms). */
27
+ staleMs?: number;
28
+ }
29
+ /**
30
+ * file-lock 자체 결함 (Sentinel).
31
+ * caller try/catch가 lock 결함과 fn 실패를 구분할 수 있도록 별도 클래스.
32
+ */
33
+ export declare class FileLockError extends Error {
34
+ readonly cause: NodeJS.ErrnoException;
35
+ readonly lockPath: string;
36
+ constructor(cause: NodeJS.ErrnoException, lockPath: string);
37
+ }
38
+ /**
39
+ * 락을 획득한 후 fn을 실행하고, 끝나면 락을 해제한다.
40
+ *
41
+ * fn은 동기 또는 async 모두 지원. 예외가 발생해도 락은 항상 해제된다.
42
+ *
43
+ * release 시 lock token을 검증해 자기가 만든 lock만 unlink한다 (H4+H7 fix).
44
+ *
45
+ * 사용 예:
46
+ * ```ts
47
+ * await withFileLock(cachePath, () => {
48
+ * const fresh = readCacheFromDisk(cachePath); // lock 안에서 fresh re-read
49
+ * const merged = mergeWithUpdates(fresh);
50
+ * atomicWriteJSON(cachePath, merged, { mode: 0o600 });
51
+ * });
52
+ * ```
53
+ */
54
+ export declare function withFileLock<T>(targetPath: string, fn: () => T | Promise<T>, options?: FileLockOptions): Promise<T>;
55
+ /** 동기 버전 — async fn을 받지 않음. setTimeout 대신 짧은 spin으로 대기. */
56
+ export declare function withFileLockSync<T>(targetPath: string, fn: () => T, options?: FileLockOptions): T;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * 파일 단위 advisory lock — read-modify-write race 방지
3
+ *
4
+ * 문제:
5
+ * - injection cache를 여러 hook(solution-injector / pre-tool-use / backfill)이
6
+ * read → modify → write 패턴으로 갱신하지만 락이 없어서 last-writer-wins.
7
+ * - rename atomicity는 찢어진 JSON만 막을 뿐, 동시 mutator의 변경을 보존하지 못한다.
8
+ *
9
+ * 해결:
10
+ * - O_EXCL로 `${target}.lock` 파일 생성 → exclusive lock.
11
+ * - withFileLock() 안에서 호출되는 fn은 fresh re-read 후 mutate해야 함.
12
+ * - lock holder가 죽어 stale lock이 남으면 mtime + PID 검증으로 안전 회수.
13
+ * - lock 파일에 randomBytes token 기록, release 시 token 일치할 때만 unlink
14
+ * → cascade lock loss 방지 (H4+H7 fix).
15
+ *
16
+ * 외부 의존성 없음. 다중 OS 호환 (POSIX + Windows).
17
+ *
18
+ * Windows 한계:
19
+ * - file-lock 자체는 동작하지만, 같은 process의 다른 fd가 lock 파일을 read하지
20
+ * 못하게 막지는 않는다 (advisory lock).
21
+ * - lock 파일 mode 0o600은 POSIX에서만 의미. Windows는 ACL 기반.
22
+ */
23
+ import * as fs from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import { randomBytes } from 'node:crypto';
26
+ // L1 hardening: O_NOFOLLOW로 symlink 공격 차단 (POSIX only).
27
+ // Windows에는 정의되지 않으므로 0으로 fallback (no-op flag).
28
+ const O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;
29
+ const DEFAULT_TIMEOUT_MS = 2000;
30
+ /**
31
+ * staleMs는 fn 실행 시간 상한 + 안전 여유. 5초는 너무 짧아서 정상 holder가
32
+ * 5.x초 fn 실행 중일 때 다른 holder가 stale로 가로챌 수 있었다 (M13).
33
+ * 30초로 늘려 일반 cache 갱신은 안전 마진 확보.
34
+ *
35
+ * backfill 같은 짧은 fn은 호출자가 staleMs를 단축할 수 있다.
36
+ */
37
+ const DEFAULT_STALE_MS = 30000;
38
+ const RETRY_MIN_MS = 5;
39
+ const RETRY_MAX_MS = 25;
40
+ /**
41
+ * file-lock 자체 결함 (Sentinel).
42
+ * caller try/catch가 lock 결함과 fn 실패를 구분할 수 있도록 별도 클래스.
43
+ */
44
+ export class FileLockError extends Error {
45
+ cause;
46
+ lockPath;
47
+ constructor(cause, lockPath) {
48
+ // PR2c-4 (security M-3): basename만 노출. 전체 path는 sessionId를 포함하므로
49
+ // 로그에 남으면 정보 노출 위험. 디버그 시 caller가 e.lockPath로 명시 접근 가능.
50
+ super(`File lock failure on ${path.basename(lockPath)}: ${cause.code ?? cause.message}`);
51
+ this.cause = cause;
52
+ this.lockPath = lockPath;
53
+ this.name = 'FileLockError';
54
+ }
55
+ }
56
+ /**
57
+ * lock 파일 내용을 파싱한다. 손상된 lock은 null 반환.
58
+ */
59
+ function readLockMeta(lockPath) {
60
+ try {
61
+ const raw = fs.readFileSync(lockPath, 'utf-8');
62
+ const meta = JSON.parse(raw);
63
+ if (typeof meta.pid !== 'number' || typeof meta.ts !== 'number' || typeof meta.token !== 'string') {
64
+ return null;
65
+ }
66
+ return meta;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * PID가 살아있는지 확인한다 (POSIX).
74
+ * Windows에서는 process.kill(pid, 0)이 동작하지 않으므로 항상 true 반환.
75
+ */
76
+ function isPidAlive(pid) {
77
+ if (process.platform === 'win32')
78
+ return true;
79
+ try {
80
+ process.kill(pid, 0);
81
+ return true;
82
+ }
83
+ catch (e) {
84
+ const code = e.code;
85
+ return code !== 'ESRCH';
86
+ }
87
+ }
88
+ /**
89
+ * stale lock이면 안전하게 회수 시도. 회수 성공하면 true.
90
+ * - mtime 기반 + PID alive 검증으로 false positive 줄임.
91
+ * - 다른 holder가 그 사이에 lock을 잡은 경우는 회수하지 않음.
92
+ *
93
+ * Race window 분석 (R-M1 주석 보강):
94
+ * statSync(102)와 readLockMeta(108) 사이에 holder 교체가 일어날 수 있다.
95
+ * 즉, mtime이 stale 기준을 통과한 시점에 첫 holder가 release하고 두 번째
96
+ * holder가 lock을 잡으면, 우리가 읽는 meta는 새 holder의 것이다.
97
+ * 그러나 새 holder의 PID는 보통 alive이므로 isPidAlive 가드에서 false 반환
98
+ * → 회수 안 함. fail-safe 방향으로 동작한다.
99
+ * PID가 dead인 케이스에만 unlink하므로 잘못된 회수 위험은 매우 낮다.
100
+ */
101
+ function tryRecoverStaleLock(lockPath, staleMs) {
102
+ let lockStat;
103
+ try {
104
+ lockStat = fs.statSync(lockPath);
105
+ }
106
+ catch {
107
+ return true; // lock 사라짐 — 즉시 재시도
108
+ }
109
+ if (Date.now() - lockStat.mtimeMs <= staleMs)
110
+ return false;
111
+ const meta = readLockMeta(lockPath);
112
+ // PID 살아있으면 회수 안 함 (정상 long-running holder).
113
+ // stat-meta race에서 새 holder를 본 경우에도 여기서 fail-safe로 차단.
114
+ if (meta && isPidAlive(meta.pid))
115
+ return false;
116
+ // 회수 시도. 그 사이 다른 holder가 잡았을 수 있으므로 unlink는 best-effort.
117
+ try {
118
+ fs.unlinkSync(lockPath);
119
+ }
120
+ catch { /* 이미 회수됐을 수 있음 */ }
121
+ return true;
122
+ }
123
+ /**
124
+ * 락을 획득한 후 fn을 실행하고, 끝나면 락을 해제한다.
125
+ *
126
+ * fn은 동기 또는 async 모두 지원. 예외가 발생해도 락은 항상 해제된다.
127
+ *
128
+ * release 시 lock token을 검증해 자기가 만든 lock만 unlink한다 (H4+H7 fix).
129
+ *
130
+ * 사용 예:
131
+ * ```ts
132
+ * await withFileLock(cachePath, () => {
133
+ * const fresh = readCacheFromDisk(cachePath); // lock 안에서 fresh re-read
134
+ * const merged = mergeWithUpdates(fresh);
135
+ * atomicWriteJSON(cachePath, merged, { mode: 0o600 });
136
+ * });
137
+ * ```
138
+ */
139
+ export async function withFileLock(targetPath, fn, options) {
140
+ const lockPath = `${targetPath}.lock`;
141
+ const timeout = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
142
+ const stale = options?.staleMs ?? DEFAULT_STALE_MS;
143
+ const start = Date.now();
144
+ const myToken = randomBytes(16).toString('hex');
145
+ let acquired = false;
146
+ while (!acquired) {
147
+ try {
148
+ // O_EXCL: 파일이 이미 존재하면 EEXIST 반환
149
+ // O_NOFOLLOW: symlink 공격 차단 (POSIX only, Windows는 0으로 fallback)
150
+ const fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
151
+ try {
152
+ const meta = { pid: process.pid, ts: Date.now(), token: myToken };
153
+ fs.writeSync(fd, JSON.stringify(meta));
154
+ }
155
+ finally {
156
+ fs.closeSync(fd);
157
+ }
158
+ acquired = true;
159
+ }
160
+ catch (e) {
161
+ const code = e.code;
162
+ if (code !== 'EEXIST') {
163
+ // EACCES, EXDEV, ENOSPC 등 — sentinel error로 wrap (H5 fix)
164
+ throw new FileLockError(e, lockPath);
165
+ }
166
+ // 이미 lock이 있음 — stale 검증 후 회수 시도
167
+ if (tryRecoverStaleLock(lockPath, stale)) {
168
+ continue;
169
+ }
170
+ if (Date.now() - start > timeout) {
171
+ throw new Error(`File lock timeout after ${timeout}ms`);
172
+ }
173
+ // 짧은 backoff (jitter 포함)
174
+ const wait = RETRY_MIN_MS + Math.floor(Math.random() * (RETRY_MAX_MS - RETRY_MIN_MS));
175
+ await new Promise(r => setTimeout(r, wait));
176
+ }
177
+ }
178
+ try {
179
+ return await fn();
180
+ }
181
+ finally {
182
+ // H4+H7 fix: token 검증 후에만 unlink. stale recovery로 다른 holder가
183
+ // 잡은 lock을 우리가 지우지 않도록 보장.
184
+ try {
185
+ const meta = readLockMeta(lockPath);
186
+ if (meta && meta.token === myToken) {
187
+ fs.unlinkSync(lockPath);
188
+ }
189
+ // token 다르면 stale로 회수당한 후 다른 holder가 잡은 것 — 건드리지 않음
190
+ }
191
+ catch { /* lock 이미 사라짐 */ }
192
+ }
193
+ }
194
+ /** 동기 버전 — async fn을 받지 않음. setTimeout 대신 짧은 spin으로 대기. */
195
+ export function withFileLockSync(targetPath, fn, options) {
196
+ const lockPath = `${targetPath}.lock`;
197
+ const timeout = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
198
+ const stale = options?.staleMs ?? DEFAULT_STALE_MS;
199
+ const start = Date.now();
200
+ const myToken = randomBytes(16).toString('hex');
201
+ let acquired = false;
202
+ while (!acquired) {
203
+ try {
204
+ // M-1 fix: sync 경로도 async와 동일하게 O_NOFOLLOW로 symlink 차단
205
+ const fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
206
+ try {
207
+ const meta = { pid: process.pid, ts: Date.now(), token: myToken };
208
+ fs.writeSync(fd, JSON.stringify(meta));
209
+ }
210
+ finally {
211
+ fs.closeSync(fd);
212
+ }
213
+ acquired = true;
214
+ }
215
+ catch (e) {
216
+ const code = e.code;
217
+ if (code !== 'EEXIST') {
218
+ throw new FileLockError(e, lockPath);
219
+ }
220
+ if (tryRecoverStaleLock(lockPath, stale)) {
221
+ continue;
222
+ }
223
+ if (Date.now() - start > timeout) {
224
+ throw new Error(`File lock timeout after ${timeout}ms`);
225
+ }
226
+ // 동기 spin: Atomics.wait로 짧게 대기 (이벤트 루프 블로킹은 의도된 것)
227
+ const wait = RETRY_MIN_MS + Math.floor(Math.random() * (RETRY_MAX_MS - RETRY_MIN_MS));
228
+ try {
229
+ const sab = new SharedArrayBuffer(4);
230
+ const view = new Int32Array(sab);
231
+ Atomics.wait(view, 0, 0, wait);
232
+ }
233
+ catch {
234
+ // SharedArrayBuffer/Atomics 미지원 환경 — bounded busy wait로 폴백
235
+ // (Cloudflare Workers 등에서 throw 후 즉시 다음 iteration 진입 방지)
236
+ const end = Date.now() + wait;
237
+ while (Date.now() < end) { /* bounded busy wait */ }
238
+ }
239
+ }
240
+ }
241
+ try {
242
+ return fn();
243
+ }
244
+ finally {
245
+ try {
246
+ const meta = readLockMeta(lockPath);
247
+ if (meta && meta.token === myToken) {
248
+ fs.unlinkSync(lockPath);
249
+ }
250
+ }
251
+ catch { /* skip */ }
252
+ }
253
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Forgen — Hook Response Utilities
3
+ *
4
+ * Claude Code Plugin SDK 공식 형식에 맞는 훅 응답 생성.
5
+ *
6
+ * 공식 형식 (검증 완료 — claude-code 소스 기반):
7
+ * hookSpecificOutput은 discriminated union이며 hookEventName이 필수.
8
+ * - PreToolUse: { hookEventName, permissionDecision, permissionDecisionReason? }
9
+ * - UserPromptSubmit: { hookEventName, additionalContext? }
10
+ * - SessionStart: { hookEventName, additionalContext?, initialUserMessage? }
11
+ *
12
+ * 주의:
13
+ * systemMessage 필드는 UI 표시용으로만 사용되며 모델에 전달되지 않음.
14
+ * 모델에 컨텍스트를 주입하려면 반드시 additionalContext를 사용해야 함.
15
+ */
16
+ /** 통과 응답 (컨텍스트 없음, 모든 이벤트 공통) */
17
+ export declare function approve(): string;
18
+ /**
19
+ * 통과 + 모델에 컨텍스트 주입.
20
+ * UserPromptSubmit, SessionStart 이벤트에서만 모델에 도달함.
21
+ */
22
+ export declare function approveWithContext(context: string, eventName: string): string;
23
+ /**
24
+ * 통과 + UI 경고 표시 (모델에는 전달되지 않음).
25
+ * PostToolUse, PreToolUse 경고 등 모델 도달이 불필요한 경우 사용.
26
+ */
27
+ export declare function approveWithWarning(warning: string): string;
28
+ /** 차단 응답 (PreToolUse 전용) */
29
+ export declare function deny(reason: string): string;
30
+ /** 사용자 확인 요청 (PreToolUse 전용) */
31
+ export declare function ask(reason: string): string;
32
+ /** fail-open: 에러 시 안전하게 통과 */
33
+ export declare function failOpen(): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Forgen — Hook Response Utilities
3
+ *
4
+ * Claude Code Plugin SDK 공식 형식에 맞는 훅 응답 생성.
5
+ *
6
+ * 공식 형식 (검증 완료 — claude-code 소스 기반):
7
+ * hookSpecificOutput은 discriminated union이며 hookEventName이 필수.
8
+ * - PreToolUse: { hookEventName, permissionDecision, permissionDecisionReason? }
9
+ * - UserPromptSubmit: { hookEventName, additionalContext? }
10
+ * - SessionStart: { hookEventName, additionalContext?, initialUserMessage? }
11
+ *
12
+ * 주의:
13
+ * systemMessage 필드는 UI 표시용으로만 사용되며 모델에 전달되지 않음.
14
+ * 모델에 컨텍스트를 주입하려면 반드시 additionalContext를 사용해야 함.
15
+ */
16
+ /** 통과 응답 (컨텍스트 없음, 모든 이벤트 공통) */
17
+ export function approve() {
18
+ return JSON.stringify({ continue: true });
19
+ }
20
+ /**
21
+ * 통과 + 모델에 컨텍스트 주입.
22
+ * UserPromptSubmit, SessionStart 이벤트에서만 모델에 도달함.
23
+ */
24
+ export function approveWithContext(context, eventName) {
25
+ return JSON.stringify({
26
+ continue: true,
27
+ hookSpecificOutput: { hookEventName: eventName, additionalContext: context },
28
+ });
29
+ }
30
+ /**
31
+ * 통과 + UI 경고 표시 (모델에는 전달되지 않음).
32
+ * PostToolUse, PreToolUse 경고 등 모델 도달이 불필요한 경우 사용.
33
+ */
34
+ export function approveWithWarning(warning) {
35
+ return JSON.stringify({ continue: true, suppressOutput: false, systemMessage: warning });
36
+ }
37
+ /** 차단 응답 (PreToolUse 전용) */
38
+ export function deny(reason) {
39
+ return JSON.stringify({
40
+ continue: false,
41
+ hookSpecificOutput: {
42
+ hookEventName: 'PreToolUse',
43
+ permissionDecision: 'deny',
44
+ permissionDecisionReason: reason,
45
+ },
46
+ });
47
+ }
48
+ /** 사용자 확인 요청 (PreToolUse 전용) */
49
+ export function ask(reason) {
50
+ return JSON.stringify({
51
+ continue: true,
52
+ hookSpecificOutput: {
53
+ hookEventName: 'PreToolUse',
54
+ permissionDecision: 'ask',
55
+ permissionDecisionReason: reason,
56
+ },
57
+ });
58
+ }
59
+ /** fail-open: 에러 시 안전하게 통과 */
60
+ export function failOpen() {
61
+ return JSON.stringify({ continue: true });
62
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Forgen — Injection Caps
3
+ *
4
+ * 컨텍스트 주입 하드캡 상수. 다른 플러그인과 공존 시 컨텍스트 윈도우를
5
+ * 과도하게 점유하지 않도록 각 훅의 최대 주입량을 제한합니다.
6
+ *
7
+ * 이 값들은 어댑티브 버짓 시스템의 "절대 상한"이며,
8
+ * 실제 주입량은 context-budget.ts에서 동적으로 조절됩니다.
9
+ */
10
+ export declare const INJECTION_CAPS: {
11
+ /** notepad-injector: 노트패드 최대 주입 글자 수 (현재: 무제한 → 2000) */
12
+ readonly notepadMax: 2000;
13
+ /** skill-injector: 스킬 파일 하나당 최대 글자 수 (현재: 무제한 → 3000) */
14
+ readonly skillContentMax: 3000;
15
+ /** solution-injector: 솔루션 하나당 최대 글자 수 (기존 값 유지) */
16
+ readonly solutionMax: 1500;
17
+ /** solution-injector: 세션 전체 주입 상한 (기존 값 유지) */
18
+ readonly solutionSessionMax: 8000;
19
+ };
20
+ /**
21
+ * .claude/rules/ 자동생성 파일의 사이즈 하드캡.
22
+ * rules 파일은 Claude Code가 세션 시작 시 전부 로드하므로
23
+ * 무제한 성장을 방지해야 합니다.
24
+ *
25
+ * 근거: Claude Code 공식 권장 — "CLAUDE.md 파일당 200줄 이하",
26
+ * "길수록 지시 준수율 저하". 3000자 ≈ 60~80줄.
27
+ */
28
+ export declare const RULE_FILE_CAPS: {
29
+ /** .claude/rules/ 파일 1개당 최대 글자 수 */
30
+ readonly perRuleFile: 3000;
31
+ /** .claude/rules/에 forgen가 쓸 수 있는 총량 (글자) */
32
+ readonly totalRuleFiles: 15000;
33
+ /** compound.md에 포함할 rule summary 최대 수 */
34
+ readonly maxRuleSummaries: 10;
35
+ };
36
+ /** truncation 시 끝에 추가되는 표시 */
37
+ export declare const TRUNCATION_SUFFIX = "\n... (truncated)";
38
+ /** 주어진 content를 maxChars로 잘라서 반환. 초과 시 truncation suffix 추가. */
39
+ export declare function truncateContent(content: string, maxChars: number): string;