@wooojin/forgen 0.3.2 → 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 +94 -0
- package/README.ja.md +119 -8
- package/README.ko.md +73 -2
- package/README.md +163 -9
- package/README.zh.md +87 -7
- 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 +158 -6
- package/dist/core/auto-compound-runner.js +85 -13
- package/dist/core/dashboard.js +9 -2
- package/dist/core/doctor.js +90 -15
- 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 +64 -5
- 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/state-gc.d.ts +19 -0
- package/dist/core/state-gc.js +48 -4
- package/dist/core/stats-cli.d.ts +36 -0
- package/dist/core/stats-cli.js +254 -0
- package/dist/core/uninstall.d.ts +1 -0
- package/dist/core/uninstall.js +25 -1
- package/dist/core/v1-bootstrap.js +9 -1
- package/dist/engine/classify-enforce-cli.d.ts +8 -0
- package/dist/engine/classify-enforce-cli.js +61 -0
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/enforce-classifier.d.ts +31 -0
- package/dist/engine/enforce-classifier.js +123 -0
- package/dist/engine/learn-cli.js +1 -4
- package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
- package/dist/engine/lifecycle/bypass-detector.js +82 -0
- package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
- package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
- package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
- package/dist/engine/lifecycle/meta-cli.js +7 -0
- package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
- package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
- package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
- package/dist/engine/lifecycle/orchestrator.js +131 -0
- package/dist/engine/lifecycle/signals.d.ts +30 -0
- package/dist/engine/lifecycle/signals.js +142 -0
- package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
- package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
- package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
- package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
- package/dist/engine/lifecycle/types.d.ts +52 -0
- package/dist/engine/lifecycle/types.js +7 -0
- 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/rule-toggle-cli.d.ts +13 -0
- package/dist/engine/rule-toggle-cli.js +76 -0
- package/dist/engine/skill-promoter.js +3 -6
- package/dist/forge/evidence-processor.js +10 -2
- package/dist/hooks/context-guard.js +72 -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 +94 -14
- 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 +79 -5
- package/dist/hooks/rate-limiter.js +1 -1
- package/dist/hooks/secret-filter.d.ts +10 -0
- package/dist/hooks/secret-filter.js +21 -1
- package/dist/hooks/session-recovery.js +1 -1
- package/dist/hooks/shared/atomic-write.d.ts +8 -1
- package/dist/hooks/shared/atomic-write.js +17 -3
- 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 +23 -2
- package/dist/hooks/shared/hook-response.js +48 -3
- package/dist/hooks/shared/safe-regex.d.ts +25 -0
- package/dist/hooks/shared/safe-regex.js +50 -0
- package/dist/hooks/shared/stop-triggers.d.ts +19 -0
- package/dist/hooks/shared/stop-triggers.js +19 -0
- 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.d.ts +84 -0
- package/dist/hooks/stop-guard.js +606 -0
- package/dist/hooks/subagent-tracker.js +1 -1
- package/dist/i18n/index.js +3 -5
- package/dist/mcp/tools.js +19 -2
- package/dist/store/evidence-store.d.ts +15 -0
- package/dist/store/evidence-store.js +61 -1
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/rule-lifecycle.d.ts +23 -0
- package/dist/store/rule-lifecycle.js +63 -0
- package/dist/store/rule-store.d.ts +21 -0
- package/dist/store/rule-store.js +136 -8
- package/dist/store/types.d.ts +83 -0
- package/dist/store/types.js +7 -1
- package/hooks/hook-registry.json +1 -0
- package/hooks/hooks.json +6 -1
- package/package.json +11 -3
- package/plugin.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface StatsSnapshot {
|
|
2
|
+
activeRules: number;
|
|
3
|
+
suppressedRules: number;
|
|
4
|
+
correctionsTotal: number;
|
|
5
|
+
corrections7d: number;
|
|
6
|
+
blocks7d: number;
|
|
7
|
+
acks7d: number;
|
|
8
|
+
bypass7d: number;
|
|
9
|
+
drift7d: number;
|
|
10
|
+
retired7d: number;
|
|
11
|
+
lastExtraction: string;
|
|
12
|
+
/**
|
|
13
|
+
* H3 / v0.4.1 — assist 축 가시화. enforcement(block/violation) 는 이미 표시되지만
|
|
14
|
+
* assist(recall hit, surface, extraction) 는 v0.4.0 에서 8,000+ 번 작동했음에도
|
|
15
|
+
* 사용자에게 0건 노출되었다. 오늘 기준 숫자로 "지금 학습되고 있다" 를 surface.
|
|
16
|
+
*/
|
|
17
|
+
assistToday: {
|
|
18
|
+
recallHits: number;
|
|
19
|
+
surfaced: number;
|
|
20
|
+
referenced: number;
|
|
21
|
+
extractedToday: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* v0.4.1 철학 고도화 지표 — forge-profile.json 이 실제로 학습됐는지 한눈에.
|
|
25
|
+
* 값이 있으면 가시화, 없으면 undefined.
|
|
26
|
+
*/
|
|
27
|
+
philosophy?: {
|
|
28
|
+
basePacks: string[];
|
|
29
|
+
trustPolicy: string;
|
|
30
|
+
axisScores: Record<string, number>;
|
|
31
|
+
lastReclassification: string | null;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare function computeStats(): StatsSnapshot;
|
|
35
|
+
export declare function renderStats(s: StatsSnapshot): string;
|
|
36
|
+
export declare function handleStats(_args: string[]): Promise<void>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R9-PA1: `forgen stats` — 7-number single-screen dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Pure aggregation over existing jsonl sources. No new telemetry; surfaces
|
|
5
|
+
* what forgen is *already* learning so users can verify the trust layer is
|
|
6
|
+
* working between Claude sessions.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { loadAllRules } from '../store/rule-store.js';
|
|
11
|
+
import { loadAllEvidence } from '../store/evidence-store.js';
|
|
12
|
+
import { STATE_DIR, ME_DIR } from './paths.js';
|
|
13
|
+
// v0.4.1 격리 fix: 이전에는 os.homedir() 직접 사용해서 FORGEN_HOME env 로
|
|
14
|
+
// 홈 격리해도 이 파일의 경로는 여전히 실 홈 가리켰음. paths.ts 상수 import.
|
|
15
|
+
const ENFORCEMENT_DIR = path.join(STATE_DIR, 'enforcement');
|
|
16
|
+
const LIFECYCLE_DIR = path.join(STATE_DIR, 'lifecycle');
|
|
17
|
+
const SOLUTIONS_DIR = path.join(ME_DIR, 'solutions');
|
|
18
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
19
|
+
function readJsonl(p) {
|
|
20
|
+
if (!fs.existsSync(p))
|
|
21
|
+
return [];
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
24
|
+
if (!line.trim())
|
|
25
|
+
continue;
|
|
26
|
+
try {
|
|
27
|
+
out.push(JSON.parse(line));
|
|
28
|
+
}
|
|
29
|
+
catch { /* skip malformed */ }
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function countWithin(entries, days, tsKey = 'at') {
|
|
34
|
+
const cutoff = Date.now() - days * MS_PER_DAY;
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
const raw = e[tsKey];
|
|
38
|
+
if (typeof raw !== 'string')
|
|
39
|
+
continue;
|
|
40
|
+
const t = Date.parse(raw);
|
|
41
|
+
if (Number.isFinite(t) && t >= cutoff)
|
|
42
|
+
n += 1;
|
|
43
|
+
}
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
function readLifecycleRetired(days) {
|
|
47
|
+
if (!fs.existsSync(LIFECYCLE_DIR))
|
|
48
|
+
return 0;
|
|
49
|
+
const cutoff = Date.now() - days * MS_PER_DAY;
|
|
50
|
+
let n = 0;
|
|
51
|
+
for (const f of fs.readdirSync(LIFECYCLE_DIR)) {
|
|
52
|
+
if (!f.endsWith('.jsonl'))
|
|
53
|
+
continue;
|
|
54
|
+
for (const entry of readJsonl(path.join(LIFECYCLE_DIR, f))) {
|
|
55
|
+
const action = entry.suggested_action;
|
|
56
|
+
const ts = typeof entry.ts === 'number' ? entry.ts : Date.parse(String(entry.ts ?? ''));
|
|
57
|
+
if (!Number.isFinite(ts) || ts < cutoff)
|
|
58
|
+
continue;
|
|
59
|
+
if (action === 'retire' || action === 'supersede')
|
|
60
|
+
n += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
function readLastExtraction() {
|
|
66
|
+
// v0.4.1 파일명 정합: auto-compound-runner 는 last-auto-compound.json 에 기록.
|
|
67
|
+
// 이전 코드가 last-extraction.json 을 찾아 "never" 가 만성적으로 표시됨 —
|
|
68
|
+
// 실은 매 auto-compound 세션마다 값이 업데이트되고 있는데도 stats 에 반영 X.
|
|
69
|
+
const candidates = ['last-auto-compound.json', 'last-extraction.json'];
|
|
70
|
+
let p = null;
|
|
71
|
+
for (const name of candidates) {
|
|
72
|
+
const candidate = path.join(STATE_DIR, name);
|
|
73
|
+
if (fs.existsSync(candidate)) {
|
|
74
|
+
p = candidate;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!p)
|
|
79
|
+
return 'never';
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
82
|
+
const ts = data.completedAt ?? data.timestamp ?? data.date;
|
|
83
|
+
if (!ts)
|
|
84
|
+
return 'never';
|
|
85
|
+
const diffDays = Math.floor((Date.now() - Date.parse(ts)) / MS_PER_DAY);
|
|
86
|
+
const dateStr = new Date(ts).toISOString().slice(0, 10);
|
|
87
|
+
if (diffDays === 0)
|
|
88
|
+
return `${dateStr} (today)`;
|
|
89
|
+
if (diffDays === 1)
|
|
90
|
+
return `${dateStr} (yesterday)`;
|
|
91
|
+
return `${dateStr} (${diffDays}d ago)`;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return 'unknown';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** H3: 오늘 (local midnight ~ now) 기준 assist 카운트. */
|
|
98
|
+
function computeAssistToday() {
|
|
99
|
+
const startOfDay = new Date();
|
|
100
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
101
|
+
const cutoffMs = startOfDay.getTime();
|
|
102
|
+
// recall hits: match-eval-log 의 오늘 entries
|
|
103
|
+
const matchLog = readJsonl(path.join(STATE_DIR, 'match-eval-log.jsonl'));
|
|
104
|
+
let recallHits = 0;
|
|
105
|
+
for (const e of matchLog) {
|
|
106
|
+
const ts = typeof e.ts === 'string' ? Date.parse(e.ts) : NaN;
|
|
107
|
+
if (Number.isFinite(ts) && ts >= cutoffMs)
|
|
108
|
+
recallHits++;
|
|
109
|
+
}
|
|
110
|
+
// surfaced + referenced — 같은 스트림 1회 loop 로.
|
|
111
|
+
const feedback = readJsonl(path.join(STATE_DIR, 'implicit-feedback.jsonl'));
|
|
112
|
+
let surfaced = 0;
|
|
113
|
+
let referenced = 0;
|
|
114
|
+
for (const e of feedback) {
|
|
115
|
+
const ts = typeof e.at === 'string' ? Date.parse(e.at) : NaN;
|
|
116
|
+
if (!Number.isFinite(ts) || ts < cutoffMs)
|
|
117
|
+
continue;
|
|
118
|
+
if (e.type === 'recommendation_surfaced')
|
|
119
|
+
surfaced++;
|
|
120
|
+
else if (e.type === 'recall_referenced')
|
|
121
|
+
referenced++;
|
|
122
|
+
}
|
|
123
|
+
// extracted today: solutions dir 에서 오늘 mtime 인 .md 파일
|
|
124
|
+
let extractedToday = 0;
|
|
125
|
+
try {
|
|
126
|
+
if (fs.existsSync(SOLUTIONS_DIR)) {
|
|
127
|
+
for (const f of fs.readdirSync(SOLUTIONS_DIR)) {
|
|
128
|
+
if (!f.endsWith('.md'))
|
|
129
|
+
continue;
|
|
130
|
+
const stat = fs.statSync(path.join(SOLUTIONS_DIR, f));
|
|
131
|
+
if (stat.mtimeMs >= cutoffMs)
|
|
132
|
+
extractedToday++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch { /* fail-open */ }
|
|
137
|
+
return { recallHits, surfaced, referenced, extractedToday };
|
|
138
|
+
}
|
|
139
|
+
/** v0.4.1: forge-profile 에서 고도화 지표 추출. 파일 없거나 깨지면 undefined. */
|
|
140
|
+
function computePhilosophy() {
|
|
141
|
+
try {
|
|
142
|
+
const profilePath = path.join(ME_DIR, 'forge-profile.json');
|
|
143
|
+
if (!fs.existsSync(profilePath))
|
|
144
|
+
return undefined;
|
|
145
|
+
const d = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
146
|
+
const axisScores = {};
|
|
147
|
+
for (const [k, v] of Object.entries(d.axes ?? {})) {
|
|
148
|
+
if (v && typeof v.score === 'number')
|
|
149
|
+
axisScores[k] = v.score;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
basePacks: Object.values(d.base_packs ?? {}),
|
|
153
|
+
trustPolicy: d.trust_preferences?.desired_policy ?? 'unknown',
|
|
154
|
+
axisScores,
|
|
155
|
+
lastReclassification: d.metadata?.last_reclassification_at ?? null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function computeStats() {
|
|
163
|
+
const rules = loadAllRules();
|
|
164
|
+
const activeRules = rules.filter((r) => r.status === 'active').length;
|
|
165
|
+
const suppressedRules = rules.filter((r) => r.status === 'suppressed').length;
|
|
166
|
+
// v0.4.1 정확도 수정: loadRecentEvidence(500) 제한은 "Corrections (total)"
|
|
167
|
+
// 라벨과 모순 — 실 behavior 618건 중 118건 누락됐음. 전체 evidence 스캔으로
|
|
168
|
+
// 교체. explicit_correction 만 filter 이므로 memory overhead 는 N * ~1KB 수준.
|
|
169
|
+
const evidence = loadAllEvidence();
|
|
170
|
+
const corrections = evidence.filter((e) => e.type === 'explicit_correction');
|
|
171
|
+
const correctionsTotal = corrections.length;
|
|
172
|
+
const cutoff7d = Date.now() - 7 * MS_PER_DAY;
|
|
173
|
+
const corrections7d = corrections.filter((e) => Date.parse(e.timestamp) >= cutoff7d).length;
|
|
174
|
+
const violations = readJsonl(path.join(ENFORCEMENT_DIR, 'violations.jsonl'));
|
|
175
|
+
// v0.4.1 historical false-positive 제거: pre-0.4.1 bypass-detector 가 Write/Edit
|
|
176
|
+
// content 의 quote 본문까지 raw 매칭해서 bypass 로 오기록. 실 관찰: L1-no-rm-rf
|
|
177
|
+
// -unconfirmed bypass 20건 중 Write/Edit 15건. stats 표시는 **실 실행 맥락** 인
|
|
178
|
+
// Bash/Agent/기타만 집계 — 앞으로의 시계열 일관성 + 과거 noise 제거.
|
|
179
|
+
const bypassRaw = readJsonl(path.join(ENFORCEMENT_DIR, 'bypass.jsonl'));
|
|
180
|
+
const bypass = bypassRaw.filter((e) => e.tool !== 'Write' && e.tool !== 'Edit');
|
|
181
|
+
const drift = readJsonl(path.join(ENFORCEMENT_DIR, 'drift.jsonl'));
|
|
182
|
+
const acks = readJsonl(path.join(ENFORCEMENT_DIR, 'acknowledgments.jsonl'));
|
|
183
|
+
// R9-PA2: violations 는 'block' (stop-guard/post-tool) + 'deny' (pre-tool Mech-A)
|
|
184
|
+
// + 'correction' (user bypass audit) 혼재. 사용자 관점에서 "Block" 은 앞의 2종이며
|
|
185
|
+
// correction 은 제외해야 ack ratio 가 의미를 갖는다. legacy-undefined 엔트리도 포함.
|
|
186
|
+
const realBlocks = violations.filter((e) => e.kind === 'block' || e.kind === 'deny' || e.kind === undefined);
|
|
187
|
+
return {
|
|
188
|
+
activeRules,
|
|
189
|
+
suppressedRules,
|
|
190
|
+
correctionsTotal,
|
|
191
|
+
corrections7d,
|
|
192
|
+
blocks7d: countWithin(realBlocks, 7),
|
|
193
|
+
acks7d: countWithin(acks, 7),
|
|
194
|
+
bypass7d: countWithin(bypass, 7),
|
|
195
|
+
drift7d: countWithin(drift, 7),
|
|
196
|
+
retired7d: readLifecycleRetired(7),
|
|
197
|
+
lastExtraction: readLastExtraction(),
|
|
198
|
+
assistToday: computeAssistToday(),
|
|
199
|
+
philosophy: computePhilosophy(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function padNum(n, width = 4) {
|
|
203
|
+
return String(n).padStart(width);
|
|
204
|
+
}
|
|
205
|
+
export function renderStats(s) {
|
|
206
|
+
const lines = [];
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(' forgen — trust layer status');
|
|
209
|
+
lines.push(' ───────────────────────────');
|
|
210
|
+
lines.push(` Active rules ${padNum(s.activeRules)} (${s.suppressedRules} suppressed)`);
|
|
211
|
+
lines.push(` Corrections (total) ${padNum(s.correctionsTotal)} (+${s.corrections7d} last 7d)`);
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push(' Last 7 days');
|
|
214
|
+
// R9-PA2: ack rate = block→retract→pass 루프가 실제 작동한 비율.
|
|
215
|
+
const ackRateLabel = s.blocks7d > 0
|
|
216
|
+
? `(${Math.round((s.acks7d / s.blocks7d) * 100)}% acknowledged)`
|
|
217
|
+
: '';
|
|
218
|
+
lines.push(` Blocks ${padNum(s.blocks7d)} — times Claude was asked to retract ${ackRateLabel}`);
|
|
219
|
+
lines.push(` Acknowledgments ${padNum(s.acks7d)} — block → retract → pass loops`);
|
|
220
|
+
lines.push(` Bypass ${padNum(s.bypass7d)} — user overrides`);
|
|
221
|
+
lines.push(` Drift events ${padNum(s.drift7d)} — stuck-loop force-approves`);
|
|
222
|
+
lines.push(` Retired rules ${padNum(s.retired7d)} — superseded or timed out`);
|
|
223
|
+
lines.push('');
|
|
224
|
+
// H3: Assist 축 — enforcement 옆에 나란히 가시화.
|
|
225
|
+
lines.push(' Today (assist)');
|
|
226
|
+
lines.push(` Recall hits ${padNum(s.assistToday.recallHits)} — compound 매칭 시도 수`);
|
|
227
|
+
lines.push(` Surfaced ${padNum(s.assistToday.surfaced)} — 실제 주입된 솔루션 수`);
|
|
228
|
+
const ratio = s.assistToday.surfaced > 0
|
|
229
|
+
? ` (${Math.round(100 * s.assistToday.referenced / s.assistToday.surfaced)}% referenced)`
|
|
230
|
+
: '';
|
|
231
|
+
lines.push(` Referenced ${padNum(s.assistToday.referenced)} — Claude 응답에 인용됨${ratio}`);
|
|
232
|
+
lines.push(` Extracted ${padNum(s.assistToday.extractedToday)} — 오늘 새로 저장된 패턴`);
|
|
233
|
+
lines.push('');
|
|
234
|
+
// v0.4.1 철학 고도화 단면 — "개인화가 어디까지 학습됐나" 1섹션.
|
|
235
|
+
if (s.philosophy) {
|
|
236
|
+
lines.push(' Philosophy (learned)');
|
|
237
|
+
lines.push(` Base packs ${s.philosophy.basePacks.join(' / ')}`);
|
|
238
|
+
lines.push(` Trust policy ${s.philosophy.trustPolicy}`);
|
|
239
|
+
const scores = Object.entries(s.philosophy.axisScores)
|
|
240
|
+
.map(([k, v]) => `${k}:${v.toFixed(2)}`)
|
|
241
|
+
.join(' ');
|
|
242
|
+
if (scores)
|
|
243
|
+
lines.push(` Axis scores ${scores}`);
|
|
244
|
+
lines.push(` Last reclass ${s.philosophy.lastReclassification ?? 'never'}`);
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
247
|
+
lines.push(` Last extraction: ${s.lastExtraction}`);
|
|
248
|
+
lines.push('');
|
|
249
|
+
return lines.join('\n');
|
|
250
|
+
}
|
|
251
|
+
export async function handleStats(_args) {
|
|
252
|
+
const snap = computeStats();
|
|
253
|
+
console.log(renderStats(snap));
|
|
254
|
+
}
|
package/dist/core/uninstall.d.ts
CHANGED
package/dist/core/uninstall.js
CHANGED
|
@@ -291,8 +291,16 @@ export async function handleUninstall(cwd, options) {
|
|
|
291
291
|
console.log(' 4. Remove forgen block from CLAUDE.md');
|
|
292
292
|
console.log(' 5. Remove slash commands (~/.claude/commands/forgen/)');
|
|
293
293
|
console.log(' 6. Remove plugin artifacts (cache, installed_plugins.json, plugin directory)');
|
|
294
|
+
if (options.purge) {
|
|
295
|
+
console.log(' 7. --purge: Delete ~/.forgen/ entirely (rules, me/, state/, solutions/, behavior/)');
|
|
296
|
+
console.log(' WARNING: this erases all accumulated corrections, rules, drift, and lifecycle history.');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log('Note: ~/.forgen/ directory is preserved. Use --purge to also delete it.');
|
|
301
|
+
console.log(' (manual: rm -rf ~/.forgen)');
|
|
302
|
+
}
|
|
294
303
|
console.log('');
|
|
295
|
-
console.log('Note: ~/.forgen/ directory is preserved (manual deletion: rm -rf ~/.forgen)\n');
|
|
296
304
|
if (!options.force) {
|
|
297
305
|
if (!process.stdin.isTTY) {
|
|
298
306
|
console.error('[forgen] Use --force flag in non-interactive environments.');
|
|
@@ -311,5 +319,21 @@ export async function handleUninstall(cwd, options) {
|
|
|
311
319
|
cleanClaudeMd(cwd);
|
|
312
320
|
cleanSlashCommands();
|
|
313
321
|
cleanPluginArtifacts();
|
|
322
|
+
if (options.purge) {
|
|
323
|
+
try {
|
|
324
|
+
const { FORGEN_HOME } = await import('./paths.js');
|
|
325
|
+
const forgenHome = FORGEN_HOME;
|
|
326
|
+
if (fs.existsSync(forgenHome)) {
|
|
327
|
+
fs.rmSync(forgenHome, { recursive: true, force: true });
|
|
328
|
+
console.log(' ✓ Deleted ~/.forgen/ (all rules, state, solutions, behavior)');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.log(' ✓ ~/.forgen/ already absent');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
console.log(` ✗ ~/.forgen/ deletion failed: ${e.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
314
338
|
console.log('\n[forgen] Uninstall complete. Restart Claude Code for a clean state.\n');
|
|
315
339
|
}
|
|
@@ -19,7 +19,7 @@ import { FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_
|
|
|
19
19
|
import { checkLegacyProfile, runLegacyCutover } from './legacy-detector.js';
|
|
20
20
|
import { detectRuntimeCapability } from './runtime-detector.js';
|
|
21
21
|
import { loadProfile, profileExists } from '../store/profile-store.js';
|
|
22
|
-
import { loadActiveRules, cleanupStaleSessionRules } from '../store/rule-store.js';
|
|
22
|
+
import { loadActiveRules, cleanupStaleSessionRules, markRulesInjected } from '../store/rule-store.js';
|
|
23
23
|
import { composeSession } from '../preset/preset-manager.js';
|
|
24
24
|
import { renderRules, DEFAULT_CONTEXT } from '../renderer/rule-renderer.js';
|
|
25
25
|
import { saveSessionState, loadRecentSessions } from '../store/session-state-store.js';
|
|
@@ -82,6 +82,14 @@ export function bootstrapV1Session() {
|
|
|
82
82
|
// 6. Rule 렌더링
|
|
83
83
|
const allRules = [...personalRules];
|
|
84
84
|
const renderedRules = renderRules(allRules, session, profile, DEFAULT_CONTEXT);
|
|
85
|
+
// 6b. Inject tracking (ADR-002 Meta signal) — rendered rules count as injected.
|
|
86
|
+
// Fail-open: tracking failure must not block bootstrap.
|
|
87
|
+
try {
|
|
88
|
+
const injected = allRules.filter((r) => r.status === 'active').map((r) => r.rule_id);
|
|
89
|
+
if (injected.length > 0)
|
|
90
|
+
markRulesInjected(injected);
|
|
91
|
+
}
|
|
92
|
+
catch { /* ignore */ }
|
|
85
93
|
// 7. Mismatch 감지 (최근 3세션 rolling)
|
|
86
94
|
let mismatchResult = null;
|
|
87
95
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handler for `forgen classify-enforce [--apply] [--force]`.
|
|
3
|
+
*
|
|
4
|
+
* 기본: dry-run — 각 rule 의 제안만 출력. 변경 없음.
|
|
5
|
+
* --apply: 제안을 rule 파일에 저장 (enforce_via 미설정 rule 만).
|
|
6
|
+
* --force: enforce_via 가 이미 있어도 덮어쓴다.
|
|
7
|
+
*/
|
|
8
|
+
export declare function handleClassifyEnforce(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handler for `forgen classify-enforce [--apply] [--force]`.
|
|
3
|
+
*
|
|
4
|
+
* 기본: dry-run — 각 rule 의 제안만 출력. 변경 없음.
|
|
5
|
+
* --apply: 제안을 rule 파일에 저장 (enforce_via 미설정 rule 만).
|
|
6
|
+
* --force: enforce_via 가 이미 있어도 덮어쓴다.
|
|
7
|
+
*/
|
|
8
|
+
import { loadAllRules, saveRule } from '../store/rule-store.js';
|
|
9
|
+
import { classifyAll, applyProposal } from './enforce-classifier.js';
|
|
10
|
+
export async function handleClassifyEnforce(args) {
|
|
11
|
+
const apply = args.includes('--apply');
|
|
12
|
+
const force = args.includes('--force');
|
|
13
|
+
const rules = loadAllRules();
|
|
14
|
+
if (rules.length === 0) {
|
|
15
|
+
console.log('\n No rules in ~/.forgen/me/rules. Nothing to classify.\n');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const proposals = classifyAll(rules);
|
|
19
|
+
let saved = 0;
|
|
20
|
+
let skipped = 0;
|
|
21
|
+
let alreadySet = 0;
|
|
22
|
+
console.log(`\n Enforce Classifier — ${rules.length} rule(s) scanned\n`);
|
|
23
|
+
for (let i = 0; i < proposals.length; i++) {
|
|
24
|
+
const p = proposals[i];
|
|
25
|
+
const rule = rules[i];
|
|
26
|
+
const marker = p.current_enforce_via ? '↻' : '+';
|
|
27
|
+
console.log(` ${marker} ${p.rule_id.slice(0, 8)} "${p.trigger_preview}"`);
|
|
28
|
+
console.log(` strength=${rule.strength} status=${rule.status}`);
|
|
29
|
+
for (const spec of p.proposed) {
|
|
30
|
+
const vparts = [spec.verifier?.kind ?? 'none'];
|
|
31
|
+
if (spec.drift_key)
|
|
32
|
+
vparts.push(`drift_key=${spec.drift_key}`);
|
|
33
|
+
console.log(` → Mech-${spec.mech} @ ${spec.hook} verifier=${vparts.join(' ')}`);
|
|
34
|
+
}
|
|
35
|
+
for (const reason of p.reasoning) {
|
|
36
|
+
console.log(` · ${reason}`);
|
|
37
|
+
}
|
|
38
|
+
if (apply) {
|
|
39
|
+
if (p.current_enforce_via && p.current_enforce_via.length > 0 && !force) {
|
|
40
|
+
alreadySet += 1;
|
|
41
|
+
console.log(' (skipped — enforce_via already set; use --force to overwrite)');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const updated = applyProposal(rule, p, { force });
|
|
45
|
+
saveRule(updated);
|
|
46
|
+
saved += 1;
|
|
47
|
+
console.log(' (saved)');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
skipped += 1;
|
|
52
|
+
}
|
|
53
|
+
console.log('');
|
|
54
|
+
}
|
|
55
|
+
if (apply) {
|
|
56
|
+
console.log(` Summary: saved=${saved} already-set=${alreadySet} total=${rules.length}\n`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(` Summary: ${skipped} proposal(s) previewed. Run with --apply to save.\n`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -65,6 +65,7 @@ export function listSolutions() {
|
|
|
65
65
|
}
|
|
66
66
|
const order = ['mature', 'verified', 'candidate', 'experiment', 'retired'];
|
|
67
67
|
console.log('\n Compound Entries\n');
|
|
68
|
+
console.log(' (inj: 누적 주입, ref: 명시 참조, neg: 부정 피드백 — ref 측정은 v0.4.1+ 부터 시작됨)\n');
|
|
68
69
|
let total = 0;
|
|
69
70
|
for (const status of order) {
|
|
70
71
|
const group = groups[status];
|
|
@@ -33,14 +33,19 @@ export function isPathInside(parentWithSep, candidate) {
|
|
|
33
33
|
return resolved.startsWith(parentWithSep) && resolved !== parentWithSep;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* Count
|
|
37
|
-
*
|
|
36
|
+
* Count knowledge files in a directory (non-recursive). `.md` (solutions) + `.json`
|
|
37
|
+
* (rules, behavior, evidence) 둘 다 포함.
|
|
38
|
+
*
|
|
39
|
+
* v0.4.1 (2026-04-24): 이전에는 `.md` 만 세서 rules/*.json 전부가 count=0, export
|
|
40
|
+
* 대상에서도 제외됐다. 실증: `forgen compound export` 결과 rules 0, behavior 18
|
|
41
|
+
* (618건의 .json 누락). 사용자 철학의 **핵심**인 rules 가 하네스에서 빠진 상태
|
|
42
|
+
* 라 "나와 같은 데이터 하네스" 비전이 반쪽이었다.
|
|
38
43
|
*/
|
|
39
44
|
function countFiles(dir) {
|
|
40
45
|
try {
|
|
41
46
|
if (!fs.existsSync(dir))
|
|
42
47
|
return 0;
|
|
43
|
-
return fs.readdirSync(dir).filter(f => f.endsWith('.md')).length;
|
|
48
|
+
return fs.readdirSync(dir).filter(f => f.endsWith('.md') || f.endsWith('.json')).length;
|
|
44
49
|
}
|
|
45
50
|
catch {
|
|
46
51
|
return 0;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Enforce Classifier (ADR-001 §Migration)
|
|
3
|
+
*
|
|
4
|
+
* 기존 Rule 에 `enforce_via: EnforceSpec[]` 이 없을 때, trigger/policy 자연어
|
|
5
|
+
* 패턴과 strength 조합으로 mech(A/B/C) 와 hook 을 자동 제안한다.
|
|
6
|
+
*
|
|
7
|
+
* 휴리스틱 (ADR-001 §Migration heuristics):
|
|
8
|
+
* - trigger/policy 에 `rm|force|DROP|credentials|\.env` → Mech-A PreToolUse + tool_arg_regex
|
|
9
|
+
* - trigger/policy 에 `완료|complete|done|e2e|mock|verify` → Mech-A Stop + artifact_check
|
|
10
|
+
* - strength ∈ {strong, hard} + 문체/응답 맥락 → Mech-B UserPromptSubmit + self_check_prompt
|
|
11
|
+
* - 그 외 soft/default → Mech-C (drift 측정)
|
|
12
|
+
*
|
|
13
|
+
* 설계 원칙:
|
|
14
|
+
* - pure: classify(rule) 는 부수효과 없음. CLI 에서만 save 가 발생.
|
|
15
|
+
* - 미리 존재하는 enforce_via 는 덮어쓰지 않음 (`force=false` 기본).
|
|
16
|
+
* - 신규 제안은 reason 주석(문자열) 과 함께 반환해 사용자 리뷰 가능.
|
|
17
|
+
*/
|
|
18
|
+
import type { Rule, EnforceSpec } from '../store/types.js';
|
|
19
|
+
export interface EnforceProposal {
|
|
20
|
+
rule_id: string;
|
|
21
|
+
trigger_preview: string;
|
|
22
|
+
current_enforce_via: EnforceSpec[] | null;
|
|
23
|
+
proposed: EnforceSpec[];
|
|
24
|
+
reasoning: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function classify(rule: Rule): EnforceProposal;
|
|
27
|
+
export declare function classifyAll(rules: Rule[]): EnforceProposal[];
|
|
28
|
+
/** 제안을 적용해 새 Rule 을 반환 (pure). 이미 enforce_via 가 있으면 force=false 에서 건너뜀. */
|
|
29
|
+
export declare function applyProposal(rule: Rule, proposal: EnforceProposal, options?: {
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}): Rule;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Enforce Classifier (ADR-001 §Migration)
|
|
3
|
+
*
|
|
4
|
+
* 기존 Rule 에 `enforce_via: EnforceSpec[]` 이 없을 때, trigger/policy 자연어
|
|
5
|
+
* 패턴과 strength 조합으로 mech(A/B/C) 와 hook 을 자동 제안한다.
|
|
6
|
+
*
|
|
7
|
+
* 휴리스틱 (ADR-001 §Migration heuristics):
|
|
8
|
+
* - trigger/policy 에 `rm|force|DROP|credentials|\.env` → Mech-A PreToolUse + tool_arg_regex
|
|
9
|
+
* - trigger/policy 에 `완료|complete|done|e2e|mock|verify` → Mech-A Stop + artifact_check
|
|
10
|
+
* - strength ∈ {strong, hard} + 문체/응답 맥락 → Mech-B UserPromptSubmit + self_check_prompt
|
|
11
|
+
* - 그 외 soft/default → Mech-C (drift 측정)
|
|
12
|
+
*
|
|
13
|
+
* 설계 원칙:
|
|
14
|
+
* - pure: classify(rule) 는 부수효과 없음. CLI 에서만 save 가 발생.
|
|
15
|
+
* - 미리 존재하는 enforce_via 는 덮어쓰지 않음 (`force=false` 기본).
|
|
16
|
+
* - 신규 제안은 reason 주석(문자열) 과 함께 반환해 사용자 리뷰 가능.
|
|
17
|
+
*/
|
|
18
|
+
const DESTRUCTIVE_PATTERN = /\b(rm\s+-rf|rm\s+-fr|force|DROP\s+TABLE|credentials|\.env|sudo|mkfs|dd\s+if=)/i;
|
|
19
|
+
const COMPLETION_PATTERN = /(완료|complete|done|ready|shipped|finished|e2e|mock|verify|검증|배포)/i;
|
|
20
|
+
const STYLE_PATTERN = /(문체|응답|설명|톤|어투|장황|간결|verbose|tone|style)/i;
|
|
21
|
+
// R6-F2: shared single source of truth — stop-guard 와 동일 regex 재사용.
|
|
22
|
+
import { DEFAULT_STOP_TRIGGER_RE as STOP_COMPLETION_TRIGGER, DEFAULT_STOP_EXCLUDE_RE as STOP_COMPLETION_EXCLUDE, MOCK_TRIGGER_RE as STOP_MOCK_TRIGGER, MOCK_EXCLUDE_RE as STOP_MOCK_EXCLUDE, } from '../hooks/shared/stop-triggers.js';
|
|
23
|
+
export function classify(rule) {
|
|
24
|
+
const reasoning = [];
|
|
25
|
+
const proposed = [];
|
|
26
|
+
const text = `${rule.trigger}\n${rule.policy}`;
|
|
27
|
+
const isDestructive = DESTRUCTIVE_PATTERN.test(text);
|
|
28
|
+
const isCompletion = COMPLETION_PATTERN.test(text);
|
|
29
|
+
const isStyle = STYLE_PATTERN.test(text);
|
|
30
|
+
const isStrong = rule.strength === 'strong' || rule.strength === 'hard';
|
|
31
|
+
// Mech-A PreToolUse — 파괴적 명령 패턴.
|
|
32
|
+
// 이전에는 DESTRUCTIVE_PATTERN.source 를 다시 .match() 하여 alternation 의 첫 리터럴
|
|
33
|
+
// ("credentials") 만 반환하는 버그가 있었음. 이제 rule 텍스트에서 실제 매칭된 구문을
|
|
34
|
+
// 뽑아 그 구문에 맞는 runtime regex 로 변환.
|
|
35
|
+
if (isDestructive) {
|
|
36
|
+
const matched = text.match(DESTRUCTIVE_PATTERN);
|
|
37
|
+
const matchedLiteral = matched?.[0] ?? '';
|
|
38
|
+
// 안전을 위해 매칭된 literal 을 공백 보존 + escape 해서 runtime regex 로 재구성.
|
|
39
|
+
// 예: "rm -rf" → "rm\s+-rf" (공백 유연); "DROP TABLE" → "DROP\s+TABLE"; ".env" → "\.env"
|
|
40
|
+
const pattern = matchedLiteral
|
|
41
|
+
? matchedLiteral
|
|
42
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex metachar
|
|
43
|
+
.replace(/\s+/g, '\\s+') // 공백 하나 이상
|
|
44
|
+
: 'rm\\s+-rf'; // fallback
|
|
45
|
+
proposed.push({
|
|
46
|
+
mech: 'A',
|
|
47
|
+
hook: 'PreToolUse',
|
|
48
|
+
verifier: {
|
|
49
|
+
kind: 'tool_arg_regex',
|
|
50
|
+
params: { pattern, requires_flag: 'user_confirmed' },
|
|
51
|
+
},
|
|
52
|
+
block_message: `${rule.rule_id.slice(0, 8)}: ${rule.policy.slice(0, 80)}`,
|
|
53
|
+
});
|
|
54
|
+
reasoning.push(`destructive literal "${matchedLiteral}" → Mech-A PreToolUse+tool_arg_regex ${pattern}`);
|
|
55
|
+
}
|
|
56
|
+
// Mech-A Stop — 완료 선언 + 증거 요구 (destructive 와 독립적으로 평가: 하나의 rule 이 둘 다 해당 가능)
|
|
57
|
+
if (isCompletion) {
|
|
58
|
+
const mockAsProof = /mock|stub|fake/i.test(text);
|
|
59
|
+
// 증거 파일 경로는 v0.4.0 최종 구현에서 rule.policy 에서 추출; 지금은 default 사용
|
|
60
|
+
proposed.push({
|
|
61
|
+
mech: 'A',
|
|
62
|
+
hook: 'Stop',
|
|
63
|
+
verifier: {
|
|
64
|
+
kind: 'artifact_check',
|
|
65
|
+
params: { path: '.forgen/state/e2e-result.json', max_age_s: 3600 },
|
|
66
|
+
},
|
|
67
|
+
block_message: `${rule.rule_id.slice(0, 8)}: ${rule.policy.slice(0, 120)}`,
|
|
68
|
+
trigger_keywords_regex: mockAsProof ? STOP_MOCK_TRIGGER : STOP_COMPLETION_TRIGGER,
|
|
69
|
+
trigger_exclude_regex: mockAsProof ? STOP_MOCK_EXCLUDE : STOP_COMPLETION_EXCLUDE,
|
|
70
|
+
system_tag: `rule:${rule.rule_id.slice(0, 8)} — ${mockAsProof ? 'no-mock-as-proof' : 'e2e-before-done'}`,
|
|
71
|
+
});
|
|
72
|
+
reasoning.push(mockAsProof
|
|
73
|
+
? 'completion + mock keyword → Mech-A Stop+artifact_check (mock trigger)'
|
|
74
|
+
: 'completion keyword → Mech-A Stop+artifact_check (completion trigger)');
|
|
75
|
+
}
|
|
76
|
+
// Mech-B — 문체/응답 관련 또는 strong/hard 정책이지만 기계 판정 어려운 경우
|
|
77
|
+
if ((isStyle || (isStrong && !isDestructive && !isCompletion))) {
|
|
78
|
+
proposed.push({
|
|
79
|
+
mech: 'B',
|
|
80
|
+
hook: 'Stop',
|
|
81
|
+
verifier: {
|
|
82
|
+
kind: 'self_check_prompt',
|
|
83
|
+
params: {
|
|
84
|
+
question: `직전 응답이 다음 규칙을 위반했는지 자가점검하라: "${rule.policy.slice(0, 120)}". 위반 시 구체적 근거와 함께 수정해 재응답하라.`,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
trigger_keywords_regex: STOP_COMPLETION_TRIGGER,
|
|
88
|
+
trigger_exclude_regex: STOP_COMPLETION_EXCLUDE,
|
|
89
|
+
system_tag: `rule:${rule.rule_id.slice(0, 8)} — style-check`,
|
|
90
|
+
});
|
|
91
|
+
reasoning.push(isStyle ? 'style/tone keyword → Mech-B Stop+self_check_prompt' : 'strong/hard strength + non-mechanical → Mech-B Stop+self_check_prompt');
|
|
92
|
+
}
|
|
93
|
+
// 잔여 — drift measure only (Mech-C)
|
|
94
|
+
if (proposed.length === 0) {
|
|
95
|
+
proposed.push({
|
|
96
|
+
mech: 'C',
|
|
97
|
+
hook: 'PostToolUse',
|
|
98
|
+
drift_key: `rule.${rule.rule_id.slice(0, 8)}`,
|
|
99
|
+
});
|
|
100
|
+
reasoning.push('no direct enforcement pattern → Mech-C drift measurement');
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
rule_id: rule.rule_id,
|
|
104
|
+
trigger_preview: rule.trigger.slice(0, 60),
|
|
105
|
+
current_enforce_via: rule.enforce_via ?? null,
|
|
106
|
+
proposed,
|
|
107
|
+
reasoning,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function classifyAll(rules) {
|
|
111
|
+
return rules.map(classify);
|
|
112
|
+
}
|
|
113
|
+
/** 제안을 적용해 새 Rule 을 반환 (pure). 이미 enforce_via 가 있으면 force=false 에서 건너뜀. */
|
|
114
|
+
export function applyProposal(rule, proposal, options = {}) {
|
|
115
|
+
if (rule.enforce_via && rule.enforce_via.length > 0 && !options.force) {
|
|
116
|
+
return rule;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
...rule,
|
|
120
|
+
enforce_via: proposal.proposed,
|
|
121
|
+
updated_at: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
}
|
package/dist/engine/learn-cli.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
3
|
import { fixupSolutions } from './solution-fixup.js';
|
|
5
4
|
import { listQuarantined, pruneQuarantine } from './solution-quarantine.js';
|
|
6
5
|
import { computeFitness } from './solution-fitness.js';
|
|
7
6
|
import { buildWeaknessReport, saveWeaknessReport } from './solution-weakness.js';
|
|
8
7
|
import { listCandidates, promoteCandidate, rollbackSince } from './solution-candidate.js';
|
|
9
|
-
|
|
10
|
-
const STATE_DIR = path.join(os.homedir(), '.forgen', 'state');
|
|
11
|
-
const OUTCOMES_DIR = path.join(STATE_DIR, 'outcomes');
|
|
8
|
+
import { ME_SOLUTIONS, OUTCOMES_DIR } from '../core/paths.js';
|
|
12
9
|
export async function handleLearn(args) {
|
|
13
10
|
const sub = args[0];
|
|
14
11
|
if (sub === 'fix-up')
|