@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.ja.md +58 -1
- package/README.ko.md +58 -1
- package/README.md +83 -15
- package/README.zh.md +26 -0
- package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
- package/dist/checks/conclusion-verification-ratio.js +86 -0
- package/dist/checks/fact-vs-agreement.d.ts +47 -0
- package/dist/checks/fact-vs-agreement.js +92 -0
- package/dist/checks/self-score-deflation.d.ts +38 -0
- package/dist/checks/self-score-deflation.js +108 -0
- package/dist/cli.js +22 -2
- package/dist/core/auto-compound-runner.js +75 -11
- package/dist/core/dashboard.js +9 -2
- package/dist/core/doctor.js +26 -5
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -0
- package/dist/core/init-cli.d.ts +26 -0
- package/dist/core/init-cli.js +104 -0
- package/dist/core/init.js +17 -0
- package/dist/core/inspect-cli.js +1 -2
- package/dist/core/migrate-cli.d.ts +10 -0
- package/dist/core/migrate-cli.js +34 -0
- package/dist/core/paths.d.ts +8 -1
- package/dist/core/paths.js +11 -2
- package/dist/core/recall-cli.d.ts +26 -0
- package/dist/core/recall-cli.js +125 -0
- package/dist/core/recall-reference-detector.d.ts +43 -0
- package/dist/core/recall-reference-detector.js +65 -0
- package/dist/core/stats-cli.d.ts +21 -0
- package/dist/core/stats-cli.js +121 -10
- package/dist/core/uninstall.js +2 -1
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/learn-cli.js +1 -4
- package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
- package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
- package/dist/engine/lifecycle/orchestrator.js +2 -2
- package/dist/engine/lifecycle/signals.js +6 -6
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
- package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
- package/dist/engine/skill-promoter.js +3 -6
- package/dist/hooks/context-guard.js +1 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +18 -2
- package/dist/hooks/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/notepad-injector.js +1 -1
- package/dist/hooks/permission-handler.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +6 -0
- package/dist/hooks/post-tool-use.js +37 -19
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +24 -6
- package/dist/hooks/rate-limiter.js +1 -1
- package/dist/hooks/secret-filter.js +1 -1
- package/dist/hooks/session-recovery.js +1 -1
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -0
- package/dist/hooks/shared/hook-response.d.ts +12 -2
- package/dist/hooks/shared/hook-response.js +30 -3
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/slop-detector.js +2 -2
- package/dist/hooks/solution-injector.d.ts +9 -0
- package/dist/hooks/solution-injector.js +48 -5
- package/dist/hooks/stop-guard.js +137 -13
- package/dist/hooks/subagent-tracker.js +1 -1
- package/dist/i18n/index.js +3 -5
- package/dist/store/evidence-store.js +11 -0
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/rule-store.js +8 -0
- package/package.json +2 -2
- 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
|
|
53
|
-
const
|
|
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
|
|
387
|
-
const
|
|
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
|
-
|
|
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({
|
|
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`);
|
package/dist/core/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
387
|
-
|
|
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
|
}
|
package/dist/core/doctor.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
119
|
-
|
|
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>;
|