@wooojin/forgen 0.4.0 → 0.4.3

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 (187) hide show
  1. package/.claude-plugin/plugin.json +5 -5
  2. package/CHANGELOG.md +194 -15
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.ja.md +74 -9
  5. package/README.ko.md +77 -12
  6. package/README.md +127 -25
  7. package/README.zh.md +43 -9
  8. package/assets/README.md +86 -0
  9. package/assets/architecture.svg +100 -0
  10. package/assets/banner.png +0 -0
  11. package/assets/banner.svg +53 -0
  12. package/assets/demo/01-install.gif +0 -0
  13. package/assets/demo/01-install.tape +54 -0
  14. package/assets/demo/02-compound-learning.gif +0 -0
  15. package/assets/demo/02-compound-learning.tape +50 -0
  16. package/assets/demo/03-forge-personalization.gif +0 -0
  17. package/assets/demo/03-forge-personalization.tape +64 -0
  18. package/assets/demo/before-after.gif +0 -0
  19. package/assets/demo/before-after.tape +98 -0
  20. package/assets/demo-preview.svg +96 -0
  21. package/assets/icon.png +0 -0
  22. package/{hooks → assets/shared}/hook-registry.json +2 -1
  23. package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
  24. package/dist/checks/conclusion-verification-ratio.js +86 -0
  25. package/dist/checks/fact-vs-agreement.d.ts +47 -0
  26. package/dist/checks/fact-vs-agreement.js +92 -0
  27. package/dist/checks/self-score-deflation.d.ts +38 -0
  28. package/dist/checks/self-score-deflation.js +108 -0
  29. package/dist/cli.js +98 -6
  30. package/dist/core/auto-compound-runner.js +137 -49
  31. package/dist/core/behavior-classifier.d.ts +28 -0
  32. package/dist/core/behavior-classifier.js +46 -0
  33. package/dist/core/dashboard.d.ts +7 -0
  34. package/dist/core/dashboard.js +41 -2
  35. package/dist/core/doctor.js +118 -5
  36. package/dist/core/extraction-notice.d.ts +18 -0
  37. package/dist/core/extraction-notice.js +64 -0
  38. package/dist/core/git-stats.d.ts +36 -0
  39. package/dist/core/git-stats.js +79 -0
  40. package/dist/core/harness.d.ts +1 -1
  41. package/dist/core/harness.js +27 -20
  42. package/dist/core/host-detect.d.ts +42 -0
  43. package/dist/core/host-detect.js +68 -0
  44. package/dist/core/init-cli.d.ts +26 -0
  45. package/dist/core/init-cli.js +104 -0
  46. package/dist/core/init.js +17 -0
  47. package/dist/core/inspect-cli.js +1 -2
  48. package/dist/core/installer.js +2 -2
  49. package/dist/core/migrate-cli.d.ts +11 -0
  50. package/dist/core/migrate-cli.js +53 -0
  51. package/dist/core/migrate-evidence-host.d.ts +36 -0
  52. package/dist/core/migrate-evidence-host.js +49 -0
  53. package/dist/core/paths.d.ts +8 -1
  54. package/dist/core/paths.js +11 -2
  55. package/dist/core/recall-cli.d.ts +26 -0
  56. package/dist/core/recall-cli.js +125 -0
  57. package/dist/core/recall-reference-detector.d.ts +43 -0
  58. package/dist/core/recall-reference-detector.js +65 -0
  59. package/dist/core/settings-injector.js +4 -2
  60. package/dist/core/spawn.d.ts +1 -1
  61. package/dist/core/spawn.js +4 -11
  62. package/dist/core/stats-cli.d.ts +21 -0
  63. package/dist/core/stats-cli.js +133 -10
  64. package/dist/core/trust-layer-intent.d.ts +35 -0
  65. package/dist/core/trust-layer-intent.js +30 -0
  66. package/dist/core/types.d.ts +1 -1
  67. package/dist/core/uninstall.js +2 -1
  68. package/dist/engine/compound-cli.js +1 -0
  69. package/dist/engine/compound-export.js +8 -3
  70. package/dist/engine/compound-extractor.js +7 -9
  71. package/dist/engine/learn-cli.js +5 -6
  72. package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
  73. package/dist/engine/lifecycle/bypass-detector.js +57 -5
  74. package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
  75. package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
  76. package/dist/engine/lifecycle/orchestrator.js +2 -2
  77. package/dist/engine/lifecycle/signals.js +6 -6
  78. package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
  79. package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
  80. package/dist/engine/skill-promoter.js +3 -6
  81. package/dist/fgx.js +2 -1
  82. package/dist/forge/evidence-processor.js +12 -0
  83. package/dist/forge/onboarding.d.ts +3 -2
  84. package/dist/forge/onboarding.js +3 -2
  85. package/dist/hooks/context-guard.js +1 -1
  86. package/dist/hooks/dangerous-patterns.json +3 -3
  87. package/dist/hooks/db-guard.js +21 -5
  88. package/dist/hooks/forge-loop-progress.d.ts +9 -0
  89. package/dist/hooks/forge-loop-progress.js +38 -0
  90. package/dist/hooks/hook-registry.js +1 -1
  91. package/dist/hooks/hooks-generator.d.ts +15 -1
  92. package/dist/hooks/hooks-generator.js +18 -16
  93. package/dist/hooks/intent-classifier.js +1 -1
  94. package/dist/hooks/keyword-detector.js +2 -2
  95. package/dist/hooks/notepad-injector.js +1 -1
  96. package/dist/hooks/permission-handler.js +1 -1
  97. package/dist/hooks/post-tool-failure.js +1 -1
  98. package/dist/hooks/post-tool-use.d.ts +7 -1
  99. package/dist/hooks/post-tool-use.js +50 -23
  100. package/dist/hooks/pre-compact.js +2 -2
  101. package/dist/hooks/pre-tool-use.d.ts +7 -0
  102. package/dist/hooks/pre-tool-use.js +28 -10
  103. package/dist/hooks/rate-limiter.js +3 -3
  104. package/dist/hooks/secret-filter.js +1 -1
  105. package/dist/hooks/session-recovery.js +12 -1
  106. package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
  107. package/dist/hooks/shared/blocking-allowlist.js +38 -0
  108. package/dist/hooks/shared/command-parser.d.ts +44 -0
  109. package/dist/hooks/shared/command-parser.js +50 -0
  110. package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
  111. package/dist/hooks/shared/forge-loop-state.js +116 -0
  112. package/dist/hooks/shared/hook-response.d.ts +30 -2
  113. package/dist/hooks/shared/hook-response.js +61 -3
  114. package/dist/hooks/skill-injector.js +2 -2
  115. package/dist/hooks/slop-detector.js +2 -2
  116. package/dist/hooks/solution-injector.d.ts +9 -0
  117. package/dist/hooks/solution-injector.js +48 -5
  118. package/dist/hooks/stop-guard.js +152 -13
  119. package/dist/hooks/subagent-tracker.js +1 -1
  120. package/dist/host/capabilities-claude.d.ts +8 -0
  121. package/dist/host/capabilities-claude.js +46 -0
  122. package/dist/host/capabilities-codex.d.ts +11 -0
  123. package/dist/host/capabilities-codex.js +50 -0
  124. package/dist/host/capabilities-registry.d.ts +11 -0
  125. package/dist/host/capabilities-registry.js +30 -0
  126. package/dist/host/codex-adapter.d.ts +8 -5
  127. package/dist/host/codex-adapter.js +10 -82
  128. package/dist/host/codex-output-parser.d.ts +39 -0
  129. package/dist/host/codex-output-parser.js +75 -0
  130. package/dist/host/exec-host.d.ts +54 -0
  131. package/dist/host/exec-host.js +92 -0
  132. package/dist/host/host-runtime.d.ts +37 -0
  133. package/dist/host/host-runtime.js +51 -0
  134. package/dist/host/install-claude.d.ts +35 -0
  135. package/dist/host/install-claude.js +238 -0
  136. package/dist/host/install-codex.d.ts +44 -0
  137. package/dist/host/install-codex.js +276 -0
  138. package/dist/host/install-orchestrator.d.ts +34 -0
  139. package/dist/host/install-orchestrator.js +126 -0
  140. package/dist/host/invoke-agent.d.ts +27 -0
  141. package/dist/host/invoke-agent.js +115 -0
  142. package/dist/host/parity-harness.d.ts +62 -0
  143. package/dist/host/parity-harness.js +283 -0
  144. package/dist/host/projection.d.ts +35 -0
  145. package/dist/host/projection.js +126 -0
  146. package/dist/i18n/index.js +3 -5
  147. package/dist/mcp/server.js +11 -0
  148. package/dist/mcp/tools.js +47 -0
  149. package/dist/services/session.d.ts +6 -3
  150. package/dist/services/session.js +33 -4
  151. package/dist/store/evidence-store.d.ts +1 -0
  152. package/dist/store/evidence-store.js +45 -3
  153. package/dist/store/host-mismatch.d.ts +42 -0
  154. package/dist/store/host-mismatch.js +65 -0
  155. package/dist/store/implicit-feedback-store.d.ts +59 -0
  156. package/dist/store/implicit-feedback-store.js +153 -0
  157. package/dist/store/profile-store.d.ts +29 -0
  158. package/dist/store/profile-store.js +53 -0
  159. package/dist/store/rule-store.js +8 -0
  160. package/dist/store/types.d.ts +13 -0
  161. package/hooks/hooks.json +6 -1
  162. package/package.json +7 -5
  163. package/plugin.json +4 -4
  164. package/scripts/postinstall.js +100 -25
  165. /package/{agents → assets/claude/agents}/analyst.md +0 -0
  166. /package/{agents → assets/claude/agents}/architect.md +0 -0
  167. /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
  168. /package/{agents → assets/claude/agents}/critic.md +0 -0
  169. /package/{agents → assets/claude/agents}/debugger.md +0 -0
  170. /package/{agents → assets/claude/agents}/designer.md +0 -0
  171. /package/{agents → assets/claude/agents}/executor.md +0 -0
  172. /package/{agents → assets/claude/agents}/explore.md +0 -0
  173. /package/{agents → assets/claude/agents}/git-master.md +0 -0
  174. /package/{agents → assets/claude/agents}/planner.md +0 -0
  175. /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
  176. /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
  177. /package/{agents → assets/claude/agents}/verifier.md +0 -0
  178. /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
  179. /package/{commands → assets/claude/commands}/calibrate.md +0 -0
  180. /package/{commands → assets/claude/commands}/code-review.md +0 -0
  181. /package/{commands → assets/claude/commands}/compound.md +0 -0
  182. /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
  183. /package/{commands → assets/claude/commands}/docker.md +0 -0
  184. /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
  185. /package/{commands → assets/claude/commands}/learn.md +0 -0
  186. /package/{commands → assets/claude/commands}/retro.md +0 -0
  187. /package/{commands → assets/claude/commands}/ship.md +0 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Forgen v0.4.1 — TEST-2: 자가 점수 인플레이션 가드
