@wooojin/forgen 0.4.0 → 0.4.1

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 (76) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +30 -0
  3. package/README.ja.md +58 -1
  4. package/README.ko.md +58 -1
  5. package/README.md +83 -15
  6. package/README.zh.md +26 -0
  7. package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
  8. package/dist/checks/conclusion-verification-ratio.js +86 -0
  9. package/dist/checks/fact-vs-agreement.d.ts +47 -0
  10. package/dist/checks/fact-vs-agreement.js +92 -0
  11. package/dist/checks/self-score-deflation.d.ts +38 -0
  12. package/dist/checks/self-score-deflation.js +108 -0
  13. package/dist/cli.js +22 -2
  14. package/dist/core/auto-compound-runner.js +75 -11
  15. package/dist/core/dashboard.js +9 -2
  16. package/dist/core/doctor.js +26 -5
  17. package/dist/core/extraction-notice.d.ts +18 -0
  18. package/dist/core/extraction-notice.js +64 -0
  19. package/dist/core/init-cli.d.ts +26 -0
  20. package/dist/core/init-cli.js +104 -0
  21. package/dist/core/init.js +17 -0
  22. package/dist/core/inspect-cli.js +1 -2
  23. package/dist/core/migrate-cli.d.ts +10 -0
  24. package/dist/core/migrate-cli.js +34 -0
  25. package/dist/core/paths.d.ts +8 -1
  26. package/dist/core/paths.js +11 -2
  27. package/dist/core/recall-cli.d.ts +26 -0
  28. package/dist/core/recall-cli.js +125 -0
  29. package/dist/core/recall-reference-detector.d.ts +43 -0
  30. package/dist/core/recall-reference-detector.js +65 -0
  31. package/dist/core/stats-cli.d.ts +21 -0
  32. package/dist/core/stats-cli.js +121 -10
  33. package/dist/core/uninstall.js +2 -1
  34. package/dist/engine/compound-cli.js +1 -0
  35. package/dist/engine/compound-export.js +8 -3
  36. package/dist/engine/learn-cli.js +1 -4
  37. package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
  38. package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
  39. package/dist/engine/lifecycle/orchestrator.js +2 -2
  40. package/dist/engine/lifecycle/signals.js +6 -6
  41. package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
  42. package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
  43. package/dist/engine/skill-promoter.js +3 -6
  44. package/dist/hooks/context-guard.js +1 -1
  45. package/dist/hooks/dangerous-patterns.json +3 -3
  46. package/dist/hooks/db-guard.js +18 -2
  47. package/dist/hooks/intent-classifier.js +1 -1
  48. package/dist/hooks/keyword-detector.js +1 -1
  49. package/dist/hooks/notepad-injector.js +1 -1
  50. package/dist/hooks/permission-handler.js +1 -1
  51. package/dist/hooks/post-tool-failure.js +1 -1
  52. package/dist/hooks/post-tool-use.d.ts +6 -0
  53. package/dist/hooks/post-tool-use.js +37 -19
  54. package/dist/hooks/pre-compact.js +1 -1
  55. package/dist/hooks/pre-tool-use.d.ts +7 -0
  56. package/dist/hooks/pre-tool-use.js +24 -6
  57. package/dist/hooks/rate-limiter.js +1 -1
  58. package/dist/hooks/secret-filter.js +1 -1
  59. package/dist/hooks/session-recovery.js +1 -1
  60. package/dist/hooks/shared/command-parser.d.ts +44 -0
  61. package/dist/hooks/shared/command-parser.js +50 -0
  62. package/dist/hooks/shared/hook-response.d.ts +12 -2
  63. package/dist/hooks/shared/hook-response.js +30 -3
  64. package/dist/hooks/skill-injector.js +1 -1
  65. package/dist/hooks/slop-detector.js +2 -2
  66. package/dist/hooks/solution-injector.d.ts +9 -0
  67. package/dist/hooks/solution-injector.js +48 -5
  68. package/dist/hooks/stop-guard.js +137 -13
  69. package/dist/hooks/subagent-tracker.js +1 -1
  70. package/dist/i18n/index.js +3 -5
  71. package/dist/store/evidence-store.js +11 -0
  72. package/dist/store/implicit-feedback-store.d.ts +59 -0
  73. package/dist/store/implicit-feedback-store.js +153 -0
  74. package/dist/store/rule-store.js +8 -0
  75. package/package.json +2 -2
  76. package/plugin.json +1 -1
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Forgen v0.4.1 — TEST-1: 사실 vs 합의 가드
3
+ *
4
+ * 목적: Claude 가 "동작합니다 / 통과했습니다 / 검증됐습니다" 같은 **사실 주장**을
5
+ * 내놓을 때, 그 턴(또는 최근 N턴)에 실제 측정/검증을 수행한 도구 호출이 있었는가?
6
+ * 측정 없이 합의(agreement)만으로 사실로 변환된다면 alert.
7
+ *
8
+ * 배경 (RC1): v0.4.0 릴리즈 직전 self-assessment 에서 점수가 조금씩 올라가는데
9
+ * 측정 도구 호출은 0건인 케이스가 반복. 메타 점수 인플레이션 (TEST-2 / US-13)
10
+ * 의 직전 단계. 여기서는 alert 레벨까지만 — block 은 TEST-2 에서.
11
+ *
12
+ * 순수 함수 설계: I/O 없이 텍스트 + 측정 신호 메타데이터만 받아 판정.
13
+ * Stop hook / session scorer / CLI 어느 쪽에서도 호출 가능.
14
+ */
15
+ /**
16
+ * 측정성 도구 — 실행 결과가 사실 주장을 뒷받침할 수 있는 카테고리.
17
+ *
18
+ * v0.4.1 coverage fix: TEST-2 와 같은 논리로, Read/Edit/Write/Grep/Glob 은 파일
19
+ * 내용 확인/수정이지 "통과/검증/완료" 같은 실 실행 주장을 뒷받침 못 함. 오직
20
+ * Bash (실 실행) + NotebookEdit (실행 결과) 만 strong measurement.
21
+ *
22
+ * 이전 넓은 집합은 신규 사용자 시나리오 (buyer-day1 R4) 에서 Claude 가 Read
23
+ * 한 번만 해도 alert 회피 → TEST-1 본 의도 훼손.
24
+ */
25
+ const MEASUREMENT_TOOL_CATEGORIES = new Set([
26
+ 'Bash',
27
+ 'NotebookEdit',
28
+ ]);
29
+ /** 사실-주장 키워드 — "측정됐다/검증됐다" 류 강한 확정 언어. */
30
+ const FACT_ASSERTION_PATTERNS = [
31
+ /\b(pass(es|ed)?|passing)\b/i,
32
+ /\bverified\b/i,
33
+ /\bconfirmed\b/i,
34
+ /\bvalidated\b/i,
35
+ /\ball tests? pass/i,
36
+ /(통과(했|됐|함|합니다))/,
37
+ /(검증(됐|했|됨|완료))/,
38
+ /(동작(합니다|함|한다))/,
39
+ /(성공(했|했습니다|적))/,
40
+ /(완료(했|됐|됨|됐습니다))/,
41
+ ];
42
+ /** 합의/추측 표현 — 측정 없이 확언으로 가는 다리. 이 패턴이 많으면 합의→사실 전환 위험. */
43
+ const AGREEMENT_SOFTENERS = [
44
+ /\b(should|would|might)\s+(work|pass)/i,
45
+ /\blikely\b/i,
46
+ /\bprobably\b/i,
47
+ /(생각합니다|생각함|생각해|봅니다|예상(합니다|돼))/,
48
+ /(그럴\s*것\s*같|맞을\s*것\s*같)/,
49
+ ];
50
+ function findMatches(text, patterns, max = 3) {
51
+ const out = [];
52
+ for (const p of patterns) {
53
+ if (out.length >= max)
54
+ break;
55
+ const m = text.match(p);
56
+ if (m)
57
+ out.push(m[0]);
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * 핵심 판정 — 텍스트에 사실-주장이 있고 측정 도구가 없으면 alert.
63
+ * 측정이 있거나 사실-주장이 없으면 alert=false.
64
+ * 합의 softener 는 참고용 — softener 많을수록 reason 에 경고 추가.
65
+ */
66
+ export function checkFactVsAgreement(input) {
67
+ const { text, recentTools } = input;
68
+ const minMeasurements = input.minMeasurements ?? 1;
69
+ const factAssertions = findMatches(text, FACT_ASSERTION_PATTERNS);
70
+ const agreementSofteners = findMatches(text, AGREEMENT_SOFTENERS);
71
+ const measurementCount = recentTools.filter((t) => MEASUREMENT_TOOL_CATEGORIES.has(t)).length;
72
+ const hasFactAssertion = factAssertions.length > 0;
73
+ const measurementMissing = measurementCount < minMeasurements;
74
+ const alert = hasFactAssertion && measurementMissing;
75
+ let reason = '';
76
+ if (alert) {
77
+ const parts = [];
78
+ parts.push(`사실-주장 키워드 ${factAssertions.length}건 감지 ("${factAssertions.join('", "')}")`);
79
+ parts.push(`그러나 최근 측정 도구 호출 ${measurementCount}회 (< ${minMeasurements})`);
80
+ if (agreementSofteners.length > 0) {
81
+ parts.push(`합의성 표현 ${agreementSofteners.length}건 (${agreementSofteners.join(', ')})`);
82
+ }
83
+ reason = parts.join('. ');
84
+ }
85
+ return {
86
+ alert,
87
+ factAssertions,
88
+ agreementSofteners,
89
+ measurementCount,
90
+ reason,
91
+ };
92
+ }
@@ -0,0 +1,38 @@
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
+ export interface SelfScoreCheckInput {
17
+ text: string;
18
+ /** 이번 턴(또는 윈도우) 내 실행된 도구 이름 목록. */
19
+ recentTools: string[];
20
+ /** score delta 임계 — 이 이상의 증가를 인플레이션으로 간주. 기본 0 (모든 상승). */
21
+ minDelta?: number;
22
+ /** 측정 도구 최소 호출 수 — 기본 1. */
23
+ minMeasurements?: number;
24
+ }
25
+ export interface SelfScoreCheckResult {
26
+ /** true = 자가 점수 인플레이션 감지 (측정 없이 숫자 증가 선언). block 대상. */
27
+ block: boolean;
28
+ /** 매칭된 점수 표현 raw 스트링 (최대 3개). */
29
+ scoreSignals: string[];
30
+ /** 감지된 positive delta 목록 (from → to). */
31
+ deltas: Array<{
32
+ from: number;
33
+ to: number;
34
+ }>;
35
+ measurementCount: number;
36
+ reason: string;
37
+ }
38
+ export declare function checkSelfScoreInflation(input: SelfScoreCheckInput): SelfScoreCheckResult;
@@ -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
@@ -226,6 +226,22 @@ const commands = [
226
226
  await handleInspect(['violations', '--last', '1']);
227
227
  },
228
228
  },
229
+ {
230
+ name: 'recall',
231
+ description: 'Show recent compound recalls (matched solutions) with optional body preview.',
232
+ handler: async (args) => {
233
+ const { handleRecall } = await import('./core/recall-cli.js');
234
+ await handleRecall(args);
235
+ },
236
+ },
237
+ {
238
+ name: 'migrate',
239
+ description: 'One-shot schema migrations (implicit-feedback category backfill).',
240
+ handler: async (args) => {
241
+ const { handleMigrate } = await import('./core/migrate-cli.js');
242
+ await handleMigrate(args);
243
+ },
244
+ },
229
245
  {
230
246
  name: 'suppress-rule',
231
247
  description: '[alias: rule suppress] Disable a rule by id/prefix. Hard rules refused.',
@@ -430,12 +446,16 @@ function printHelp() {
430
446
  Inspect v1 state (alias: evidence → corrections)
431
447
  forgen rule <list|suppress|activate|scan|health-scan|classify>
432
448
  Rule management (see: forgen rule help)
433
- forgen stats One-screen trust-layer dashboard
449
+ forgen stats One-screen trust-layer dashboard (+ philosophy)
434
450
  forgen last-block Show the most recent block event
451
+ forgen recall [--limit N] [--show]
452
+ 최근 compound 주입 이력 (solution body preview)
453
+ forgen migrate [implicit-feedback|all]
454
+ One-shot schema migration (category backfill)
435
455
  forgen compound Manage accumulated knowledge
436
456
  forgen dashboard Compound system dashboard
437
457
  forgen me Personal dashboard
438
- forgen init Initialize project
458
+ forgen init Initialize project (+ starter-pack solutions)
439
459
  forgen config hooks Hook management
440
460
  forgen mcp MCP server management
441
461
  forgen skill promote|list Skill management
@@ -11,12 +11,12 @@
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';
16
15
  import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
17
16
  import { redactSecrets } from '../hooks/secret-filter.js';
18
17
  import { createEvidence, saveEvidence, promoteSessionCandidates } from '../store/evidence-store.js';
19
18
  import { loadProfile } from '../store/profile-store.js';
19
+ import { FORGEN_HOME, ME_DIR } from './paths.js';
20
20
  /** Auto-compound에 사용할 모델 — background 추출이므로 haiku로 충분 */
21
21
  const COMPOUND_MODEL = 'haiku';
22
22
  /** execFileSync wrapper: transient 에러(ETIMEDOUT 등) 시 1회 재시도 */
@@ -49,9 +49,8 @@ const [, , cwd, transcriptPath, sessionId] = process.argv;
49
49
  if (!cwd || !transcriptPath || !sessionId) {
50
50
  process.exit(1);
51
51
  }
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');
52
+ const SOLUTIONS_DIR = path.join(ME_DIR, 'solutions');
53
+ const BEHAVIOR_DIR = path.join(ME_DIR, 'behavior');
55
54
  /** Lightweight quality gate for auto-extracted solution files */
56
55
  /** Toxicity patterns — code-context only to avoid false positives on prose */
57
56
  const SOLUTION_TOXICITY_PATTERNS = [/@ts-ignore/i, /:\s*any\b/, /\/\/\s*TODO\b/];
@@ -383,10 +382,8 @@ ${sanitizedSummary.slice(0, 4000)}
383
382
  }
384
383
  // 3단계: 세션 학습 요약 (SessionLearningSummary) 생성
385
384
  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');
385
+ const V1_PROFILE = path.join(ME_DIR, 'forge-profile.json');
386
+ const V1_EVIDENCE_DIR = path.join(ME_DIR, 'behavior');
390
387
  if (fs.existsSync(V1_PROFILE)) {
391
388
  const currentProfile = loadProfile();
392
389
  let profileContext = '';
@@ -485,8 +482,9 @@ ${sanitizedSummary.slice(0, 4000)}
485
482
  process.stderr.write(`[forgen-auto-compound] session learning: ${e instanceof Error ? e.message : String(e)}\n`);
486
483
  }
487
484
  // Step 4: prefer-from-now / avoid-this 교정 → scope:'me' 영구 규칙 승격
485
+ let promotedCount = 0;
488
486
  try {
489
- const promotedCount = promoteSessionCandidates(sessionId);
487
+ promotedCount = promoteSessionCandidates(sessionId);
490
488
  if (promotedCount > 0) {
491
489
  process.stderr.write(`[forgen-auto-compound] promoted ${promotedCount} correction(s) to permanent rules\n`);
492
490
  }
@@ -494,6 +492,21 @@ ${sanitizedSummary.slice(0, 4000)}
494
492
  catch (e) {
495
493
  process.stderr.write(`[forgen-auto-compound] rule promotion: ${e instanceof Error ? e.message : String(e)}\n`);
496
494
  }
495
+ // H2: count newly extracted solutions (post-quality-gate) for Stop hook 알림.
496
+ // solutionsBefore 스냅샷 vs 현재 디스크 상태 차분 → "N개 패턴 학습됨" 1줄.
497
+ let extractedSolutionsCount = 0;
498
+ try {
499
+ if (fs.existsSync(SOLUTIONS_DIR)) {
500
+ const current = fs.readdirSync(SOLUTIONS_DIR).filter((f) => f.endsWith('.md'));
501
+ for (const f of current) {
502
+ if (!solutionsBefore.has(f))
503
+ extractedSolutionsCount++;
504
+ }
505
+ }
506
+ }
507
+ catch (e) {
508
+ process.stderr.write(`[forgen-auto-compound] solution count failed: ${e instanceof Error ? e.message : String(e)}\n`);
509
+ }
497
510
  // Step 5: meta-learning (HyperAgents-inspired self-tuning)
498
511
  try {
499
512
  const { runMetaLearning } = await import('../engine/meta-learning/runner.js');
@@ -508,10 +521,61 @@ ${sanitizedSummary.slice(0, 4000)}
508
521
  catch (e) {
509
522
  process.stderr.write(`[forgen-meta] ${e instanceof Error ? e.message : String(e)}\n`);
510
523
  }
511
- // 완료 기록
524
+ // Step 5.5 (v0.4.1): state hygiene — 세션 스코프 ephemeral 파일 7일 retention
525
+ // 자동 정리. 이전에는 `forgen doctor --prune-state` 수동만 있어서 injection-cache
526
+ // 2343 / modified-files 431 처럼 수천 파일 누적. 몇 달 사용하면 10만+ 파일 → stat
527
+ // 호출 느려지고 디스크 낭비. auto-compound 마다 호출되면 자연스레 정돈.
528
+ try {
529
+ const { pruneState } = await import('./state-gc.js');
530
+ const report = pruneState({ dryRun: false });
531
+ if (report.pruned > 0) {
532
+ const mb = (report.bytesFreed / 1024 / 1024).toFixed(2);
533
+ process.stderr.write(`[forgen-gc] pruned ${report.pruned} stale state files (${mb} MB freed)\n`);
534
+ }
535
+ }
536
+ catch (e) {
537
+ process.stderr.write(`[forgen-gc] state prune failed: ${e instanceof Error ? e.message : String(e)}\n`);
538
+ }
539
+ // Step 6 (v0.4.1): rule lifecycle 자동 실행 — rule 의 violations/bypass/drift
540
+ // 신호에 따른 자동 강등/승격. 이전에는 CLI (`forgen rule scan --apply`) 수동
541
+ // 호출만 있어서 구매자가 몇 주 써도 rule 정비 안 됨 → 쓸모없는 rule 이 계속
542
+ // active. 판매 관점 심각한 "자동 학습 단절". auto-compound-runner 끝에 자동
543
+ // 실행해 세션마다 rule 품질 유지.
544
+ try {
545
+ const { handleLifecycleScan } = await import('../engine/lifecycle/lifecycle-cli.js');
546
+ // silent mode 로 돌리기 위해 stdout 을 임시 리다이렉트 (내부가 console.log 씀)
547
+ const origLog = console.log;
548
+ let applied = 0;
549
+ console.log = (...args) => {
550
+ const msg = args.join(' ');
551
+ const match = msg.match(/apply(?:ied)?\s+(\d+)/i);
552
+ if (match)
553
+ applied = Number(match[1]);
554
+ };
555
+ try {
556
+ await handleLifecycleScan(['--apply']);
557
+ }
558
+ finally {
559
+ console.log = origLog;
560
+ }
561
+ if (applied > 0) {
562
+ process.stderr.write(`[forgen-meta] rule lifecycle: ${applied} event(s) applied\n`);
563
+ }
564
+ }
565
+ catch (e) {
566
+ process.stderr.write(`[forgen-meta] lifecycle scan failed: ${e instanceof Error ? e.message : String(e)}\n`);
567
+ }
568
+ // 완료 기록 — H2: Stop hook 알림용으로 extractedSolutions / promotedRules 포함.
569
+ // noticeShown=false 로 시작해서 Stop hook 가 최초 1회만 surface.
512
570
  const statePath = path.join(FORGEN_HOME, 'state', 'last-auto-compound.json');
513
571
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
514
- fs.writeFileSync(statePath, JSON.stringify({ sessionId, completedAt: new Date().toISOString() }));
572
+ fs.writeFileSync(statePath, JSON.stringify({
573
+ sessionId,
574
+ completedAt: new Date().toISOString(),
575
+ extractedSolutions: extractedSolutionsCount,
576
+ promotedRules: promotedCount,
577
+ noticeShown: false,
578
+ }));
515
579
  }
516
580
  catch (e) {
517
581
  process.stderr.write(`[forgen-auto-compound] ${e instanceof Error ? e.message : String(e)}\n`);
@@ -372,9 +372,15 @@ export function collectLearningCurve() {
372
372
  const files = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.json'));
373
373
  for (const f of files) {
374
374
  try {
375
+ // v0.4.1 정확도 수정: "교정 추이" 라벨은 explicit_correction evidence 만 포함.
376
+ // 이전에는 behavior_observation + session_summary 까지 전부 "교정" 으로
377
+ // 카운트되어 실측 488건 중 ~1건만 실제 교정인데 신뢰도 훼손. axis_hint 는
378
+ // raw_payload 에도 저장되므로 fallback 체크.
375
379
  const data = JSON.parse(fs.readFileSync(path.join(ME_BEHAVIOR, f), 'utf-8'));
376
380
  if (!data.timestamp)
377
381
  continue;
382
+ if (data.type && data.type !== 'explicit_correction')
383
+ continue;
378
384
  const ts = new Date(data.timestamp).getTime();
379
385
  if (!Number.isFinite(ts))
380
386
  continue;
@@ -383,8 +389,9 @@ export function collectLearningCurve() {
383
389
  correctionsLast7d++;
384
390
  else if (age < 2 * SEVEN_DAYS_MS)
385
391
  correctionsPrev7d++;
386
- if (data.axis_hint) {
387
- axisCounts.set(data.axis_hint, (axisCounts.get(data.axis_hint) ?? 0) + 1);
392
+ const axisHint = data.axis_hint ?? data.raw_payload?.axis_hint;
393
+ if (axisHint) {
394
+ axisCounts.set(axisHint, (axisCounts.get(axisHint) ?? 0) + 1);
388
395
  }
389
396
  uniqueDays.add(new Date(ts).toISOString().slice(0, 10));
390
397
  }
@@ -105,18 +105,39 @@ export async function runDoctor(opts = {}) {
105
105
  check('Inside tmux session', !!process.env.TMUX, 'FORGEN auto-compound relies on tmux. Launch: tmux new -s forgen');
106
106
  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
107
  console.log();
108
- // 솔루션/규칙
108
+ // v0.4.1 파일 확장자 버그 수정: rules 는 .json, behavior 도 대부분 .json 포맷.
109
+ // 이전에 .md 만 count 해서 실 rules 4개인데 0 으로 표시되는 incident 관찰.
110
+ // (compound-export countFiles 와 동일 결함 — 일관된 수정).
111
+ const isKnowledgeFile = (f) => f.endsWith('.md') || f.endsWith('.json');
109
112
  if (exists(ME_SOLUTIONS)) {
110
- const solutions = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md')).length;
113
+ const solutions = fs.readdirSync(ME_SOLUTIONS).filter(isKnowledgeFile).length;
111
114
  console.log(` Personal solutions: ${solutions}`);
112
115
  }
113
116
  if (exists(ME_BEHAVIOR)) {
114
- const behavior = fs.readdirSync(ME_BEHAVIOR).filter((f) => f.endsWith('.md')).length;
117
+ const behavior = fs.readdirSync(ME_BEHAVIOR).filter(isKnowledgeFile).length;
115
118
  console.log(` Behavioral patterns: ${behavior}`);
116
119
  }
117
120
  if (exists(ME_RULES)) {
118
- const rules = fs.readdirSync(ME_RULES).filter((f) => f.endsWith('.md')).length;
119
- console.log(` Personal rules: ${rules}`);
121
+ // v0.4.1 정확도: removed 상태 rule 은 "학습된 규칙" 에서 제외하고 별도 표시.
122
+ // 이전에는 디렉터리 파일 수만 세어 이미 제거된 rule 도 count 되어 판매 관점
123
+ // "살아있는 규칙" 수치가 부풀려짐. 실제 구매자 가치는 active + suppressed.
124
+ const ruleFiles = fs.readdirSync(ME_RULES).filter(isKnowledgeFile);
125
+ let active = 0, suppressed = 0, removed = 0;
126
+ for (const f of ruleFiles) {
127
+ try {
128
+ const d = JSON.parse(fs.readFileSync(path.join(ME_RULES, f), 'utf-8'));
129
+ if (d.status === 'active')
130
+ active++;
131
+ else if (d.status === 'suppressed')
132
+ suppressed++;
133
+ else if (d.status === 'removed' || d.status === 'superseded')
134
+ removed++;
135
+ }
136
+ catch { /* skip */ }
137
+ }
138
+ const live = active + suppressed;
139
+ const removedTag = removed > 0 ? ` (${removed} removed/superseded)` : '';
140
+ console.log(` Personal rules: ${live} [active:${active} suppressed:${suppressed}]${removedTag}`);
120
141
  }
121
142
  console.log();
122
143
  console.log(' [Log Locations]');
@@ -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,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>;