@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
@@ -23,6 +23,7 @@ const require = createRequire(import.meta.url);
23
23
  import { ME_SOLUTIONS, ME_RULES, ME_BEHAVIOR, STATE_DIR, } from './paths.js';
24
24
  import { parseFrontmatterOnly } from '../engine/solution-format.js';
25
25
  import { readMatchEvalLog } from '../engine/match-eval-log.js';
26
+ import { summarizeAllByHost } from '../store/host-mismatch.js';
26
27
  // ── ANSI color helpers ──
27
28
  const BOLD = '\x1b[1m';
28
29
  const DIM = '\x1b[2m';
@@ -356,6 +357,34 @@ function renderHookHealth(data) {
356
357
  lines.push(tableSep(widths, false, true));
357
358
  return lines.join('\n');
358
359
  }
360
+ /** Collect multi-host evidence distribution from host-mismatch store. */
361
+ export function collectMultiHostData() {
362
+ try {
363
+ return summarizeAllByHost();
364
+ }
365
+ catch {
366
+ return { claude: 0, codex: 0, total: 0 };
367
+ }
368
+ }
369
+ function renderMultiHost(data) {
370
+ const lines = [];
371
+ lines.push(` ${bold(cyan('Multi-Host Evidence'))}`);
372
+ lines.push('');
373
+ if (data.total === 0) {
374
+ lines.push(` ${dim('No evidence recorded yet.')}`);
375
+ return lines.join('\n');
376
+ }
377
+ const claudePct = Math.round((data.claude / data.total) * 100);
378
+ const codexPct = Math.round((data.codex / data.total) * 100);
379
+ lines.push(` Hosts claude:${data.claude} (${claudePct}%) codex:${data.codex} (${codexPct}%) total:${data.total}`);
380
+ // skew 경고: 80%+ 집중
381
+ const maxShare = Math.max(claudePct, codexPct);
382
+ if (data.total >= 5 && maxShare >= 80) {
383
+ const dominant = data.claude >= data.codex ? 'claude' : 'codex';
384
+ lines.push(` ${yellow(`⚠ ${dominant} 에 ${maxShare}% 집중 — 다른 host 데이터 부족`)}`);
385
+ }
386
+ return lines.join('\n');
387
+ }
359
388
  /**
360
389
  * Learning Curve 수집.
361
390
  * evidence 파일(교정 기록)과 compound 활용률을 교차 분석하여 "쓸수록 나아진다"를 정량화.
@@ -372,9 +401,15 @@ export function collectLearningCurve() {
372
401
  const files = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.json'));
373
402
  for (const f of files) {
374
403
  try {
404
+ // v0.4.1 정확도 수정: "교정 추이" 라벨은 explicit_correction evidence 만 포함.
405
+ // 이전에는 behavior_observation + session_summary 까지 전부 "교정" 으로
406
+ // 카운트되어 실측 488건 중 ~1건만 실제 교정인데 신뢰도 훼손. axis_hint 는
407
+ // raw_payload 에도 저장되므로 fallback 체크.
375
408
  const data = JSON.parse(fs.readFileSync(path.join(ME_BEHAVIOR, f), 'utf-8'));
376
409
  if (!data.timestamp)
377
410
  continue;
411
+ if (data.type && data.type !== 'explicit_correction')
412
+ continue;
378
413
  const ts = new Date(data.timestamp).getTime();
379
414
  if (!Number.isFinite(ts))
380
415
  continue;
@@ -383,8 +418,9 @@ export function collectLearningCurve() {
383
418
  correctionsLast7d++;
384
419
  else if (age < 2 * SEVEN_DAYS_MS)
385
420
  correctionsPrev7d++;
386
- if (data.axis_hint) {
387
- axisCounts.set(data.axis_hint, (axisCounts.get(data.axis_hint) ?? 0) + 1);
421
+ const axisHint = data.axis_hint ?? data.raw_payload?.axis_hint;
422
+ if (axisHint) {
423
+ axisCounts.set(axisHint, (axisCounts.get(axisHint) ?? 0) + 1);
388
424
  }
389
425
  uniqueDays.add(new Date(ts).toISOString().slice(0, 10));
390
426
  }
@@ -516,6 +552,7 @@ export function renderDashboard() {
516
552
  const session = collectSessionHistory();
517
553
  const hookHealth = collectHookHealth();
518
554
  const learning = collectLearningCurve();
555
+ const multiHost = collectMultiHostData();
519
556
  const divider = ` ${dim('─'.repeat(50))}`;
520
557
  const sections = [
521
558
  '',
@@ -531,6 +568,8 @@ export function renderDashboard() {
531
568
  divider,
532
569
  renderInjectionActivity(injection),
533
570
  divider,
571
+ renderMultiHost(multiHost),
572
+ divider,
534
573
  renderReflectionData(reflection),
535
574
  divider,
536
575
  renderLifecycleActivity(lifecycle),
@@ -5,6 +5,7 @@ import { execFileSync } from 'node:child_process';
5
5
  import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
6
6
  import { getTimingStats } from '../hooks/shared/hook-timing.js';
7
7
  import { countSessionScopedFiles, pruneState } from './state-gc.js';
8
+ import { summarizeAllByHost } from '../store/host-mismatch.js';
8
9
  /** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
9
10
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
10
11
  let currentSection = '';
@@ -34,6 +35,58 @@ function commandExists(cmd) {
34
35
  return false;
35
36
  }
36
37
  }
38
+ /** parity-result.json 내용에서 경과 시간을 사람이 읽기 좋은 문자열로 변환 */
39
+ function relativeTime(isoString) {
40
+ const diffMs = Date.now() - new Date(isoString).getTime();
41
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
42
+ if (diffDays === 0) {
43
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
44
+ if (diffHours === 0) {
45
+ const diffMins = Math.floor(diffMs / (1000 * 60));
46
+ return `${diffMins}m ago`;
47
+ }
48
+ return `${diffHours}h ago`;
49
+ }
50
+ return `${diffDays}d ago`;
51
+ }
52
+ /** [Codex Parity] 섹션 렌더링 — ~/.forgen/state/parity-result.json 신선도 검사 */
53
+ function renderCodexParity() {
54
+ console.log(' [Codex Parity]');
55
+ const parityPath = path.join(STATE_DIR, 'parity-result.json');
56
+ if (!fs.existsSync(parityPath)) {
57
+ console.log(' △ Codex parity 미실행 — tests/e2e/codex/run-parity.sh 또는 forgen parity codex');
58
+ return;
59
+ }
60
+ let data;
61
+ try {
62
+ data = JSON.parse(fs.readFileSync(parityPath, 'utf-8'));
63
+ }
64
+ catch {
65
+ console.log(' ✗ Codex parity — parity-result.json 파싱 실패');
66
+ return;
67
+ }
68
+ if (data.passed === null || data.passed === undefined) {
69
+ console.log(' △ Codex parity dry-run only — 실 실행 필요');
70
+ return;
71
+ }
72
+ if (!data.passed) {
73
+ const timeStr = data.at ? relativeTime(data.at) : 'unknown';
74
+ const detail = data.result ?? data.note ?? 'no detail';
75
+ console.log(` ✗ Codex parity FAILED (at: ${timeStr}, detail: ${detail})`);
76
+ return;
77
+ }
78
+ // passed === true
79
+ const timeStr = data.at ? relativeTime(data.at) : 'unknown';
80
+ const version = data.version ? ` version ${data.version}` : '';
81
+ const diffMs = data.at ? Date.now() - new Date(data.at).getTime() : Infinity;
82
+ const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
83
+ if (diffMs > sevenDaysMs) {
84
+ console.log(` △ Codex parity green but stale (last run: ${timeStr}) — 재실행 권장`);
85
+ }
86
+ else {
87
+ console.log(` ✓ Codex parity green (last run: ${timeStr},${version})`);
88
+ }
89
+ }
37
90
  export async function runDoctor(opts = {}) {
38
91
  failedChecks = [];
39
92
  console.log('\n Forgen — Diagnostics\n');
@@ -105,18 +158,39 @@ export async function runDoctor(opts = {}) {
105
158
  check('Inside tmux session', !!process.env.TMUX, 'FORGEN auto-compound relies on tmux. Launch: tmux new -s forgen');
106
159
  check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1', 'Set by `forgen` / `fgx` launcher. Hooks assume harness mode is active.');
107
160
  console.log();
108
- // 솔루션/규칙
161
+ // v0.4.1 파일 확장자 버그 수정: rules 는 .json, behavior 도 대부분 .json 포맷.
162
+ // 이전에 .md 만 count 해서 실 rules 4개인데 0 으로 표시되는 incident 관찰.
163
+ // (compound-export countFiles 와 동일 결함 — 일관된 수정).
164
+ const isKnowledgeFile = (f) => f.endsWith('.md') || f.endsWith('.json');
109
165
  if (exists(ME_SOLUTIONS)) {
110
- const solutions = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md')).length;
166
+ const solutions = fs.readdirSync(ME_SOLUTIONS).filter(isKnowledgeFile).length;
111
167
  console.log(` Personal solutions: ${solutions}`);
112
168
  }
113
169
  if (exists(ME_BEHAVIOR)) {
114
- const behavior = fs.readdirSync(ME_BEHAVIOR).filter((f) => f.endsWith('.md')).length;
170
+ const behavior = fs.readdirSync(ME_BEHAVIOR).filter(isKnowledgeFile).length;
115
171
  console.log(` Behavioral patterns: ${behavior}`);
116
172
  }
117
173
  if (exists(ME_RULES)) {
118
- const rules = fs.readdirSync(ME_RULES).filter((f) => f.endsWith('.md')).length;
119
- console.log(` Personal rules: ${rules}`);
174
+ // v0.4.1 정확도: removed 상태 rule 은 "학습된 규칙" 에서 제외하고 별도 표시.
175
+ // 이전에는 디렉터리 파일 수만 세어 이미 제거된 rule 도 count 되어 판매 관점
176
+ // "살아있는 규칙" 수치가 부풀려짐. 실제 구매자 가치는 active + suppressed.
177
+ const ruleFiles = fs.readdirSync(ME_RULES).filter(isKnowledgeFile);
178
+ let active = 0, suppressed = 0, removed = 0;
179
+ for (const f of ruleFiles) {
180
+ try {
181
+ const d = JSON.parse(fs.readFileSync(path.join(ME_RULES, f), 'utf-8'));
182
+ if (d.status === 'active')
183
+ active++;
184
+ else if (d.status === 'suppressed')
185
+ suppressed++;
186
+ else if (d.status === 'removed' || d.status === 'superseded')
187
+ removed++;
188
+ }
189
+ catch { /* skip */ }
190
+ }
191
+ const live = active + suppressed;
192
+ const removedTag = removed > 0 ? ` (${removed} removed/superseded)` : '';
193
+ console.log(` Personal rules: ${live} [active:${active} suppressed:${suppressed}]${removedTag}`);
120
194
  }
121
195
  console.log();
122
196
  console.log(' [Log Locations]');
@@ -365,6 +439,45 @@ export async function runDoctor(opts = {}) {
365
439
  // git 저장소가 아니거나 origin이 없으면 표시하지 않음
366
440
  console.log(' git remote: (none)');
367
441
  }
442
+ // P4 셀프 가드: fix:feat 비율 30% 초과 시 회귀 패턴 의심 경고.
443
+ try {
444
+ const { computeFixFeatRatio, formatFixRatio } = await import('./git-stats.js');
445
+ const ratio = computeFixFeatRatio();
446
+ if (ratio.available) {
447
+ console.log(` ${formatFixRatio(ratio)}`);
448
+ if (ratio.exceedsThreshold) {
449
+ console.log(' ⚠ fix:feat 비율이 임계값을 초과했습니다. "이거 고치면 저거 버그난다" 패턴 의심 — 검증 레이어 invariant 점검 권장.');
450
+ }
451
+ }
452
+ }
453
+ catch { /* fail-open */ }
454
+ console.log();
455
+ // [Multi-Host] — host 별 evidence 분포
456
+ console.log(' [Multi-Host]');
457
+ try {
458
+ const hostStats = summarizeAllByHost();
459
+ if (hostStats.total === 0) {
460
+ console.log(' No evidence recorded yet.');
461
+ }
462
+ else {
463
+ const claudePct = hostStats.total > 0 ? Math.round((hostStats.claude / hostStats.total) * 100) : 0;
464
+ const codexPct = hostStats.total > 0 ? Math.round((hostStats.codex / hostStats.total) * 100) : 0;
465
+ console.log(` Registered hosts: claude, codex`);
466
+ console.log(` Evidence by host: claude:${hostStats.claude} (${claudePct}%) codex:${hostStats.codex} (${codexPct}%) total:${hostStats.total}`);
467
+ // 한 host 가 80% 이상이면 skew 경고
468
+ const maxShare = Math.max(claudePct, codexPct);
469
+ if (hostStats.total >= 5 && maxShare >= 80) {
470
+ const dominant = claudePct >= codexPct ? 'claude' : 'codex';
471
+ console.log(` ⚠ evidence 가 ${dominant} 에 ${maxShare}% 집중됨 — 다른 host 에서 학습 데이터 부족 가능`);
472
+ }
473
+ }
474
+ }
475
+ catch {
476
+ console.log(' Unable to read host evidence data.');
477
+ }
478
+ console.log();
479
+ // [Codex Parity] — parity-result.json 신선도 검사 (v0.4.2 패턴 확장)
480
+ renderCodexParity();
368
481
  console.log();
369
482
  // [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
370
483
  console.log(' [Summary]');
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Forgen v0.4.1 — Extraction Notice (H2)
3
+ *
4
+ * `~/.forgen/state/last-auto-compound.json` 에 기록된 이전 세션의 추출 결과를
5
+ * Stop hook 에서 1회 surface. noticeShown 플래그로 한번 보여주면 다시 안뜸.
6
+ *
7
+ * 목적: v0.4.0 에서 auto-compound 가 8,000+ 번 돌았는데 사용자는 0건 노출. 추출이
8
+ * 실제로 일어났는지 사용자가 확인할 수 없었다. H2 는 "세션 종료 시 N개 패턴
9
+ * 학습됨" 1줄을 Stop hook UI (systemMessage) 로 밀어넣는다.
10
+ */
11
+ /**
12
+ * Stop hook approve 경로에서 호출. 보여줄 알림이 있으면 1줄 문자열 반환하고
13
+ * noticeShown=true 로 파일 업데이트 (한 번만 surface). 없으면 null.
14
+ *
15
+ * 신선도 컷오프: completedAt 이 30분 이상 지나면 stale 로 간주하고 surface 안함.
16
+ * 이미 다른 세션에서 본 알림이 튀어나오는 걸 방지.
17
+ */
18
+ export declare function takeLastExtractionNotice(nowMs?: number): string | null;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Forgen v0.4.1 — Extraction Notice (H2)
3
+ *
4
+ * `~/.forgen/state/last-auto-compound.json` 에 기록된 이전 세션의 추출 결과를
5
+ * Stop hook 에서 1회 surface. noticeShown 플래그로 한번 보여주면 다시 안뜸.
6
+ *
7
+ * 목적: v0.4.0 에서 auto-compound 가 8,000+ 번 돌았는데 사용자는 0건 노출. 추출이
8
+ * 실제로 일어났는지 사용자가 확인할 수 없었다. H2 는 "세션 종료 시 N개 패턴
9
+ * 학습됨" 1줄을 Stop hook UI (systemMessage) 로 밀어넣는다.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { STATE_DIR } from './paths.js';
14
+ const LAST_AUTO_COMPOUND_PATH = path.join(STATE_DIR, 'last-auto-compound.json');
15
+ /** 정상 실행이면 건너뛰기 좋게 fail-open. */
16
+ function readRecord() {
17
+ try {
18
+ if (!fs.existsSync(LAST_AUTO_COMPOUND_PATH))
19
+ return null;
20
+ return JSON.parse(fs.readFileSync(LAST_AUTO_COMPOUND_PATH, 'utf-8'));
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /**
27
+ * Stop hook approve 경로에서 호출. 보여줄 알림이 있으면 1줄 문자열 반환하고
28
+ * noticeShown=true 로 파일 업데이트 (한 번만 surface). 없으면 null.
29
+ *
30
+ * 신선도 컷오프: completedAt 이 30분 이상 지나면 stale 로 간주하고 surface 안함.
31
+ * 이미 다른 세션에서 본 알림이 튀어나오는 걸 방지.
32
+ */
33
+ export function takeLastExtractionNotice(nowMs = Date.now()) {
34
+ const record = readRecord();
35
+ if (!record || record.noticeShown)
36
+ return null;
37
+ const completed = Date.parse(record.completedAt);
38
+ if (!Number.isFinite(completed))
39
+ return null;
40
+ const ageMs = nowMs - completed;
41
+ if (ageMs > 30 * 60 * 1000)
42
+ return null; // stale
43
+ const extracted = record.extractedSolutions ?? 0;
44
+ const promoted = record.promotedRules ?? 0;
45
+ if (extracted === 0 && promoted === 0) {
46
+ // 아무것도 학습되지 않았으면 노이즈. 알림을 소비한 상태로만 마킹.
47
+ try {
48
+ fs.writeFileSync(LAST_AUTO_COMPOUND_PATH, JSON.stringify({ ...record, noticeShown: true }));
49
+ }
50
+ catch { /* fail-open */ }
51
+ return null;
52
+ }
53
+ // 마킹 — race 는 있으나 double-notice 가 치명적이지 않음 (fail-open).
54
+ try {
55
+ fs.writeFileSync(LAST_AUTO_COMPOUND_PATH, JSON.stringify({ ...record, noticeShown: true }));
56
+ }
57
+ catch { /* fail-open */ }
58
+ const parts = [];
59
+ if (extracted > 0)
60
+ parts.push(`${extracted}개 패턴 추출`);
61
+ if (promoted > 0)
62
+ parts.push(`${promoted}개 규칙 승격`);
63
+ return `[Forgen] 🧠 세션 학습 완료 — ${parts.join(', ')}`;
64
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Git Stats — P4 셀프 가드 (2026-04-27)
3
+ *
4
+ * 최근 N커밋의 conventional commit 분포를 측정해 fix:feat 비율을 계산.
5
+ * 정상 OSS 권장은 fix < 20%. 36% 초과 시 회귀 패턴 의심 — forgen 의 자기 메타 가드.
6
+ *
7
+ * 이번 세션 측정값: v0.4.1 시점 fix 비율 36% (정상의 약 2배). 이 코드가 다음 릴리즈
8
+ * 시 같은 비율을 자동 노출하여 사용자가 회귀 패턴을 빠르게 인지하게 한다.
9
+ */
10
+ export interface FixRatioStats {
11
+ windowSize: number;
12
+ fixCount: number;
13
+ featCount: number;
14
+ /** fix / (fix + feat), 0~1. fix+feat=0 이면 0. */
15
+ ratio: number;
16
+ threshold: number;
17
+ exceedsThreshold: boolean;
18
+ /** git 명령이 성공했는지 (저장소 외부 또는 git 미설치 시 false). */
19
+ available: boolean;
20
+ }
21
+ /**
22
+ * git log --no-merges -N 결과에서 conventional commit 형식의 fix/feat 만 카운트.
23
+ *
24
+ * 분류:
25
+ * - `feat: ...` / `feat(scope): ...` → feat
26
+ * - `fix: ...` / `fix(scope): ...` → fix (단, scope ∈ {test, tests, docs, doc} 제외)
27
+ * - 그 외 (chore, refactor, docs, style, test, hash 없는 라인) → 무시
28
+ *
29
+ * fix(test):, fix(docs): 가 제외되는 이유: 사소한 노이즈 fix 가 회귀 신호를
30
+ * 흐리지 않도록. 진짜 위험은 fix(core), fix(hook), fix(api) 같은 logic fix.
31
+ */
32
+ export declare function computeFixFeatRatio(cwd?: string, windowSize?: number, threshold?: number): FixRatioStats;
33
+ /** 테스트용 — git log 출력 텍스트를 직접 파싱. */
34
+ export declare function parseGitLog(rawLog: string, windowSize?: number, threshold?: number): FixRatioStats;
35
+ /** 사람용 한 줄 라벨. */
36
+ export declare function formatFixRatio(s: FixRatioStats): string;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Git Stats — P4 셀프 가드 (2026-04-27)
3
+ *
4
+ * 최근 N커밋의 conventional commit 분포를 측정해 fix:feat 비율을 계산.
5
+ * 정상 OSS 권장은 fix < 20%. 36% 초과 시 회귀 패턴 의심 — forgen 의 자기 메타 가드.
6
+ *
7
+ * 이번 세션 측정값: v0.4.1 시점 fix 비율 36% (정상의 약 2배). 이 코드가 다음 릴리즈
8
+ * 시 같은 비율을 자동 노출하여 사용자가 회귀 패턴을 빠르게 인지하게 한다.
9
+ */
10
+ import { execFileSync } from 'node:child_process';
11
+ const DEFAULT_THRESHOLD = 0.30;
12
+ const DEFAULT_WINDOW = 30;
13
+ const SCOPE_EXCLUSIONS = new Set(['test', 'tests', 'docs', 'doc']);
14
+ /**
15
+ * git log --no-merges -N 결과에서 conventional commit 형식의 fix/feat 만 카운트.
16
+ *
17
+ * 분류:
18
+ * - `feat: ...` / `feat(scope): ...` → feat
19
+ * - `fix: ...` / `fix(scope): ...` → fix (단, scope ∈ {test, tests, docs, doc} 제외)
20
+ * - 그 외 (chore, refactor, docs, style, test, hash 없는 라인) → 무시
21
+ *
22
+ * fix(test):, fix(docs): 가 제외되는 이유: 사소한 노이즈 fix 가 회귀 신호를
23
+ * 흐리지 않도록. 진짜 위험은 fix(core), fix(hook), fix(api) 같은 logic fix.
24
+ */
25
+ export function computeFixFeatRatio(cwd = process.cwd(), windowSize = DEFAULT_WINDOW, threshold = DEFAULT_THRESHOLD) {
26
+ try {
27
+ const out = execFileSync('git', ['log', '--no-merges', '--oneline', `-${windowSize}`], { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
28
+ return parseGitLog(out, windowSize, threshold);
29
+ }
30
+ catch {
31
+ return makeUnavailable(windowSize, threshold);
32
+ }
33
+ }
34
+ /** 테스트용 — git log 출력 텍스트를 직접 파싱. */
35
+ export function parseGitLog(rawLog, windowSize = DEFAULT_WINDOW, threshold = DEFAULT_THRESHOLD) {
36
+ const lines = rawLog.trim().split('\n').filter(Boolean);
37
+ let fix = 0;
38
+ let feat = 0;
39
+ for (const line of lines) {
40
+ const msg = line.replace(/^[a-f0-9]{4,40}\s+/, '');
41
+ const m = msg.match(/^(fix|feat)(?:\(([^)]+)\))?:/);
42
+ if (!m)
43
+ continue;
44
+ const type = m[1];
45
+ const scope = (m[2] ?? '').toLowerCase().trim();
46
+ if (type === 'fix' && SCOPE_EXCLUSIONS.has(scope))
47
+ continue;
48
+ if (type === 'fix')
49
+ fix++;
50
+ else
51
+ feat++;
52
+ }
53
+ const total = fix + feat;
54
+ const ratio = total === 0 ? 0 : fix / total;
55
+ return {
56
+ windowSize,
57
+ fixCount: fix,
58
+ featCount: feat,
59
+ ratio,
60
+ threshold,
61
+ exceedsThreshold: ratio > threshold,
62
+ available: true,
63
+ };
64
+ }
65
+ function makeUnavailable(windowSize, threshold) {
66
+ return {
67
+ windowSize, fixCount: 0, featCount: 0, ratio: 0,
68
+ threshold, exceedsThreshold: false, available: false,
69
+ };
70
+ }
71
+ /** 사람용 한 줄 라벨. */
72
+ export function formatFixRatio(s) {
73
+ if (!s.available)
74
+ return 'fix:feat ratio n/a (git unavailable)';
75
+ const pct = (s.ratio * 100).toFixed(0);
76
+ const thresholdPct = (s.threshold * 100).toFixed(0);
77
+ const flag = s.exceedsThreshold ? ` ⚠ over ${thresholdPct}%` : '';
78
+ return `fix:feat ratio ${pct}% (${s.fixCount}/${s.fixCount + s.featCount} in last ${s.windowSize})${flag}`;
79
+ }
@@ -9,7 +9,7 @@
9
9
  * - Lines 50-120: Rule file injection, gitignore, compound memory
10
10
  * - Lines 120+: prepareHarness — main orchestration
11
11
  */
12
- import { type RuntimeHost } from './types.js';
12
+ import type { RuntimeHost } from './types.js';
13
13
  import { rollbackSettings } from './settings-lock.js';
14
14
  import { type V1BootstrapResult } from './v1-bootstrap.js';
15
15
  export interface V1HarnessContext {
@@ -326,32 +326,39 @@ export async function prepareHarness(cwd, options = {}) {
326
326
  }
327
327
  // 3. 환경 확인
328
328
  const inTmux = !!process.env.TMUX;
329
- // 4. Claude Code 설정 주입 (환경변수 + trust 기반 permissions).
329
+ // 4-7. Claude artifact 작업 (settings.json + agents + rules + slash commands).
330
330
  //
331
- // Audit fix #1 (2026-04-21): acquireLock에서 live holder 감지
332
- // SettingsLockError가 throw될 있다. 사용자 작업 자체를 실패시키지
333
- // 않도록 warn 계속 진행 (이번 실행에서 settings는 기존 유지).
331
+ // feat/codex-support P1-7 (2026-04-27): runtime === 'codex' *.claude/* 계열
332
+ // 작업은 *no-op*. Codex 동치 prep Phase 3 (install-codex.ts 의 prompts +
333
+ // AGENTS.md inject) 에서 처리. 분기는 *Claude artifact Codex 환경을
334
+ // 오염시키지 않도록* 보호하는 비대칭 게이트.
334
335
  const pkgRoot = getPackageRoot();
335
336
  const env = buildEnv(cwd, v1Result.session?.session_id, runtime);
336
- try {
337
- injectSettings(env, v1Result, runtime, cwd, pkgRoot);
338
- }
339
- catch (e) {
340
- const msg = e instanceof Error ? e.message : String(e);
341
- if (msg.includes('settings.json lock') || msg.includes('SettingsLockError')) {
342
- console.error(`[forgen] ${msg} — settings 갱신 스킵, 이전 값 유지`);
337
+ if (runtime === 'claude') {
338
+ // 4. settings.json 인젝션
339
+ try {
340
+ injectSettings(env, v1Result, runtime, cwd, pkgRoot);
343
341
  }
344
- else {
345
- throw e;
342
+ catch (e) {
343
+ const msg = e instanceof Error ? e.message : String(e);
344
+ if (msg.includes('settings.json lock') || msg.includes('SettingsLockError')) {
345
+ console.error(`[forgen] ${msg} — settings 갱신 스킵, 이전 값 유지`);
346
+ }
347
+ else {
348
+ throw e;
349
+ }
346
350
  }
351
+ // 5. 에이전트 설치
352
+ installAgents(cwd, pkgRoot);
353
+ // 6. 규칙 파일 생성 + 주입
354
+ const ruleFiles = generateClaudeRuleFiles(cwd, v1Result.renderedRules);
355
+ injectClaudeRuleFiles(cwd, ruleFiles);
356
+ // 7. 슬래시 명령 설치
357
+ installSlashCommands(cwd, pkgRoot);
358
+ }
359
+ else {
360
+ log.debug(`prepareHarness: runtime=${runtime} — Claude artifact prep skipped (Phase 3 handles Codex prep)`);
347
361
  }
348
- // 5. 에이전트 설치
349
- installAgents(cwd, pkgRoot);
350
- // 6. 규칙 파일 생성 및 주입 (v1 부트스트랩 결과의 renderedRules를 직접 전달)
351
- const ruleFiles = generateClaudeRuleFiles(cwd, v1Result.renderedRules);
352
- injectClaudeRuleFiles(cwd, ruleFiles);
353
- // 7. 슬래시 명령 설치
354
- installSlashCommands(cwd, pkgRoot);
355
362
  // 8. tmux 바인딩 등록
356
363
  if (inTmux) {
357
364
  await registerTmuxBindings();
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Host detection — feat/codex-support Phase 1
3
+ *
4
+ * `forgen install` interactive 의 prerequisite — 사용자 환경에 어떤 host (Claude/Codex)
5
+ * 가 가용한지 탐지. spec §10 Phase 1 + interview R3.
6
+ *
7
+ * 탐지 신호 (각 host 별):
8
+ * - binary 가 PATH 에 있음 (`which claude` / `which codex`)
9
+ * - host 디렉토리 존재 (~/.claude/ / ~/.codex/)
10
+ * - (Codex 만) `~/.codex/auth.json` 존재 (로그인 흔적)
11
+ *
12
+ * detect 결과는 *추론* 만. install 강제 안 함.
13
+ */
14
+ import type { HostId } from './trust-layer-intent.js';
15
+ export interface HostAvailability {
16
+ readonly host: HostId;
17
+ /** binary 가 PATH 에 있음. */
18
+ readonly binaryFound: boolean;
19
+ /** binary 절대경로 (없으면 null). */
20
+ readonly binaryPath: string | null;
21
+ /** host home 디렉토리 존재 (~/.claude/ 또는 ~/.codex/). */
22
+ readonly homeExists: boolean;
23
+ /** host home 절대경로. */
24
+ readonly homePath: string;
25
+ /** Codex 의 경우 auth.json 존재 (로그인 흔적). Claude 는 항상 null. */
26
+ readonly authPresent: boolean | null;
27
+ /**
28
+ * 종합 판단 — *install 후보로 적합한가*.
29
+ * - binaryFound 또는 homeExists 중 하나 이상이면 true.
30
+ * - 둘 다 없으면 false (사용자가 host 를 안 쓸 가능성 높음).
31
+ */
32
+ readonly available: boolean;
33
+ }
34
+ export interface HostDetectionResult {
35
+ readonly claude: HostAvailability;
36
+ readonly codex: HostAvailability;
37
+ /** 둘 다 사용 가능. */
38
+ readonly bothAvailable: boolean;
39
+ /** 하나도 사용 가능하지 않음 (warn). */
40
+ readonly noneAvailable: boolean;
41
+ }
42
+ export declare function detectAvailableHosts(): HostDetectionResult;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Host detection — feat/codex-support Phase 1
3
+ *
4
+ * `forgen install` interactive 의 prerequisite — 사용자 환경에 어떤 host (Claude/Codex)
5
+ * 가 가용한지 탐지. spec §10 Phase 1 + interview R3.
6
+ *
7
+ * 탐지 신호 (각 host 별):
8
+ * - binary 가 PATH 에 있음 (`which claude` / `which codex`)
9
+ * - host 디렉토리 존재 (~/.claude/ / ~/.codex/)
10
+ * - (Codex 만) `~/.codex/auth.json` 존재 (로그인 흔적)
11
+ *
12
+ * detect 결과는 *추론* 만. install 강제 안 함.
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as os from 'node:os';
16
+ import * as path from 'node:path';
17
+ import { execFileSync } from 'node:child_process';
18
+ function which(binary) {
19
+ try {
20
+ const out = execFileSync('which', [binary], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
21
+ const trimmed = out.trim();
22
+ return trimmed.length > 0 ? trimmed : null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function detectClaude() {
29
+ const binaryPath = which('claude');
30
+ const homePath = path.join(os.homedir(), '.claude');
31
+ const homeExists = fs.existsSync(homePath);
32
+ const binaryFound = binaryPath !== null;
33
+ return {
34
+ host: 'claude',
35
+ binaryFound,
36
+ binaryPath,
37
+ homeExists,
38
+ homePath,
39
+ authPresent: null, // Claude 는 별도 auth.json 패턴이 없음 (subscription 통합)
40
+ available: binaryFound || homeExists,
41
+ };
42
+ }
43
+ function detectCodex() {
44
+ const binaryPath = which('codex');
45
+ const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
46
+ const homeExists = fs.existsSync(codexHome);
47
+ const binaryFound = binaryPath !== null;
48
+ const authPresent = fs.existsSync(path.join(codexHome, 'auth.json'));
49
+ return {
50
+ host: 'codex',
51
+ binaryFound,
52
+ binaryPath,
53
+ homeExists,
54
+ homePath: codexHome,
55
+ authPresent,
56
+ available: binaryFound || homeExists,
57
+ };
58
+ }
59
+ export function detectAvailableHosts() {
60
+ const claude = detectClaude();
61
+ const codex = detectCodex();
62
+ return {
63
+ claude,
64
+ codex,
65
+ bothAvailable: claude.available && codex.available,
66
+ noneAvailable: !claude.available && !codex.available,
67
+ };
68
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Forgen v0.4.1 — `forgen init` CLI
3
+ *
4
+ * 빈 FORGEN_HOME (또는 기존에 starter 미설치 홈) 에 starter-pack 솔루션을
5
+ * 프로비저닝. npm install-g 시의 postinstall 이 하던 starter 배포 로직을 런타임
6
+ * CLI 로 노출해 다음 시나리오 지원:
7
+ * - `FORGEN_HOME=/tmp/fresh forgen init` — 격리 테스트 환경
8
+ * - CI pipeline 신규 컨테이너 프로비저닝
9
+ * - 사용자가 실수로 me/solutions 전부 삭제한 뒤 복구
10
+ *
11
+ * 보수적 정책: me/solutions 에 **≥5개 파일**이 이미 있으면 건너뜀 (사용자
12
+ * 실 축적물 보호). `--force` 플래그로 우회 가능. postinstall 의 installStarterPack
13
+ * 과 동일 규칙.
14
+ */
15
+ export interface InitResult {
16
+ solutionsInstalled: number;
17
+ solutionsSkippedExisting: number;
18
+ solutionsDir: string;
19
+ starterDir: string | null;
20
+ skipped: boolean;
21
+ skipReason?: string;
22
+ }
23
+ export declare function initializeForgenHome(options?: {
24
+ force?: boolean;
25
+ }): InitResult;
26
+ export declare function handleInit(args: string[]): Promise<void>;