3
+ *
4
+ * Claude 가 자신의 작업 품질/확신도/완성도를 **숫자**로 상향 선언하면서 해당
5
+ * 턴(또는 세션)에 측정 도구 호출이 0 건이면 block. TEST-1 (사실 vs 합의) 보다
6
+ * 강한 신호 — 구체적 숫자 인플레이션은 합의-기반 자기-아부(sycophancy)의
7
+ * 가장 또렷한 표식.
8
+ *
9
+ * 배경 (RC2): v0.4.0 self-interview 에서 "8/10", "신뢰도 90%", "0.85 → 0.95"
10
+ * 같은 자가 점수가 턴마다 올라갔지만 `npm test` / `curl` / `Read` 등 실제
11
+ * 측정 호출은 0건. TEST-1 이 서술체 사실 주장을 잡았다면, TEST-2 는 **숫자**
12
+ * 점수의 인플레이션에 초점을 맞춘다.
13
+ *
14
+ * 순수 함수 — Stop hook block 경로에 붙는다.
15
+ */
16
+ /**
17
+ * 측정성 도구 — **숫자 점수**를 뒷받침할 수 있는 실 **실행** 범주.
18
+ *
19
+ * v0.4.1 coverage fix (2026-04-24 buyer-day1 R4 관찰): 이전에는 Read/Edit/Write/
20
+ * Grep/Glob 도 측정으로 간주했으나, 파일 "읽기/수정" 은 "신뢰도 95/100" 같은
21
+ * 수치 판정을 뒷받침 못 함. Read 1회면 minMeasurements=1 충족되어 block 회피.
22
+ * 실제 구매자 시나리오: Claude 가 자가평가 전에 대상 파일 한 번 Read 하면
23
+ * TEST-2 무력화 — 가드의 본 의도 훼손.
24
+ *
25
+ * 새 기준: **실행 결과** 만 측정 — `Bash` (npm test / curl / node --check 등)
26
+ * 와 `NotebookEdit` (cell 실행). 읽기 전용 도구는 수치 점수의 근거가 될 수 없음.
27
+ */
28
+ const MEASUREMENT_TOOLS = new Set([
29
+ 'Bash',
30
+ 'NotebookEdit',
31
+ ]);
32
+ /**
33
+ * "자가 점수" 신호 — 숫자 + 품질/완성도/확신도 컨텍스트.
34
+ * - "신뢰도 90%", "품질 점수 85/100", "확신도 0.9", "8/10", "90점"
35
+ * - "0.7 → 0.9" 같은 증감 표기
36
+ *
37
+ * 이 regex 들은 *숫자 그 자체* 만 매칭하지 않고 품질-관련 명사와 같이 나타날 때만
38
+ * 매칭하도록 좁힘 (false positive 방지).
39
+ */
40
+ const SELF_SCORE_PATTERNS = [
41
+ // "신뢰도 90%" / "quality 85%" / "확신도 0.9"
42
+ /(신뢰도|확신도|완성도|품질|자신감|confidence|quality|completeness)[\s::]*(\d+(?:\.\d+)?)\s*(%|점|\/\s*\d+|\/100|\/10)?/gi,
43
+ // "0.85 → 0.95" / "7 -> 9" score delta
44
+ /(\d+(?:\.\d+)?)\s*(?:→|->|–>|~>)\s*(\d+(?:\.\d+)?)/g,
45
+ // "8/10", "85/100" — 단독 분수 점수 (앞뒤 품질 컨텍스트 확인은 하지 않지만 보수적 매칭)
46
+ /\b(\d+(?:\.\d+)?)\s*\/\s*(10|100)\b/g,
47
+ // 별 점수 "⭐⭐⭐⭐" 4개 이상
48
+ /⭐{4,}/g,
49
+ ];
50
+ function extractDeltas(text) {
51
+ const re = /(\d+(?:\.\d+)?)\s*(?:→|->|–>|~>)\s*(\d+(?:\.\d+)?)/g;
52
+ const out = [];
53
+ let m;
54
+ while ((m = re.exec(text)) !== null) {
55
+ const from = Number(m[1]);
56
+ const to = Number(m[2]);
57
+ if (Number.isFinite(from) && Number.isFinite(to))
58
+ out.push({ from, to });
59
+ }
60
+ return out;
61
+ }
62
+ function findScoreSignals(text, max = 3) {
63
+ const out = [];
64
+ for (const p of SELF_SCORE_PATTERNS) {
65
+ if (out.length >= max)
66
+ break;
67
+ // 각 호출마다 lastIndex 초기화를 위해 새 RegExp 생성
68
+ const re = new RegExp(p.source, p.flags);
69
+ let m;
70
+ while ((m = re.exec(text)) !== null && out.length < max) {
71
+ out.push(m[0]);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ export function checkSelfScoreInflation(input) {
77
+ const minDelta = input.minDelta ?? 0;
78
+ const minMeasurements = input.minMeasurements ?? 1;
79
+ const scoreSignals = findScoreSignals(input.text);
80
+ const allDeltas = extractDeltas(input.text);
81
+ const positiveDeltas = allDeltas.filter((d) => d.to - d.from > minDelta);
82
+ const measurementCount = input.recentTools.filter((t) => MEASUREMENT_TOOLS.has(t)).length;
83
+ const measurementMissing = measurementCount < minMeasurements;
84
+ // 인플레이션 신호가 하나라도 있고 측정이 없으면 block
85
+ const hasInflationSignal = scoreSignals.length > 0 || positiveDeltas.length > 0;
86
+ const block = hasInflationSignal && measurementMissing;
87
+ let reason = '';
88
+ if (block) {
89
+ const parts = [];
90
+ if (positiveDeltas.length > 0) {
91
+ const sample = positiveDeltas.slice(0, 2).map((d) => `${d.from}→${d.to}`).join(', ');
92
+ parts.push(`자가 점수 상승 선언 ${positiveDeltas.length}건 (${sample})`);
93
+ }
94
+ if (scoreSignals.length > 0) {
95
+ parts.push(`점수 표현 ${scoreSignals.length}건 ("${scoreSignals[0]}")`);
96
+ }
97
+ parts.push(`측정 도구 호출 ${measurementCount}회 (< ${minMeasurements}) — 숫자 변동을 뒷받침할 실행/확인 증거 없음`);
98
+ parts.push('block: 테스트/빌드/curl 실행 결과를 턴에 포함하여 재응답');
99
+ reason = parts.join('. ');
100
+ }
101
+ return {
102
+ block,
103
+ scoreSignals,
104
+ deltas: positiveDeltas,
105
+ measurementCount,
106
+ reason,
107
+ };
108
+ }
package/dist/cli.js CHANGED
@@ -112,8 +112,31 @@ const commands = [
112
112
  await displayHookStatus(process.cwd());
113
113
  }
114
114
  }
115
+ else if (sub === 'default-host') {
116
+ const value = args[1];
117
+ const valid = new Set(['claude', 'codex', 'ask']);
118
+ if (value === undefined) {
119
+ const { getDefaultHost } = await import('./store/profile-store.js');
120
+ const current = getDefaultHost();
121
+ console.log(` current default_host: ${current ?? '(unset → claude fallback)'}`);
122
+ console.log(' Usage: forgen config default-host {claude|codex|ask}');
123
+ }
124
+ else if (!valid.has(value)) {
125
+ console.log(` Invalid value: ${value}. Use one of: claude, codex, ask`);
126
+ process.exit(1);
127
+ }
128
+ else {
129
+ const { setDefaultHost } = await import('./store/profile-store.js');
130
+ const ok = setDefaultHost(value);
131
+ if (!ok) {
132
+ console.log(' ✗ Profile not found. Run `forgen onboarding` first.');
133
+ process.exit(1);
134
+ }
135
+ console.log(` ✓ default_host set to: ${value}`);
136
+ }
137
+ }
115
138
  else {
116
- console.log('Usage: forgen config hooks [--regenerate]');
139
+ console.log('Usage:\n forgen config hooks [--regenerate]\n forgen config default-host [claude|codex|ask]');
117
140
  }
118
141
  },
119
142
  },
