@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,83 @@
1
+ /**
2
+ * Forgen v1 — Pack Mismatch Detector
3
+ *
4
+ * rolling 3세션 mismatch score 계산.
5
+ * Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §8
6
+ *
7
+ * 신호별 점수:
8
+ * - 반대 방향 explicit_correction: +2
9
+ * - session_summary가 반대 pack 성향 명시: +1
10
+ * - 같은 축 strong rule 2개+ 신규 생성: +1
11
+ *
12
+ * 후보 조건:
13
+ * - 최근 3세션 rolling sum >= 4 (축별)
14
+ * - 또는 최근 3세션에 같은 방향 explicit_correction 2회+
15
+ */
16
+ /**
17
+ * 단일 세션의 mismatch 신호를 계산.
18
+ *
19
+ * @param sessionId 세션 ID
20
+ * @param corrections 해당 세션의 explicit_correction evidence
21
+ * @param summaries 해당 세션의 session_summary evidence
22
+ * @param newStrongRules 해당 세션에서 신규 생성된 strong rule
23
+ * @param currentQuality 현재 quality pack
24
+ * @param currentAutonomy 현재 autonomy pack
25
+ */
26
+ export function computeSessionSignals(sessionId, corrections, summaries, newStrongRules, _currentQuality, _currentAutonomy) {
27
+ const signals = [];
28
+ // 반대 방향 explicit_correction
29
+ for (const c of corrections) {
30
+ for (const axis of c.axis_refs) {
31
+ if (axis === 'quality_safety' || axis === 'autonomy') {
32
+ // correction의 raw_payload에 direction 힌트가 있으면 pack과 비교
33
+ const direction = c.raw_payload?.direction;
34
+ if (direction === 'opposite') {
35
+ signals.push({ session_id: sessionId, axis, score: 2, reason: `반대 방향 correction: ${c.summary}` });
36
+ }
37
+ }
38
+ }
39
+ }
40
+ // session_summary가 반대 성향 명시
41
+ for (const s of summaries) {
42
+ const packHint = s.raw_payload?.pack_direction;
43
+ if (packHint === 'opposite_quality') {
44
+ signals.push({ session_id: sessionId, axis: 'quality_safety', score: 1, reason: `session summary 반대 성향: ${s.summary}` });
45
+ }
46
+ if (packHint === 'opposite_autonomy') {
47
+ signals.push({ session_id: sessionId, axis: 'autonomy', score: 1, reason: `session summary 반대 성향: ${s.summary}` });
48
+ }
49
+ }
50
+ // 같은 축 strong rule 2개+ 신규 생성
51
+ const qualityStrong = newStrongRules.filter(r => r.category === 'quality' && r.strength === 'strong');
52
+ const autonomyStrong = newStrongRules.filter(r => r.category === 'autonomy' && r.strength === 'strong');
53
+ if (qualityStrong.length >= 2) {
54
+ signals.push({ session_id: sessionId, axis: 'quality_safety', score: 1, reason: `${qualityStrong.length}개 strong quality rule 신규 생성` });
55
+ }
56
+ if (autonomyStrong.length >= 2) {
57
+ signals.push({ session_id: sessionId, axis: 'autonomy', score: 1, reason: `${autonomyStrong.length}개 strong autonomy rule 신규 생성` });
58
+ }
59
+ return signals;
60
+ }
61
+ /**
62
+ * 최근 3세션 rolling sum으로 mismatch 판정.
63
+ */
64
+ export function detectMismatch(recentSignals) {
65
+ let qualityScore = 0;
66
+ let autonomyScore = 0;
67
+ for (const s of recentSignals) {
68
+ if (s.axis === 'quality_safety')
69
+ qualityScore += s.score;
70
+ if (s.axis === 'autonomy')
71
+ autonomyScore += s.score;
72
+ }
73
+ // 같은 방향 correction 2회+ 체크
74
+ const qualityCorrections = recentSignals.filter(s => s.axis === 'quality_safety' && s.score === 2);
75
+ const autonomyCorrections = recentSignals.filter(s => s.axis === 'autonomy' && s.score === 2);
76
+ return {
77
+ quality_mismatch: qualityScore >= 4 || qualityCorrections.length >= 2,
78
+ autonomy_mismatch: autonomyScore >= 4 || autonomyCorrections.length >= 2,
79
+ quality_score: qualityScore,
80
+ autonomy_score: autonomyScore,
81
+ signals: recentSignals,
82
+ };
83
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Forgen v1 — Onboarding CLI
3
+ *
4
+ * 언어 선택 + 4문항 온보딩 인터랙티브 flow.
5
+ */
6
+ export declare function runOnboarding(): Promise<void>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Forgen v1 — Onboarding CLI
3
+ *
4
+ * 언어 선택 + 4문항 온보딩 인터랙티브 flow.
5
+ */
6
+ import * as readline from 'node:readline';
7
+ import { computeOnboarding, onboardingToRecommendation } from './onboarding.js';
8
+ import { createProfile, saveProfile } from '../store/profile-store.js';
9
+ import { saveRecommendation, updateRecommendationStatus } from '../store/recommendation-store.js';
10
+ import { ensureV1Directories } from '../core/v1-bootstrap.js';
11
+ import { ONBOARDING, qualityName, autonomyName, judgmentName, communicationName, trustName, setLocale } from '../i18n/index.js';
12
+ import { saveGlobalConfig, loadGlobalConfig } from '../core/global-config.js';
13
+ function askChoice(rl, question, validChoices, errorMsg) {
14
+ return new Promise((resolve) => {
15
+ const ask = () => {
16
+ rl.question(question, (answer) => {
17
+ const upper = answer.trim().toUpperCase();
18
+ if (validChoices.includes(upper)) {
19
+ resolve(upper);
20
+ }
21
+ else {
22
+ console.log(errorMsg);
23
+ ask();
24
+ }
25
+ });
26
+ };
27
+ ask();
28
+ });
29
+ }
30
+ export async function runOnboarding() {
31
+ ensureV1Directories();
32
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
33
+ try {
34
+ // 0. 언어 선택
35
+ console.log(`
36
+ ╔══════════════════════════════════════════════╗
37
+ ║ Forgen — Setup ║
38
+ ╚══════════════════════════════════════════════╝
39
+
40
+ Select language / 언어를 선택하세요:
41
+
42
+ 1) English
43
+ 2) 한국어
44
+ `);
45
+ const langChoice = await askChoice(rl, ' Choice / 선택 (1/2): ', ['1', '2'], ' Please enter 1 or 2. / 1 또는 2를 입력하세요.');
46
+ const locale = langChoice === '2' ? 'ko' : 'en';
47
+ setLocale(locale);
48
+ // locale을 GlobalConfig에 저장
49
+ const config = loadGlobalConfig();
50
+ config.locale = locale;
51
+ saveGlobalConfig(config);
52
+ const strings = ONBOARDING[locale];
53
+ console.log(`
54
+ ╔══════════════════════════════════════════════╗
55
+ ║${strings.header.padEnd(46)}║
56
+ ╚══════════════════════════════════════════════╝
57
+
58
+ ${strings.subtitle}`);
59
+ // 1-4. 4문항
60
+ const q1 = await askChoice(rl, strings.q1, ['A', 'B', 'C'], strings.invalidChoice);
61
+ const q2 = await askChoice(rl, strings.q2, ['A', 'B', 'C'], strings.invalidChoice);
62
+ const q3 = await askChoice(rl, strings.q3, ['A', 'B', 'C'], strings.invalidChoice);
63
+ const q4 = await askChoice(rl, strings.q4, ['A', 'B', 'C'], strings.invalidChoice);
64
+ const result = computeOnboarding(q1, q2, q3, q4);
65
+ console.log(`
66
+ ─────────────────────────────────────────
67
+ ${strings.resultHeader}
68
+
69
+ Quality: ${qualityName(result.qualityPack, locale)} (confidence: ${result.qualityConfidence.toFixed(2)})
70
+ Autonomy: ${autonomyName(result.autonomyPack, locale)} (confidence: ${result.autonomyConfidence.toFixed(2)})
71
+ Judgment: ${judgmentName(result.judgmentPack, locale)} (confidence: ${result.judgmentConfidence.toFixed(2)})
72
+ Communication: ${communicationName(result.communicationPack, locale)} (confidence: ${result.communicationConfidence.toFixed(2)})
73
+ Trust: ${trustName(result.suggestedTrustPolicy, locale)}
74
+ ─────────────────────────────────────────`);
75
+ // Recommendation 저장
76
+ const rec = onboardingToRecommendation(result);
77
+ saveRecommendation(rec);
78
+ updateRecommendationStatus(rec.recommendation_id, 'accepted');
79
+ // Profile 생성
80
+ const profile = createProfile('default', result.qualityPack, result.autonomyPack, result.suggestedTrustPolicy, 'onboarding', result.judgmentPack, result.communicationPack);
81
+ saveProfile(profile);
82
+ console.log(`
83
+ ${strings.profileSaved}
84
+ `);
85
+ }
86
+ finally {
87
+ rl.close();
88
+ }
89
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Forgen v1 — Onboarding
3
+ *
4
+ * 2문항 온보딩, 점수 계산, pack 추천.
5
+ * Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §3-4
6
+ */
7
+ import type { QualityPack, AutonomyPack, JudgmentPack, CommunicationPack, TrustPolicy, PackRecommendation } from '../store/types.js';
8
+ export type ChoiceId = 'A' | 'B' | 'C';
9
+ export interface OnboardingResult {
10
+ qualityScore: number;
11
+ autonomyScore: number;
12
+ judgmentScore: number;
13
+ communicationScore: number;
14
+ qualityPack: QualityPack;
15
+ autonomyPack: AutonomyPack;
16
+ judgmentPack: JudgmentPack;
17
+ communicationPack: CommunicationPack;
18
+ qualityConfidence: number;
19
+ autonomyConfidence: number;
20
+ judgmentConfidence: number;
21
+ communicationConfidence: number;
22
+ suggestedTrustPolicy: TrustPolicy;
23
+ }
24
+ export declare function computeOnboarding(q1: ChoiceId, q2: ChoiceId, q3?: ChoiceId, q4?: ChoiceId): OnboardingResult;
25
+ export declare function onboardingToRecommendation(result: OnboardingResult): PackRecommendation;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Forgen v1 — Onboarding
3
+ *
4
+ * 2문항 온보딩, 점수 계산, pack 추천.
5
+ * Authoritative spec: docs/plans/2026-04-03-forgen-onboarding-adaptation-spec.md §3-4
6
+ */
7
+ import { createRecommendation } from '../store/recommendation-store.js';
8
+ // 질문 1: 애매한 구현 요청 + 인접 영향 가능성
9
+ // 주 판별: 자율 실행 성향, 보조: 품질/안전
10
+ const Q1_SCORES = {
11
+ A: { quality: -1, autonomy: -2 },
12
+ B: { quality: 0, autonomy: 0 },
13
+ C: { quality: +1, autonomy: +2 },
14
+ };
15
+ // 질문 2: 수정 완료 직전 + 검증 강도 vs 완료 속도
16
+ // 주 판별: 품질/안전, 보조: 자율 실행 성향
17
+ const Q2_SCORES = {
18
+ A: { quality: -2, autonomy: 0 },
19
+ B: { quality: 0, autonomy: 0 },
20
+ C: { quality: +2, autonomy: +1 },
21
+ };
22
+ // 질문 3: 코드 수정 접근법 — 최소변경 vs 구조적 리팩토링
23
+ // 주 판별: 판단 철학
24
+ const Q3_SCORES = {
25
+ A: { judgment: -2 }, // 최소 변경 우선
26
+ B: { judgment: 0 },
27
+ C: { judgment: +2 }, // 구조적 정리 우선
28
+ };
29
+ // 질문 4: 설명/보고 스타일 — 간결 vs 상세
30
+ // 주 판별: 커뮤니케이션 스타일
31
+ const Q4_SCORES = {
32
+ A: { communication: +2 }, // 상세 설명 선호
33
+ B: { communication: 0 },
34
+ C: { communication: -2 }, // 간결 선호
35
+ };
36
+ // ── Pack 매핑 ──
37
+ function qualityFromScore(score) {
38
+ if (score <= -2)
39
+ return '보수형';
40
+ if (score >= 2)
41
+ return '속도형';
42
+ return '균형형';
43
+ }
44
+ function autonomyFromScore(score) {
45
+ if (score <= -2)
46
+ return '확인 우선형';
47
+ if (score >= 2)
48
+ return '자율 실행형';
49
+ return '균형형';
50
+ }
51
+ function judgmentFromScore(score) {
52
+ if (score <= -1)
53
+ return '최소변경형';
54
+ if (score >= 1)
55
+ return '구조적접근형';
56
+ return '균형형';
57
+ }
58
+ function communicationFromScore(score) {
59
+ if (score <= -1)
60
+ return '간결형';
61
+ if (score >= 1)
62
+ return '상세형';
63
+ return '균형형';
64
+ }
65
+ // ── Confidence ──
66
+ function computeConfidence(score, q1Contribution, q2Contribution) {
67
+ // contradiction: 2문항 기여 부호가 반대일 때
68
+ const contradictions = (q1Contribution > 0 && q2Contribution < 0) || (q1Contribution < 0 && q2Contribution > 0) ? 1 : 0;
69
+ const raw = 0.45 + (0.2 * Math.abs(score)) - (0.15 * contradictions);
70
+ return Math.max(0.2, Math.min(0.95, raw));
71
+ }
72
+ // ── Trust Policy 추천 ──
73
+ const TRUST_MAP = {
74
+ '보수형+확인 우선형': '가드레일 우선',
75
+ '속도형+자율 실행형': '완전 신뢰 실행',
76
+ };
77
+ function suggestTrustPolicy(quality, autonomy) {
78
+ return TRUST_MAP[`${quality}+${autonomy}`] ?? '승인 완화';
79
+ }
80
+ export function computeOnboarding(q1, q2, q3 = 'B', q4 = 'B') {
81
+ const qualityScore = Q1_SCORES[q1].quality + Q2_SCORES[q2].quality;
82
+ const autonomyScore = Q1_SCORES[q1].autonomy + Q2_SCORES[q2].autonomy;
83
+ const judgmentScore = Q3_SCORES[q3].judgment;
84
+ const communicationScore = Q4_SCORES[q4].communication;
85
+ const qualityPack = qualityFromScore(qualityScore);
86
+ const autonomyPack = autonomyFromScore(autonomyScore);
87
+ const judgmentPack = judgmentFromScore(judgmentScore);
88
+ const communicationPack = communicationFromScore(communicationScore);
89
+ const qualityConfidence = computeConfidence(qualityScore, Q1_SCORES[q1].quality, Q2_SCORES[q2].quality);
90
+ const autonomyConfidence = computeConfidence(autonomyScore, Q1_SCORES[q1].autonomy, Q2_SCORES[q2].autonomy);
91
+ // Q3, Q4는 단일 질문이므로 confidence가 낮음
92
+ const judgmentConfidence = Math.max(0.2, Math.min(0.75, 0.35 + 0.2 * Math.abs(judgmentScore)));
93
+ const communicationConfidence = Math.max(0.2, Math.min(0.75, 0.35 + 0.2 * Math.abs(communicationScore)));
94
+ return {
95
+ qualityScore,
96
+ autonomyScore,
97
+ judgmentScore,
98
+ communicationScore,
99
+ qualityPack,
100
+ autonomyPack,
101
+ judgmentPack,
102
+ communicationPack,
103
+ qualityConfidence,
104
+ autonomyConfidence,
105
+ judgmentConfidence,
106
+ communicationConfidence,
107
+ suggestedTrustPolicy: suggestTrustPolicy(qualityPack, autonomyPack),
108
+ };
109
+ }
110
+ export function onboardingToRecommendation(result) {
111
+ const avgConfidence = (result.qualityConfidence + result.autonomyConfidence + result.judgmentConfidence + result.communicationConfidence) / 4;
112
+ return createRecommendation({
113
+ source: 'onboarding',
114
+ quality_pack: result.qualityPack,
115
+ autonomy_pack: result.autonomyPack,
116
+ judgment_pack: result.judgmentPack,
117
+ communication_pack: result.communicationPack,
118
+ suggested_trust_policy: result.suggestedTrustPolicy,
119
+ confidence: avgConfidence,
120
+ reason_summary: `온보딩 4문항 결과: quality=${result.qualityScore}, autonomy=${result.autonomyScore}, judgment=${result.judgmentScore}, communication=${result.communicationScore}`,
121
+ });
122
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Forgen — Compound Reflection Logic
3
+ *
4
+ * Code Reflection의 false positive를 줄이기 위한 3중 필터:
5
+ * 1. 시간 윈도우: 솔루션 주입 후 15분 이내만 반영으로 인정
6
+ * 2. 매칭 비율: 유효 식별자의 50% 이상 매칭 필요
7
+ * 3. 공통 식별자 차단: 프레임워크 기본 용어는 매칭에서 제외
8
+ *
9
+ * ADR: 기존 pre-tool-use.ts의 checkCompoundReflection 인라인 로직을
10
+ * 별도 모듈로 분리. 이유: (1) 테스트 가능성 (순수 함수), (2) false
11
+ * positive 문제(action-plan §2.1)의 근본 수정에 명확한 책임 경계 필요.
12
+ */
13
+ /** 주입 후 이 시간 내에 코드에 식별자가 출현해야 reflection으로 인정 */
14
+ export declare const REFLECTION_WINDOW_MS: number;
15
+ /**
16
+ * 프레임워크/라이브러리 기본 식별자 블록리스트.
17
+ * 이 식별자들은 솔루션 주입과 무관하게 코드에 자연 출현하므로
18
+ * reflection 매칭에서 제외한다.
19
+ *
20
+ * 기준: "이 단어가 코드에 있다고 해서 사용자가 forgen의 솔루션을
21
+ * 참고했다고 볼 수 없는 단어"
22
+ */
23
+ export declare const COMMON_IDENTIFIERS: Set<string>;
24
+ export interface ReflectionInput {
25
+ identifiers: string[];
26
+ code: string;
27
+ injectedAt: string;
28
+ now?: Date;
29
+ }
30
+ export interface ReflectionResult {
31
+ reflected: boolean;
32
+ matchedCount: number;
33
+ eligibleCount: number;
34
+ reason?: 'outside-window' | 'low-match-ratio' | 'no-eligible-identifiers' | 'code-too-short' | 'invalid-injection-time';
35
+ }
36
+ /**
37
+ * 솔루션의 식별자가 코드에 반영되었는지 판정한다 (순수 함수).
38
+ *
39
+ * 3중 필터:
40
+ * 1. 코드 최소 길이 (10자)
41
+ * 2. 시간 윈도우 (주입 후 15분)
42
+ * 3. 유효 식별자 필터링 (6자 이상 + 블록리스트 제외)
43
+ * 4. 매칭 비율 (유효 식별자의 50% 이상, 최소 1개)
44
+ */
45
+ export declare function isReflectionCandidate(input: ReflectionInput): ReflectionResult;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Forgen — Compound Reflection Logic
3
+ *
4
+ * Code Reflection의 false positive를 줄이기 위한 3중 필터:
5
+ * 1. 시간 윈도우: 솔루션 주입 후 15분 이내만 반영으로 인정
6
+ * 2. 매칭 비율: 유효 식별자의 50% 이상 매칭 필요
7
+ * 3. 공통 식별자 차단: 프레임워크 기본 용어는 매칭에서 제외
8
+ *
9
+ * ADR: 기존 pre-tool-use.ts의 checkCompoundReflection 인라인 로직을
10
+ * 별도 모듈로 분리. 이유: (1) 테스트 가능성 (순수 함수), (2) false
11
+ * positive 문제(action-plan §2.1)의 근본 수정에 명확한 책임 경계 필요.
12
+ */
13
+ /** 주입 후 이 시간 내에 코드에 식별자가 출현해야 reflection으로 인정 */
14
+ export const REFLECTION_WINDOW_MS = 15 * 60 * 1000; // 15분
15
+ /**
16
+ * 프레임워크/라이브러리 기본 식별자 블록리스트.
17
+ * 이 식별자들은 솔루션 주입과 무관하게 코드에 자연 출현하므로
18
+ * reflection 매칭에서 제외한다.
19
+ *
20
+ * 기준: "이 단어가 코드에 있다고 해서 사용자가 forgen의 솔루션을
21
+ * 참고했다고 볼 수 없는 단어"
22
+ */
23
+ export const COMMON_IDENTIFIERS = new Set([
24
+ // React
25
+ 'useState', 'useEffect', 'useCallback', 'useReducer', 'useContext', 'useLayoutEffect',
26
+ 'useImperativeHandle', 'useDebugValue', 'useDeferredValue', 'useTransition', 'useSyncExternalStore',
27
+ 'useInsertionEffect', 'createElement', 'createContext', 'createRef', 'forwardRef',
28
+ 'ErrorBoundary', 'Suspense', 'StrictMode', 'Fragment', 'Component',
29
+ // Next.js
30
+ 'getServerSideProps', 'getStaticProps', 'getStaticPaths', 'NextRequest', 'NextResponse',
31
+ 'useRouter', 'usePathname', 'useSearchParams',
32
+ // Node.js / common
33
+ 'require', 'exports', 'module', 'process', 'console', 'setTimeout', 'setInterval',
34
+ 'Promise', 'Buffer', 'EventEmitter',
35
+ // Testing
36
+ 'describe', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll',
37
+ // TypeScript
38
+ 'interface', 'implements', 'extends', 'abstract', 'readonly',
39
+ // Common methods/patterns
40
+ 'toString', 'valueOf', 'constructor', 'prototype', 'hasOwnProperty',
41
+ 'addEventListener', 'removeEventListener', 'querySelector', 'getElementById',
42
+ // Libraries
43
+ 'express', 'mongoose', 'sequelize', 'prisma',
44
+ ]);
45
+ /**
46
+ * 솔루션의 식별자가 코드에 반영되었는지 판정한다 (순수 함수).
47
+ *
48
+ * 3중 필터:
49
+ * 1. 코드 최소 길이 (10자)
50
+ * 2. 시간 윈도우 (주입 후 15분)
51
+ * 3. 유효 식별자 필터링 (6자 이상 + 블록리스트 제외)
52
+ * 4. 매칭 비율 (유효 식별자의 50% 이상, 최소 1개)
53
+ */
54
+ export function isReflectionCandidate(input) {
55
+ const { identifiers, code, injectedAt } = input;
56
+ const now = input.now ?? new Date();
57
+ // Gate 1: 코드 최소 길이
58
+ if (!code || code.length < 10) {
59
+ return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'code-too-short' };
60
+ }
61
+ // Gate 2: 주입 시각 유효성 + 시간 윈도우
62
+ const injectedTime = new Date(injectedAt).getTime();
63
+ if (Number.isNaN(injectedTime)) {
64
+ return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'invalid-injection-time' };
65
+ }
66
+ const elapsed = now.getTime() - injectedTime;
67
+ if (elapsed > REFLECTION_WINDOW_MS || elapsed < 0) {
68
+ return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'outside-window' };
69
+ }
70
+ // Gate 3: 유효 식별자 필터링
71
+ const eligible = identifiers.filter(id => id.length >= 6 && !COMMON_IDENTIFIERS.has(id));
72
+ if (eligible.length === 0) {
73
+ return { reflected: false, matchedCount: 0, eligibleCount: 0, reason: 'no-eligible-identifiers' };
74
+ }
75
+ // Gate 4: 매칭 비율 검사
76
+ const matchedCount = eligible.filter(id => code.includes(id)).length;
77
+ const minRequired = Math.max(1, Math.ceil(eligible.length * 0.5));
78
+ if (matchedCount < minRequired) {
79
+ return { reflected: false, matchedCount, eligibleCount: eligible.length, reason: 'low-match-ratio' };
80
+ }
81
+ return { reflected: true, matchedCount, eligibleCount: eligible.length };
82
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forgen — Context Guard Hook
4
+ *
5
+ * Claude Code Stop 훅으로 등록.
6
+ * context window limit, edit error 등 실행 중 에러를 감지하여
7
+ * 사용자에게 경고하고 상태를 보존합니다.
8
+ *
9
+ * 또한 UserPromptSubmit에서 현재 대화 길이를 추적하여
10
+ * context 한계에 접근 시 preemptive 경고를 제공합니다.
11
+ */
12
+ /** 경고 표시 여부 판정 (순수 함수) */
13
+ export declare function shouldWarn(contextPercent: {
14
+ promptCount: number;
15
+ totalChars: number;
16
+ lastWarningAt: number;
17
+ }, thresholds?: {
18
+ promptThreshold?: number;
19
+ charsThreshold?: number;
20
+ cooldownMs?: number;
21
+ }): boolean;
22
+ /** 경고 메시지 생성 (순수 함수) */
23
+ export declare function buildContextWarningMessage(promptCount: number, totalChars: number): string;
24
+ export declare function main(): Promise<void>;
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forgen — Context Guard Hook
4
+ *
5
+ * Claude Code Stop 훅으로 등록.
6
+ * context window limit, edit error 등 실행 중 에러를 감지하여
7
+ * 사용자에게 경고하고 상태를 보존합니다.
8
+ *
9
+ * 또한 UserPromptSubmit에서 현재 대화 길이를 추적하여
10
+ * context 한계에 접근 시 preemptive 경고를 제공합니다.
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { createLogger } from '../core/logger.js';
16
+ import { readStdinJSON } from './shared/read-stdin.js';
17
+ import { atomicWriteJSON } from './shared/atomic-write.js';
18
+ import { loadHookConfig, isHookEnabled } from './hook-config.js';
19
+ import { approve, approveWithContext, approveWithWarning, failOpen } from './shared/hook-response.js';
20
+ import { HANDOFFS_DIR, STATE_DIR } from '../core/paths.js';
21
+ const log = createLogger('context-guard');
22
+ const CONTEXT_STATE_PATH = path.join(STATE_DIR, 'context-guard.json');
23
+ // 경고 임계값: 프롬프트 50회 또는 총 문자 수 200K 이상
24
+ const PROMPT_WARNING_THRESHOLD = 50;
25
+ const CHARS_WARNING_THRESHOLD = 200_000;
26
+ const WARNING_COOLDOWN_MS = 10 * 60 * 1000; // 10분 쿨다운
27
+ /** 경고 표시 여부 판정 (순수 함수) */
28
+ export function shouldWarn(contextPercent, thresholds = {}) {
29
+ const promptThreshold = thresholds.promptThreshold ?? PROMPT_WARNING_THRESHOLD;
30
+ const charsThreshold = thresholds.charsThreshold ?? CHARS_WARNING_THRESHOLD;
31
+ const cooldownMs = thresholds.cooldownMs ?? WARNING_COOLDOWN_MS;
32
+ const now = Date.now();
33
+ return ((contextPercent.promptCount >= promptThreshold || contextPercent.totalChars >= charsThreshold) &&
34
+ (now - contextPercent.lastWarningAt > cooldownMs));
35
+ }
36
+ /** 경고 메시지 생성 (순수 함수) */
37
+ export function buildContextWarningMessage(promptCount, totalChars) {
38
+ return `<compound-context-warning>\n[Forgen] Context limit approaching: ${promptCount} prompts, ${Math.round(totalChars / 1000)}K characters.\nIf you have important progress, save it now:\n- Use cancelforgen to reset mode state and start a new session\n- Or continue current work (auto compaction may occur)\n</compound-context-warning>`;
39
+ }
40
+ function loadContextState(sessionId) {
41
+ try {
42
+ if (fs.existsSync(CONTEXT_STATE_PATH)) {
43
+ const data = JSON.parse(fs.readFileSync(CONTEXT_STATE_PATH, 'utf-8'));
44
+ if (data.sessionId === sessionId)
45
+ return data;
46
+ }
47
+ }
48
+ catch (e) {
49
+ log.debug('context state 파일 읽기/파싱 실패', e);
50
+ }
51
+ return { promptCount: 0, totalChars: 0, lastWarningAt: 0, sessionId };
52
+ }
53
+ function saveContextState(state) {
54
+ atomicWriteJSON(CONTEXT_STATE_PATH, state);
55
+ }
56
+ export async function main() {
57
+ const input = await readStdinJSON();
58
+ if (!isHookEnabled('context-guard')) {
59
+ console.log(approve());
60
+ return;
61
+ }
62
+ if (!input) {
63
+ console.log(approve());
64
+ return;
65
+ }
66
+ const sessionId = input.session_id ?? 'default';
67
+ // Stop 훅: stop_hook_type이 있으면 처리
68
+ if (input.stop_hook_type) {
69
+ // 에러가 포함된 경우: context limit 감지
70
+ if (input.error) {
71
+ const errorMsg = input.error;
72
+ if (/context.*limit|token.*limit|conversation.*too.*long/i.test(errorMsg)) {
73
+ saveHandoff(sessionId, 'context-limit', errorMsg);
74
+ console.log(approveWithWarning(`[Forgen] Context limit reached. Current state has been saved to ~/.forgen/handoffs/.\nThe previous work will be automatically recovered in the next session.`));
75
+ return;
76
+ }
77
+ }
78
+ // 정상 종료 시: 의미 있는 세션이었으면 compound 안내
79
+ if (input.stop_hook_type === 'user' || input.stop_hook_type === 'end_turn') {
80
+ const state = loadContextState(sessionId);
81
+ if (state.promptCount >= 10) {
82
+ // 10 프롬프트 이상이면 의미 있는 세션 — compound 안내
83
+ console.log(approveWithWarning(`[Forgen] 이 세션에서 ${state.promptCount}개의 프롬프트를 처리했습니다. /compound 를 실행하면 이 세션의 학습 내용을 축적할 수 있습니다.`));
84
+ return;
85
+ }
86
+ }
87
+ console.log(approve());
88
+ return;
89
+ }
90
+ // error만 있는 경우 (stop_hook_type 없이)
91
+ if (input.error) {
92
+ console.log(approve());
93
+ return;
94
+ }
95
+ // UserPromptSubmit 훅: 대화 길이 추적
96
+ if (input.prompt) {
97
+ const config = loadHookConfig('context-guard');
98
+ // maxTokens가 설정되어 있으면 chars threshold로 사용 (토큰 ≈ 4자 기준 환산)
99
+ const charsThreshold = typeof config?.maxTokens === 'number' ? config.maxTokens * 4 : undefined;
100
+ const state = loadContextState(sessionId);
101
+ state.promptCount++;
102
+ state.totalChars += input.prompt.length;
103
+ if (shouldWarn(state, charsThreshold !== undefined ? { charsThreshold } : {})) {
104
+ state.lastWarningAt = Date.now();
105
+ saveContextState(state);
106
+ console.log(approveWithContext(buildContextWarningMessage(state.promptCount, state.totalChars), 'UserPromptSubmit'));
107
+ return;
108
+ }
109
+ saveContextState(state);
110
+ }
111
+ console.log(approve());
112
+ }
113
+ function saveHandoff(sessionId, reason, detail) {
114
+ fs.mkdirSync(HANDOFFS_DIR, { recursive: true });
115
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
116
+ const handoffPath = path.join(HANDOFFS_DIR, `${timestamp}-${reason}.md`);
117
+ // 활성 모드 상태 수집
118
+ const stateDir = STATE_DIR;
119
+ const activeStates = [];
120
+ if (fs.existsSync(stateDir)) {
121
+ for (const f of fs.readdirSync(stateDir)) {
122
+ if (f.endsWith('-state.json') && !f.startsWith('skill-cache-') && !f.startsWith('context-guard')) {
123
+ try {
124
+ const data = JSON.parse(fs.readFileSync(path.join(stateDir, f), 'utf-8'));
125
+ if (data.active) {
126
+ activeStates.push(`- ${f.replace('-state.json', '')}: ${data.prompt ?? 'no prompt'}`);
127
+ }
128
+ }
129
+ catch (e) {
130
+ log.debug(`상태 파일 파싱 실패: ${f}`, e);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ const content = [
136
+ `# Handoff: ${reason}`,
137
+ `- Session: ${sessionId}`,
138
+ `- Time: ${new Date().toISOString()}`,
139
+ `- Reason: ${detail}`,
140
+ '',
141
+ '## Active Modes',
142
+ activeStates.length > 0 ? activeStates.join('\n') : '- none',
143
+ '',
144
+ '## Recovery Instructions',
145
+ 'Automatically recovered in the next session (session-recovery hook).',
146
+ 'Manual recovery: Check the last state of the previous work and continue from there.',
147
+ ].join('\n');
148
+ fs.writeFileSync(handoffPath, content);
149
+ }
150
+ // ESM main guard: import 시 main() 실행 방지
151
+ if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
152
+ main().catch((e) => {
153
+ process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
154
+ console.log(failOpen());
155
+ });
156
+ }
@@ -0,0 +1,18 @@
1
+ [
2
+ { "pattern": "rm\\s+(-rf|-fr)\\s+[/~]", "description": "rm -rf on root/home path", "severity": "block" },
3
+ { "pattern": "rm\\s+(-rf|-fr)\\s+\\.\\s", "description": "rm -rf on current directory", "severity": "block" },
4
+ { "pattern": "git\\s+push\\s+.*--force(?!-)", "description": "git push --force", "severity": "warn" },
5
+ { "pattern": "git\\s+reset\\s+--hard", "description": "git reset --hard", "severity": "warn" },
6
+ { "pattern": "git\\s+clean\\s+-[a-z]*f", "description": "git clean -f", "severity": "warn" },
7
+ { "pattern": "drop\\s+(?:table|database)", "description": "DROP TABLE/DATABASE", "severity": "block", "flags": "i" },
8
+ { "pattern": "truncate\\s+table", "description": "TRUNCATE TABLE", "severity": "warn", "flags": "i" },
9
+ { "pattern": ">\\s*\\/dev\\/sd[a-z]", "description": "write to block device", "severity": "block" },
10
+ { "pattern": "mkfs\\s", "description": "mkfs (format filesystem)", "severity": "block" },
11
+ { "pattern": ":\\(\\)\\s*\\{\\s*:\\|:&\\s*\\}\\s*;:", "description": "fork bomb", "severity": "block" },
12
+ { "pattern": "\\beval\\s+[\"'`]", "description": "eval with string (injection risk)", "severity": "warn" },
13
+ { "pattern": "curl\\s+.*\\|\\s*(ba)?sh", "description": "curl pipe to shell", "severity": "block" },
14
+ { "pattern": "wget\\s+.*\\|\\s*(ba)?sh", "description": "wget pipe to shell", "severity": "block" },
15
+ { "pattern": "python[23]?\\s+-c\\s+['\"].*(?:import\\s+os|subprocess|exec|eval)", "description": "python -c with dangerous imports", "severity": "warn" },
16
+ { "pattern": "\\bchmod\\s+[0-7]*777\\b", "description": "chmod 777 (overly permissive)", "severity": "warn" },
17
+ { "pattern": "\\bdd\\s+.*of=\\/dev\\/", "description": "dd write to device", "severity": "block" }
18
+ ]