@@ -133,6 +156,53 @@ const commands = [
133
156
  await handleInit(args);
134
157
  },
135
158
  },
159
+ {
160
+ name: 'install',
161
+ description: 'Install forgen into a host. Usage: forgen install [claude|codex|both] [--dry-run] [--no-mcp]',
162
+ handler: async (args) => {
163
+ const knownSubs = new Set(['claude', 'codex', 'both']);
164
+ const target = args[0] && knownSubs.has(args[0]) ? args[0] : args[0]?.startsWith('--') ? undefined : args[0];
165
+ if (target !== undefined && !knownSubs.has(target)) {
166
+ console.log('Usage:\n forgen install [claude|codex|both] [--dry-run] [--no-mcp]\n\n No arg → interactive 3-choice (Claude/Codex/Both).');
167
+ return;
168
+ }
169
+ const dryRun = args.includes('--dry-run');
170
+ const registerMcp = !args.includes('--no-mcp');
171
+ const { runInstall, renderResult, resolvePkgRootFromBinary } = await import('./host/install-orchestrator.js');
172
+ const pkgRoot = resolvePkgRootFromBinary(import.meta.url);
173
+ const result = await runInstall({ target, pkgRoot, dryRun, registerMcp });
174
+ if (result === null) {
175
+ console.log('\n [forgen] Install skipped.');
176
+ return;
177
+ }
178
+ console.log(renderResult(result, dryRun));
179
+ },
180
+ },
181
+ {
182
+ name: 'parity',
183
+ description: 'Run host parity checks. Usage: forgen parity codex [--dry-run]',
184
+ handler: async (args) => {
185
+ const sub = args[0];
186
+ if (sub !== 'codex') {
187
+ console.log('Usage:\n forgen parity codex [--dry-run]\n\nNotes:\n - source 체크아웃에서만 작동합니다 (tests/ 디렉토리 필요).\n - npm install 로 설치된 패키지에서는 run-parity.sh 가 없습니다.');
188
+ return;
189
+ }
190
+ const here = path.dirname(new URL(import.meta.url).pathname);
191
+ const scriptPath = path.resolve(here, '..', 'tests', 'e2e', 'codex', 'run-parity.sh');
192
+ if (!fs.existsSync(scriptPath)) {
193
+ console.error('[forgen] run-parity.sh 는 source 체크아웃에서만 작동. 직접 git clone 후 실행하세요.');
194
+ console.error(` expected: ${scriptPath}`);
195
+ process.exit(1);
196
+ }
197
+ const { spawnSync } = await import('node:child_process');
198
+ const dryRun = args.includes('--dry-run');
199
+ const spawnArgs = dryRun ? ['--dry-run'] : [];
200
+ const result = spawnSync('bash', [scriptPath, ...spawnArgs], { stdio: 'inherit' });
201
+ if (result.status !== 0) {
202
+ process.exit(result.status ?? 1);
203
+ }
204
+ },
205
+ },
136
206
  {
137
207
  name: 'notepad',
138
208
  description: 'Notepad (show|add|clear)',
@@ -151,7 +221,7 @@ const commands = [
151
221
  },
152
222
  {
153
223
  name: 'onboarding',
154
- description: 'v1 2-question onboarding flow',
224
+ description: 'v1 4-question onboarding flow',
155
225
  handler: async (_args) => {
156
226
  const { runOnboarding } = await import('./forge/onboarding-cli.js');
157
227
  await runOnboarding();
@@ -226,6 +296,22 @@ const commands = [
226
296
  await handleInspect(['violations', '--last', '1']);
227
297
  },
228
298
  },
299
+ {
300
+ name: 'recall',
301
+ description: 'Show recent compound recalls (matched solutions) with optional body preview.',
302
+ handler: async (args) => {
303
+ const { handleRecall } = await import('./core/recall-cli.js');
304
+ await handleRecall(args);
305
+ },
306
+ },
307
+ {
308
+ name: 'migrate',
309
+ description: 'One-shot schema migrations (implicit-feedback category backfill).',
310
+ handler: async (args) => {
311
+ const { handleMigrate } = await import('./core/migrate-cli.js');
312
+ await handleMigrate(args);
313
+ },
314
+ },
229
315
  {
230
316
  name: 'suppress-rule',
231
317
  description: '[alias: rule suppress] Disable a rule by id/prefix. Hard rules refused.',
@@ -391,7 +477,8 @@ async function main() {
391
477
  ${dim}Code, forged for you.${reset}
392
478
  ${dim}Scope: v1(${context.v1.session?.quality_pack ?? 'onboarding needed'})${reset}
393
479
  `);
394
- const runtimeLabel = runtime === 'codex' ? 'Codex' : 'Claude';
480
+ const { getHostRuntime } = await import('./host/host-runtime.js');
481
+ const runtimeLabel = getHostRuntime(runtime).displayName;
395
482
  console.log(`[forgen] Starting ${runtimeLabel}...\n`);
396
483
  await spawnClaudeWithResume(args, context, () => prepareHarness(process.cwd(), { runtime }), runtime);
397
484
  }
@@ -425,17 +512,22 @@ function printHelp() {
425
512
 
426
513
  Commands:
427
514
  forgen forge Personalize your coding profile
428
- forgen onboarding Run 2-question onboarding
515
+ forgen onboarding Run 4-question onboarding
429
516
  forgen inspect [profile|rules|corrections|session]
430
517
  Inspect v1 state (alias: evidence → corrections)
431
518
  forgen rule <list|suppress|activate|scan|health-scan|classify>
432
519
  Rule management (see: forgen rule help)
433
- forgen stats One-screen trust-layer dashboard
520
+ forgen stats One-screen trust-layer dashboard (+ philosophy)
434
521
  forgen last-block Show the most recent block event
522
+ forgen recall [--limit N] [--show]
523
+ 최근 compound 주입 이력 (solution body preview)
524
+ forgen migrate [implicit-feedback|evidence-host|all]
525
+ One-shot schema migration (category backfill / host backfill)
526
+ forgen parity codex [--dry-run] Run codex parity checks (source checkout only)
435
527
  forgen compound Manage accumulated knowledge
436
528
  forgen dashboard Compound system dashboard
437
529
  forgen me Personal dashboard
438
- forgen init Initialize project
530
+ forgen init Initialize project (+ starter-pack solutions)
439
531
  forgen config hooks Hook management
440
532
  forgen mcp MCP server management
441
533
  forgen skill promote|list Skill management
@@ -11,47 +11,74 @@
11
11
  */
12
12
  import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
14
- import * as os from 'node:os';
15
14
  import { execFileSync } from 'node:child_process';
15
+ import { createRequire } from 'node:module';
16
16
  import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
17
17
  import { redactSecrets } from '../hooks/secret-filter.js';
18
18
  import { createEvidence, saveEvidence, promoteSessionCandidates } from '../store/evidence-store.js';
19
19
  import { loadProfile } from '../store/profile-store.js';
20
+ import { FORGEN_HOME, ME_DIR } from './paths.js';
21
+ import { classifyBehaviorKind, mapKindToAxisRefs } from './behavior-classifier.js';
20
22
  /** Auto-compound에 사용할 모델 — background 추출이므로 haiku로 충분 */
21
23
  const COMPOUND_MODEL = 'haiku';
22
- /** execFileSync wrapper: transient 에러(ETIMEDOUT 등) 시 1회 재시도 */
24
+ /**
25
+ * Host-aware exec retry — feat/codex-support P2-3 (Phase 2 critic fix).
26
+ *
27
+ * 보안 회귀 방지: Claude 분기는 *args 그대로* execFileSync 호출 → P1-S1 의
28
+ * `--allowedTools Bash(forgen compound:*)` sandbox hardening 보존.
29
+ * Codex 분기에서만 -p prompt 추출 → execHost (codex 는 --allowedTools 모름).
30
+ *
31
+ * Codex retry 정책 fix: ETIMEDOUT 시 sleep 후 retry 는 *Claude only*. Codex 는
32
+ * 60-90s response 가 정상이라 timeout 누적 retry 가 무의미 (즉시 fail).
33
+ */
23
34
  function execClaudeRetry(args, opts) {
24
- const TRANSIENT = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE/;
25
- for (let attempt = 0; attempt < 2; attempt++) {
26
- try {
27
- return execFileSync('claude', args, opts);
28
- }
29
- catch (e) {
30
- const msg = e instanceof Error ? e.message : String(e);
31
- if (attempt === 0 && TRANSIENT.test(msg)) {
32
- process.stderr.write(`[forgen-auto-compound] transient error, retrying in 3s...\n`);
33
- // Blocking synchronous sleep: Atomics.wait on a zero-initialized
34
- // SharedArrayBuffer is the Node.js idiom for blocking the event
35
- // loop without spawning child processes. This file runs as a
36
- // detached subprocess (`auto-compound-runner`) so blocking is
37
- // both safe and intentional. The 3000ms matches the backoff
38
- // before retry. Alternative setTimeout would require making this
39
- // function async, which would ripple through the entire runner.
40
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
41
- continue;
35
+ const mod = createRequire(import.meta.url)('../host/exec-host.js');
36
+ // profile.default_host host 결정 (lazy load)
37
+ const profileMod = createRequire(import.meta.url)('../store/profile-store.js');
38
+ const resolved = profileMod.resolveDefaultHost();
39
+ const host = resolved === 'codex' ? 'codex' : 'claude';
40
+ if (host === 'claude') {
41
+ // Claude 측은 기존 보안 hardening 보존: --allowedTools args 그대로 전달.
42
+ const TRANSIENT = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE/;
43
+ for (let attempt = 0; attempt < 2; attempt++) {
44
+ try {
45
+ return execFileSync('claude', args, opts);
46
+ }
47
+ catch (e) {
48
+ const msg = e instanceof Error ? e.message : String(e);
49
+ if (attempt === 0 && TRANSIENT.test(msg)) {
50
+ process.stderr.write(`[forgen-auto-compound] transient error, retrying in 3s...\n`);
51
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
52
+ continue;
53
+ }
54
+ throw e;
42
55
  }
43
- throw e;
44
56
  }
57
+ throw new Error('unreachable');
45
58
  }
46
- throw new Error('unreachable');
59
+ // host === 'codex' — prompt 만 추출 (codex 는 --allowedTools 등 미인식).
60
+ const pIdx = args.indexOf('-p');
61
+ if (pIdx === -1 || !args[pIdx + 1]) {
62
+ throw new Error('execClaudeRetry: codex host requires -p prompt argument');
63
+ }
64
+ const prompt = args[pIdx + 1];
65
+ const modelIdx = args.indexOf('--model');
66
+ const model = modelIdx !== -1 ? args[modelIdx + 1] : undefined;
67
+ const r = mod.execHost({
68
+ prompt,
69
+ model,
70
+ host: 'codex',
71
+ timeout: typeof opts.timeout === 'number' ? opts.timeout : 60000,
72
+ cwd: typeof opts.cwd === 'string' ? opts.cwd : undefined,
73
+ });
74
+ return r.message;
47
75
  }
48
76
  const [, , cwd, transcriptPath, sessionId] = process.argv;
49
77
  if (!cwd || !transcriptPath || !sessionId) {
50
78
  process.exit(1);
51
79
  }
52
- const FORGEN_HOME = path.join(os.homedir(), '.forgen');
53
- const SOLUTIONS_DIR = path.join(FORGEN_HOME, 'me', 'solutions');
54
- const BEHAVIOR_DIR = path.join(FORGEN_HOME, 'me', 'behavior');
80
+ const SOLUTIONS_DIR = path.join(ME_DIR, 'solutions');
81
+ const BEHAVIOR_DIR = path.join(ME_DIR, 'behavior');
55
82
  /** Lightweight quality gate for auto-extracted solution files */
56
83
  /** Toxicity patterns — code-context only to avoid false positives on prose */
57
84
  const SOLUTION_TOXICITY_PATTERNS = [/@ts-ignore/i, /:\s*any\b/, /\/\/\s*TODO\b/];
@@ -206,9 +233,7 @@ function mergeOrCreateBehavior(dir, newContent, kind, today) {
206
233
  fs.writeFileSync(filePath, updated);
207
234
  return true;
208
235
  }
209
- catch {
210
- continue;
211
- }
236
+ catch { }
212
237
  }
213
238
  return false;
214
239
  }
@@ -309,14 +334,15 @@ ${sanitizedSummary.slice(0, 6000)}
309
334
  관찰된 패턴을 다음 형식으로 1~3개만 출력해주세요 (없으면 "관찰된 패턴 없음"):
310
335
  - [카테고리] 패턴 설명 (관찰 근거)
311
336
 
312
- 카테고리: 커뮤니케이션/작업습관/기술선호/의사결정/워크플로우
337
+ 카테고리: 커뮤니케이션/작업습관/기술선호/의사결정/워크플로우/품질안전/자율성
313
338
 
314
- 특히 "워크플로우" 카테고리에 주목하세요:
315
- - 사용자가 반복하는 작업 순서 패턴 (예: "항상 테스트 먼저 작성 → 구현 → 리팩토링 순서로 진행")
316
- - 특정 상황에서의 판단 규칙 (예: "PR 리뷰 보안 테스트 코드 품질 순서로 확인")
317
- - 조건부 접근법 (예: "버그 수정 재현 테스트부터 작성, 성능 이슈면 프로파일링부터")
339
+ 카테고리 가이드:
340
+ - "워크플로우": 반복하는 작업 순서, 판단 규칙, 조건부 접근법 (예: "테스트 먼저 → 구현 → 리팩토링 순서")
341
+ - "품질안전": 검증/테스트/안전성 관련 강한 선호 (예: "프로덕션 배포 Docker e2e 의무", "mock-only 검증 거부")
342
+ - "자율성": 확인/독립 결정 관련 선호 (예: "사소한 변경은 묻지 않고 진행", "큰 결정은 반드시 확인")
318
343
 
319
344
  워크플로우 패턴이 감지되면 반드시 구체적인 순서를 포함하세요.
345
+ 품질안전/자율성 패턴은 4축 개인화의 입력이므로 quality/autonomy 신호가 명확하면 반드시 해당 라벨을 사용하세요 (커뮤니케이션/작업습관 으로 흡수 금지).
320
346
 
321
347
  기존 패턴과 중복이면 건너뛰세요.${existingBehaviorPatterns}
322
348
 
@@ -347,11 +373,11 @@ ${sanitizedSummary.slice(0, 4000)}
347
373
  fs.mkdirSync(BEHAVIOR_DIR, { recursive: true });
348
374
  const today = new Date().toISOString().split('T')[0];
349
375
  const trimmed = userResult.trim();
350
- // 카테고리에 따라 kind 분류
351
- const kind = trimmed.includes('[워크플로우]') || trimmed.includes('순서') || trimmed.includes('→')
352
- ? 'workflow'
353
- : trimmed.includes('[의사결정]') ? 'thinking'
354
- : 'preference';
376
+ // 카테고리에 따라 kind 분류 — D1'' (2026-04-27): quality/autonomy 라벨 추가.
377
+ // 이전 3분기(workflow/thinking/preference) quality_safety/autonomy 축으로
378
+ // 가는 자동 신호를 communication_style 로 흡수해 626건 중 자동 추출 0건이
379
+ // 이 두 축에 닿지 못했음. 5분기로 확장. (분류 로직은 behavior-classifier.ts)
380
+ const kind = classifyBehaviorKind(trimmed);
355
381
  // 기존 유사 패턴이 있으면 observedCount 누적
356
382
  const merged = mergeOrCreateBehavior(BEHAVIOR_DIR, trimmed, kind, today);
357
383
  if (!merged) {
@@ -368,10 +394,7 @@ ${sanitizedSummary.slice(0, 4000)}
368
394
  session_id: sessionId,
369
395
  source_component: 'auto-compound-runner',
370
396
  summary: trimmed.slice(0, 200),
371
- axis_refs: kind === 'workflow' ? ['judgment_philosophy']
372
- : kind === 'preference' ? ['communication_style']
373
- : kind === 'thinking' ? ['judgment_philosophy']
374
- : [],
397
+ axis_refs: mapKindToAxisRefs(kind),
375
398
  confidence: 0.6,
376
399
  raw_payload: { kind, observedCount: 1 },
377
400
  });
@@ -383,10 +406,8 @@ ${sanitizedSummary.slice(0, 4000)}
383
406
  }
384
407
  // 3단계: 세션 학습 요약 (SessionLearningSummary) 생성
385
408
  try {
386
- const FORGEN_HOME = path.join(os.homedir(), '.forgen');
387
- const V1_ME_DIR = path.join(FORGEN_HOME, 'me');
388
- const V1_PROFILE = path.join(V1_ME_DIR, 'forge-profile.json');
389
- const V1_EVIDENCE_DIR = path.join(V1_ME_DIR, 'behavior');
409
+ const V1_PROFILE = path.join(ME_DIR, 'forge-profile.json');
410
+ const V1_EVIDENCE_DIR = path.join(ME_DIR, 'behavior');
390
411
  if (fs.existsSync(V1_PROFILE)) {
391
412
  const currentProfile = loadProfile();
392
413
  let profileContext = '';
@@ -485,8 +506,9 @@ ${sanitizedSummary.slice(0, 4000)}
485
506
  process.stderr.write(`[forgen-auto-compound] session learning: ${e instanceof Error ? e.message : String(e)}\n`);
486
507
  }
487
508
  // Step 4: prefer-from-now / avoid-this 교정 → scope:'me' 영구 규칙 승격
509
+ let promotedCount = 0;
488
510
  try {
489
- const promotedCount = promoteSessionCandidates(sessionId);
511
+ promotedCount = promoteSessionCandidates(sessionId);
490
512
  if (promotedCount > 0) {
491
513
  process.stderr.write(`[forgen-auto-compound] promoted ${promotedCount} correction(s) to permanent rules\n`);
492
514
  }
@@ -494,6 +516,21 @@ ${sanitizedSummary.slice(0, 4000)}
494
516
  catch (e) {
495
517
  process.stderr.write(`[forgen-auto-compound] rule promotion: ${e instanceof Error ? e.message : String(e)}\n`);
496
518
  }
519
+ // H2: count newly extracted solutions (post-quality-gate) for Stop hook 알림.
520
+ // solutionsBefore 스냅샷 vs 현재 디스크 상태 차분 → "N개 패턴 학습됨" 1줄.
521
+ let extractedSolutionsCount = 0;
522
+ try {
523
+ if (fs.existsSync(SOLUTIONS_DIR)) {
524
+ const current = fs.readdirSync(SOLUTIONS_DIR).filter((f) => f.endsWith('.md'));
525
+ for (const f of current) {
526
+ if (!solutionsBefore.has(f))
527
+ extractedSolutionsCount++;
528
+ }
529
+ }
530
+ }
531
+ catch (e) {
532
+ process.stderr.write(`[forgen-auto-compound] solution count failed: ${e instanceof Error ? e.message : String(e)}\n`);
533
+ }
497
534
  // Step 5: meta-learning (HyperAgents-inspired self-tuning)
498
535
  try {
499
536
  const { runMetaLearning } = await import('../engine/meta-learning/runner.js');
@@ -508,10 +545,61 @@ ${sanitizedSummary.slice(0, 4000)}
508
545
  catch (e) {
509
546
  process.stderr.write(`[forgen-meta] ${e instanceof Error ? e.message : String(e)}\n`);
510
547
  }
511
- // 완료 기록
548
+ // Step 5.5 (v0.4.1): state hygiene — 세션 스코프 ephemeral 파일 7일 retention
549
+ // 자동 정리. 이전에는 `forgen doctor --prune-state` 수동만 있어서 injection-cache
550
+ // 2343 / modified-files 431 처럼 수천 파일 누적. 몇 달 사용하면 10만+ 파일 → stat
551
+ // 호출 느려지고 디스크 낭비. auto-compound 마다 호출되면 자연스레 정돈.
552
+ try {
553
+ const { pruneState } = await import('./state-gc.js');
554
+ const report = pruneState({ dryRun: false });
555
+ if (report.pruned > 0) {
556
+ const mb = (report.bytesFreed / 1024 / 1024).toFixed(2);
557
+ process.stderr.write(`[forgen-gc] pruned ${report.pruned} stale state files (${mb} MB freed)\n`);
558
+ }
559
+ }
560
+ catch (e) {
561
+ process.stderr.write(`[forgen-gc] state prune failed: ${e instanceof Error ? e.message : String(e)}\n`);
562
+ }
563
+ // Step 6 (v0.4.1): rule lifecycle 자동 실행 — rule 의 violations/bypass/drift
564
+ // 신호에 따른 자동 강등/승격. 이전에는 CLI (`forgen rule scan --apply`) 수동
565
+ // 호출만 있어서 구매자가 몇 주 써도 rule 정비 안 됨 → 쓸모없는 rule 이 계속
566
+ // active. 판매 관점 심각한 "자동 학습 단절". auto-compound-runner 끝에 자동
567
+ // 실행해 세션마다 rule 품질 유지.
568
+ try {
569
+ const { handleLifecycleScan } = await import('../engine/lifecycle/lifecycle-cli.js');
570
+ // silent mode 로 돌리기 위해 stdout 을 임시 리다이렉트 (내부가 console.log 씀)
571
+ const origLog = console.log;
572
+ let applied = 0;
573
+ console.log = (...args) => {
574
+ const msg = args.join(' ');
575
+ const match = msg.match(/apply(?:ied)?\s+(\d+)/i);
576
+ if (match)
577
+ applied = Number(match[1]);
578
+ };
579
+ try {
580
+ await handleLifecycleScan(['--apply']);
581
+ }
582
+ finally {
583
+ console.log = origLog;
584
+ }
585
+ if (applied > 0) {
586
+ process.stderr.write(`[forgen-meta] rule lifecycle: ${applied} event(s) applied\n`);
587
+ }
588
+ }
589
+ catch (e) {
590
+ process.stderr.write(`[forgen-meta] lifecycle scan failed: ${e instanceof Error ? e.message : String(e)}\n`);
591
+ }
592
+ // 완료 기록 — H2: Stop hook 알림용으로 extractedSolutions / promotedRules 포함.
593
+ // noticeShown=false 로 시작해서 Stop hook 가 최초 1회만 surface.
512
594
  const statePath = path.join(FORGEN_HOME, 'state', 'last-auto-compound.json');
513
595
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
514
- fs.writeFileSync(statePath, JSON.stringify({ sessionId, completedAt: new Date().toISOString() }));
596
+ fs.writeFileSync(statePath, JSON.stringify({
597
+ sessionId,
598
+ completedAt: new Date().toISOString(),
599
+ extractedSolutions: extractedSolutionsCount,
600
+ promotedRules: promotedCount,
601
+ noticeShown: false,
602
+ }));
515
603
  }
516
604
  catch (e) {
517
605
  process.stderr.write(`[forgen-auto-compound] ${e instanceof Error ? e.message : String(e)}\n`);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Behavior Classifier — D1'' (2026-04-27)
3
+ *
4
+ * LLM 이 추출한 사용자 패턴을 5개 kind 로 분류하고 4축 axis_refs 로 매핑한다.
5
+ *
6
+ * 결함 history:
7
+ * v0.4.1 까지: kind 3분기(workflow/thinking/preference) → axis 2축
8
+ * (judgment_philosophy / communication_style) 만 자동 추출 가능.
9
+ * quality_safety / autonomy 축은 explicit_correction 16건 (Hooks 경로) 으로만
10
+ * 자라고, 자동 학습 600+ 건은 이 두 축에 0% 기여 — 측정 자기증거.
11
+ *
12
+ * v0.4.2: 5분기 [품질안전] / [자율성] 추가 → 4축 모두 cover.
13
+ * LLM prompt (auto-compound-runner) 에도 같은 라벨 가이드를 명시하여
14
+ * 형식 강제. 새 라벨이 안 나오면 기존 5분기로 fallback (호환).
15
+ */
16
+ export type BehaviorKind = 'safety' | 'autonomy' | 'workflow' | 'thinking' | 'preference';
17
+ /**
18
+ * LLM 출력 텍스트(`[카테고리] 설명` 형식)를 5개 kind 로 분류.
19
+ *
20
+ * 라벨 우선순위 (위에서 아래):
21
+ * 1. [품질안전] → safety
22
+ * 2. [자율성] → autonomy
23
+ * 3. [워크플로우] OR "순서"/"→" 토큰 → workflow
24
+ * 4. [의사결정] → thinking
25
+ * 5. 그 외 → preference (default)
26
+ */
27
+ export declare function classifyBehaviorKind(text: string): BehaviorKind;
28
+ export declare function mapKindToAxisRefs(kind: BehaviorKind): string[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Behavior Classifier — D1'' (2026-04-27)
3
+ *
4
+ * LLM 이 추출한 사용자 패턴을 5개 kind 로 분류하고 4축 axis_refs 로 매핑한다.
5
+ *
6
+ * 결함 history:
7
+ * v0.4.1 까지: kind 3분기(workflow/thinking/preference) → axis 2축
8
+ * (judgment_philosophy / communication_style) 만 자동 추출 가능.
9
+ * quality_safety / autonomy 축은 explicit_correction 16건 (Hooks 경로) 으로만
10
+ * 자라고, 자동 학습 600+ 건은 이 두 축에 0% 기여 — 측정 자기증거.
11
+ *
12
+ * v0.4.2: 5분기 [품질안전] / [자율성] 추가 → 4축 모두 cover.
13
+ * LLM prompt (auto-compound-runner) 에도 같은 라벨 가이드를 명시하여
14
+ * 형식 강제. 새 라벨이 안 나오면 기존 5분기로 fallback (호환).
15
+ */
16
+ const AXIS_REFS_BY_KIND = {
17
+ safety: ['quality_safety'],
18
+ autonomy: ['autonomy'],
19
+ workflow: ['judgment_philosophy'],
20
+ thinking: ['judgment_philosophy'],
21
+ preference: ['communication_style'],
22
+ };
23
+ /**
24
+ * LLM 출력 텍스트(`[카테고리] 설명` 형식)를 5개 kind 로 분류.
25
+ *
26
+ * 라벨 우선순위 (위에서 아래):
27
+ * 1. [품질안전] → safety
28
+ * 2. [자율성] → autonomy
29
+ * 3. [워크플로우] OR "순서"/"→" 토큰 → workflow
30
+ * 4. [의사결정] → thinking
31
+ * 5. 그 외 → preference (default)
32
+ */
33
+ export function classifyBehaviorKind(text) {
34
+ if (text.includes('[품질안전]'))
35
+ return 'safety';
36
+ if (text.includes('[자율성]'))
37
+ return 'autonomy';
38
+ if (text.includes('[워크플로우]') || text.includes('순서') || text.includes('→'))
39
+ return 'workflow';
40
+ if (text.includes('[의사결정]'))
41
+ return 'thinking';
42
+ return 'preference';
43
+ }
44
+ export function mapKindToAxisRefs(kind) {
45
+ return [...AXIS_REFS_BY_KIND[kind]];
46
+ }
@@ -86,6 +86,13 @@ export declare function collectLifecycleActivity(): LifecycleActivity;
86
86
  export declare function collectSessionHistory(): SessionHistory;
87
87
  /** Collect hook error data. */
88
88
  export declare function collectHookHealth(): HookHealth;
89
+ export interface MultiHostData {
90
+ claude: number;
91
+ codex: number;
92
+ total: number;
93
+ }
94
+ /** Collect multi-host evidence distribution from host-mismatch store. */
95
+ export declare function collectMultiHostData(): MultiHostData;
89
96
  export interface LearningCurve {
90
97
  correctionsLast7d: number;
91
98
  correctionsPrev7d: number;