@wooojin/forgen 0.4.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +5 -5
- package/CHANGELOG.md +194 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +74 -9
- package/README.ko.md +77 -12
- package/README.md +127 -25
- package/README.zh.md +43 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- 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 +98 -6
- package/dist/core/auto-compound-runner.js +137 -49
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +41 -2
- package/dist/core/doctor.js +118 -5
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -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/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +11 -0
- package/dist/core/migrate-cli.js +53 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -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/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.d.ts +21 -0
- package/dist/core/stats-cli.js +133 -10
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- 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/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +5 -6
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- 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/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/context-guard.js +1 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +21 -5
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +2 -2
- 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 +7 -1
- package/dist/hooks/post-tool-use.js +50 -23
- package/dist/hooks/pre-compact.js +2 -2
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +28 -10
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +1 -1
- package/dist/hooks/session-recovery.js +12 -1
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +30 -2
- package/dist/hooks/shared/hook-response.js +61 -3
- package/dist/hooks/skill-injector.js +2 -2
- 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 +152 -13
- package/dist/hooks/subagent-tracker.js +1 -1
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/i18n/index.js +3 -5
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +45 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/rule-store.js +8 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +7 -5
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/calibrate.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/retro.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
package/dist/core/spawn.js
CHANGED
|
@@ -7,13 +7,11 @@ import { buildEnv } from './config-injector.js';
|
|
|
7
7
|
import { loadGlobalConfig } from './global-config.js';
|
|
8
8
|
import { createLogger } from './logger.js';
|
|
9
9
|
import { STATE_DIR } from './paths.js';
|
|
10
|
+
import { getHostRuntime } from '../host/host-runtime.js';
|
|
10
11
|
const log = createLogger('spawn');
|
|
11
|
-
/**
|
|
12
|
-
function findClaude() {
|
|
13
|
-
return 'claude';
|
|
14
|
-
}
|
|
12
|
+
/** Phase 2: host-runtime 어댑터 위임. */
|
|
15
13
|
function findRuntimeLauncher(runtime) {
|
|
16
|
-
return runtime
|
|
14
|
+
return getHostRuntime(runtime).launcher;
|
|
17
15
|
}
|
|
18
16
|
function transcriptProjectDir(cwd) {
|
|
19
17
|
// Claude Code는 cwd의 /를 -로 치환하고 선행 -를 유지
|
|
@@ -161,12 +159,7 @@ export async function spawnClaude(args, context, runtime = 'claude') {
|
|
|
161
159
|
});
|
|
162
160
|
child.on('error', (err) => {
|
|
163
161
|
if (err.code === 'ENOENT') {
|
|
164
|
-
|
|
165
|
-
reject(new Error('Codex is not installed.'));
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
reject(new Error('Claude Code is not installed. npm install -g @anthropic-ai/claude-code'));
|
|
169
|
-
}
|
|
162
|
+
reject(new Error(getHostRuntime(runtime).missingInstallMessage));
|
|
170
163
|
}
|
|
171
164
|
else {
|
|
172
165
|
reject(err);
|
package/dist/core/stats-cli.d.ts
CHANGED
|
@@ -9,6 +9,27 @@ export interface StatsSnapshot {
|
|
|
9
9
|
drift7d: number;
|
|
10
10
|
retired7d: number;
|
|
11
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
|
+
};
|
|
12
33
|
}
|
|
13
34
|
export declare function computeStats(): StatsSnapshot;
|
|
14
35
|
export declare function renderStats(s: StatsSnapshot): string;
|
package/dist/core/stats-cli.js
CHANGED
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
* working between Claude sessions.
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
|
-
import * as os from 'node:os';
|
|
10
9
|
import * as path from 'node:path';
|
|
11
10
|
import { loadAllRules } from '../store/rule-store.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
import { loadAllEvidence } from '../store/evidence-store.js';
|
|
12
|
+
import { STATE_DIR, ME_DIR } from './paths.js';
|
|
13
|
+
import { computeFixFeatRatio, formatFixRatio } from './git-stats.js';
|
|
14
|
+
// v0.4.1 격리 fix: 이전에는 os.homedir() 직접 사용해서 FORGEN_HOME env 로
|
|
15
|
+
// 홈 격리해도 이 파일의 경로는 여전히 실 홈 가리켰음. paths.ts 상수 import.
|
|
16
|
+
const ENFORCEMENT_DIR = path.join(STATE_DIR, 'enforcement');
|
|
17
|
+
const LIFECYCLE_DIR = path.join(STATE_DIR, 'lifecycle');
|
|
18
|
+
const SOLUTIONS_DIR = path.join(ME_DIR, 'solutions');
|
|
16
19
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
17
20
|
function readJsonl(p) {
|
|
18
21
|
if (!fs.existsSync(p))
|
|
@@ -61,12 +64,23 @@ function readLifecycleRetired(days) {
|
|
|
61
64
|
return n;
|
|
62
65
|
}
|
|
63
66
|
function readLastExtraction() {
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
// v0.4.1 파일명 정합: auto-compound-runner 는 last-auto-compound.json 에 기록.
|
|
68
|
+
// 이전 코드가 last-extraction.json 을 찾아 "never" 가 만성적으로 표시됨 —
|
|
69
|
+
// 실은 매 auto-compound 세션마다 값이 업데이트되고 있는데도 stats 에 반영 X.
|
|
70
|
+
const candidates = ['last-auto-compound.json', 'last-extraction.json'];
|
|
71
|
+
let p = null;
|
|
72
|
+
for (const name of candidates) {
|
|
73
|
+
const candidate = path.join(STATE_DIR, name);
|
|
74
|
+
if (fs.existsSync(candidate)) {
|
|
75
|
+
p = candidate;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!p)
|
|
66
80
|
return 'never';
|
|
67
81
|
try {
|
|
68
82
|
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
69
|
-
const ts = data.timestamp ?? data.date;
|
|
83
|
+
const ts = data.completedAt ?? data.timestamp ?? data.date;
|
|
70
84
|
if (!ts)
|
|
71
85
|
return 'never';
|
|
72
86
|
const diffDays = Math.floor((Date.now() - Date.parse(ts)) / MS_PER_DAY);
|
|
@@ -81,17 +95,90 @@ function readLastExtraction() {
|
|
|
81
95
|
return 'unknown';
|
|
82
96
|
}
|
|
83
97
|
}
|
|
98
|
+
/** H3: 오늘 (local midnight ~ now) 기준 assist 카운트. */
|
|
99
|
+
function computeAssistToday() {
|
|
100
|
+
const startOfDay = new Date();
|
|
101
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
102
|
+
const cutoffMs = startOfDay.getTime();
|
|
103
|
+
// recall hits: match-eval-log 의 오늘 entries
|
|
104
|
+
const matchLog = readJsonl(path.join(STATE_DIR, 'match-eval-log.jsonl'));
|
|
105
|
+
let recallHits = 0;
|
|
106
|
+
for (const e of matchLog) {
|
|
107
|
+
const ts = typeof e.ts === 'string' ? Date.parse(e.ts) : NaN;
|
|
108
|
+
if (Number.isFinite(ts) && ts >= cutoffMs)
|
|
109
|
+
recallHits++;
|
|
110
|
+
}
|
|
111
|
+
// surfaced + referenced — 같은 스트림 1회 loop 로.
|
|
112
|
+
const feedback = readJsonl(path.join(STATE_DIR, 'implicit-feedback.jsonl'));
|
|
113
|
+
let surfaced = 0;
|
|
114
|
+
let referenced = 0;
|
|
115
|
+
for (const e of feedback) {
|
|
116
|
+
const ts = typeof e.at === 'string' ? Date.parse(e.at) : NaN;
|
|
117
|
+
if (!Number.isFinite(ts) || ts < cutoffMs)
|
|
118
|
+
continue;
|
|
119
|
+
if (e.type === 'recommendation_surfaced')
|
|
120
|
+
surfaced++;
|
|
121
|
+
else if (e.type === 'recall_referenced')
|
|
122
|
+
referenced++;
|
|
123
|
+
}
|
|
124
|
+
// extracted today: solutions dir 에서 오늘 mtime 인 .md 파일
|
|
125
|
+
let extractedToday = 0;
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(SOLUTIONS_DIR)) {
|
|
128
|
+
for (const f of fs.readdirSync(SOLUTIONS_DIR)) {
|
|
129
|
+
if (!f.endsWith('.md'))
|
|
130
|
+
continue;
|
|
131
|
+
const stat = fs.statSync(path.join(SOLUTIONS_DIR, f));
|
|
132
|
+
if (stat.mtimeMs >= cutoffMs)
|
|
133
|
+
extractedToday++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch { /* fail-open */ }
|
|
138
|
+
return { recallHits, surfaced, referenced, extractedToday };
|
|
139
|
+
}
|
|
140
|
+
/** v0.4.1: forge-profile 에서 고도화 지표 추출. 파일 없거나 깨지면 undefined. */
|
|
141
|
+
function computePhilosophy() {
|
|
142
|
+
try {
|
|
143
|
+
const profilePath = path.join(ME_DIR, 'forge-profile.json');
|
|
144
|
+
if (!fs.existsSync(profilePath))
|
|
145
|
+
return undefined;
|
|
146
|
+
const d = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
147
|
+
const axisScores = {};
|
|
148
|
+
for (const [k, v] of Object.entries(d.axes ?? {})) {
|
|
149
|
+
if (v && typeof v.score === 'number')
|
|
150
|
+
axisScores[k] = v.score;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
basePacks: Object.values(d.base_packs ?? {}),
|
|
154
|
+
trustPolicy: d.trust_preferences?.desired_policy ?? 'unknown',
|
|
155
|
+
axisScores,
|
|
156
|
+
lastReclassification: d.metadata?.last_reclassification_at ?? null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
84
163
|
export function computeStats() {
|
|
85
164
|
const rules = loadAllRules();
|
|
86
165
|
const activeRules = rules.filter((r) => r.status === 'active').length;
|
|
87
166
|
const suppressedRules = rules.filter((r) => r.status === 'suppressed').length;
|
|
88
|
-
|
|
167
|
+
// v0.4.1 정확도 수정: loadRecentEvidence(500) 제한은 "Corrections (total)"
|
|
168
|
+
// 라벨과 모순 — 실 behavior 618건 중 118건 누락됐음. 전체 evidence 스캔으로
|
|
169
|
+
// 교체. explicit_correction 만 filter 이므로 memory overhead 는 N * ~1KB 수준.
|
|
170
|
+
const evidence = loadAllEvidence();
|
|
89
171
|
const corrections = evidence.filter((e) => e.type === 'explicit_correction');
|
|
90
172
|
const correctionsTotal = corrections.length;
|
|
91
173
|
const cutoff7d = Date.now() - 7 * MS_PER_DAY;
|
|
92
174
|
const corrections7d = corrections.filter((e) => Date.parse(e.timestamp) >= cutoff7d).length;
|
|
93
175
|
const violations = readJsonl(path.join(ENFORCEMENT_DIR, 'violations.jsonl'));
|
|
94
|
-
|
|
176
|
+
// v0.4.1 historical false-positive 제거: pre-0.4.1 bypass-detector 가 Write/Edit
|
|
177
|
+
// content 의 quote 본문까지 raw 매칭해서 bypass 로 오기록. 실 관찰: L1-no-rm-rf
|
|
178
|
+
// -unconfirmed bypass 20건 중 Write/Edit 15건. stats 표시는 **실 실행 맥락** 인
|
|
179
|
+
// Bash/Agent/기타만 집계 — 앞으로의 시계열 일관성 + 과거 noise 제거.
|
|
180
|
+
const bypassRaw = readJsonl(path.join(ENFORCEMENT_DIR, 'bypass.jsonl'));
|
|
181
|
+
const bypass = bypassRaw.filter((e) => e.tool !== 'Write' && e.tool !== 'Edit');
|
|
95
182
|
const drift = readJsonl(path.join(ENFORCEMENT_DIR, 'drift.jsonl'));
|
|
96
183
|
const acks = readJsonl(path.join(ENFORCEMENT_DIR, 'acknowledgments.jsonl'));
|
|
97
184
|
// R9-PA2: violations 는 'block' (stop-guard/post-tool) + 'deny' (pre-tool Mech-A)
|
|
@@ -109,6 +196,8 @@ export function computeStats() {
|
|
|
109
196
|
drift7d: countWithin(drift, 7),
|
|
110
197
|
retired7d: readLifecycleRetired(7),
|
|
111
198
|
lastExtraction: readLastExtraction(),
|
|
199
|
+
assistToday: computeAssistToday(),
|
|
200
|
+
philosophy: computePhilosophy(),
|
|
112
201
|
};
|
|
113
202
|
}
|
|
114
203
|
function padNum(n, width = 4) {
|
|
@@ -133,6 +222,40 @@ export function renderStats(s) {
|
|
|
133
222
|
lines.push(` Drift events ${padNum(s.drift7d)} — stuck-loop force-approves`);
|
|
134
223
|
lines.push(` Retired rules ${padNum(s.retired7d)} — superseded or timed out`);
|
|
135
224
|
lines.push('');
|
|
225
|
+
// H3: Assist 축 — enforcement 옆에 나란히 가시화.
|
|
226
|
+
lines.push(' Today (assist)');
|
|
227
|
+
lines.push(` Recall hits ${padNum(s.assistToday.recallHits)} — compound 매칭 시도 수`);
|
|
228
|
+
lines.push(` Surfaced ${padNum(s.assistToday.surfaced)} — 실제 주입된 솔루션 수`);
|
|
229
|
+
const ratio = s.assistToday.surfaced > 0
|
|
230
|
+
? ` (${Math.round(100 * s.assistToday.referenced / s.assistToday.surfaced)}% referenced)`
|
|
231
|
+
: '';
|
|
232
|
+
lines.push(` Referenced ${padNum(s.assistToday.referenced)} — Claude 응답에 인용됨${ratio}`);
|
|
233
|
+
lines.push(` Extracted ${padNum(s.assistToday.extractedToday)} — 오늘 새로 저장된 패턴`);
|
|
234
|
+
lines.push('');
|
|
235
|
+
// v0.4.1 철학 고도화 단면 — "개인화가 어디까지 학습됐나" 1섹션.
|
|
236
|
+
if (s.philosophy) {
|
|
237
|
+
lines.push(' Philosophy (learned)');
|
|
238
|
+
lines.push(` Base packs ${s.philosophy.basePacks.join(' / ')}`);
|
|
239
|
+
lines.push(` Trust policy ${s.philosophy.trustPolicy}`);
|
|
240
|
+
const scores = Object.entries(s.philosophy.axisScores)
|
|
241
|
+
.map(([k, v]) => `${k}:${v.toFixed(2)}`)
|
|
242
|
+
.join(' ');
|
|
243
|
+
if (scores)
|
|
244
|
+
lines.push(` Axis scores ${scores}`);
|
|
245
|
+
lines.push(` Last reclass ${s.philosophy.lastReclassification ?? 'never'}`);
|
|
246
|
+
lines.push('');
|
|
247
|
+
}
|
|
248
|
+
// P4 셀프 가드 — 최근 30커밋 fix:feat 비율로 회귀 패턴 자가 노출.
|
|
249
|
+
// 30% 초과 시 "이거 고치면 저거 버그난다" 패턴 의심 → forgen doctor 가 경고.
|
|
250
|
+
try {
|
|
251
|
+
const ratio = computeFixFeatRatio();
|
|
252
|
+
if (ratio.available) {
|
|
253
|
+
lines.push(' Repo health (last 30 commits)');
|
|
254
|
+
lines.push(` ${formatFixRatio(ratio)}`);
|
|
255
|
+
lines.push('');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch { /* fail-open: git 없거나 비-repo 환경 */ }
|
|
136
259
|
lines.push(` Last extraction: ${s.lastExtraction}`);
|
|
137
260
|
lines.push('');
|
|
138
261
|
return lines.join('\n');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Layer Intent — Multi-Host Core Design §9.0 산출물 #1
|
|
3
|
+
*
|
|
4
|
+
* forgen 이 host 위에서 보장하는 행동의 enum. spec §9.0 의 7 의도 매트릭스와 1:1.
|
|
5
|
+
* 각 host adapter 는 이 enum 의 모든 항목에 대해 CapabilityDeclaration 을 선언해야 하며,
|
|
6
|
+
* 미선언은 컴파일 타임(`Record<TrustLayerIntent, _>`) + 런타임(`assertCapabilitiesComplete`) 양쪽에서 fail.
|
|
7
|
+
*
|
|
8
|
+
* 1원칙: Claude semantics 가 reference. 본 enum 의 의미는 Claude Hook schema 의 행동을 그대로 사용한다.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TRUST_LAYER_INTENTS: readonly ["block-completion", "block-tool-use", "inject-context", "observe-only", "secret-filter", "forge-loop-state-inject", "self-evidence-record"];
|
|
11
|
+
export type TrustLayerIntent = (typeof TRUST_LAYER_INTENTS)[number];
|
|
12
|
+
export type CapabilityStatus = 'supported' | 'partial' | 'unsupported';
|
|
13
|
+
export interface CapabilityDeclaration {
|
|
14
|
+
readonly status: CapabilityStatus;
|
|
15
|
+
/** host 표면이 이 의도를 표현하는 hook/필드 (예: "Stop + decision:'block' + reason"). */
|
|
16
|
+
readonly expression: string;
|
|
17
|
+
/** partial/unsupported 시 등가성 보존을 위한 mitigation 핸들. supported 면 undefined. */
|
|
18
|
+
readonly mitigation?: string;
|
|
19
|
+
/** source-of-truth (spec 또는 외부 docs/source 인용). */
|
|
20
|
+
readonly source?: string;
|
|
21
|
+
}
|
|
22
|
+
export type HostId = 'claude' | 'codex';
|
|
23
|
+
export interface HostCapabilities {
|
|
24
|
+
readonly hostId: HostId;
|
|
25
|
+
/**
|
|
26
|
+
* 모든 TrustLayerIntent 에 대한 선언. `Record<TrustLayerIntent, _>` 타입이
|
|
27
|
+
* 컴파일 타임에 누락을 차단한다.
|
|
28
|
+
*/
|
|
29
|
+
readonly intents: Record<TrustLayerIntent, CapabilityDeclaration>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 런타임 assertion — host adapter 가 새 의도 추가를 누락한 경우 fail.
|
|
33
|
+
* 컴파일 타임 가드를 우회하는 동적 생성 코드를 위한 안전망.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertCapabilitiesComplete(caps: HostCapabilities): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Layer Intent — Multi-Host Core Design §9.0 산출물 #1
|
|
3
|
+
*
|
|
4
|
+
* forgen 이 host 위에서 보장하는 행동의 enum. spec §9.0 의 7 의도 매트릭스와 1:1.
|
|
5
|
+
* 각 host adapter 는 이 enum 의 모든 항목에 대해 CapabilityDeclaration 을 선언해야 하며,
|
|
6
|
+
* 미선언은 컴파일 타임(`Record<TrustLayerIntent, _>`) + 런타임(`assertCapabilitiesComplete`) 양쪽에서 fail.
|
|
7
|
+
*
|
|
8
|
+
* 1원칙: Claude semantics 가 reference. 본 enum 의 의미는 Claude Hook schema 의 행동을 그대로 사용한다.
|
|
9
|
+
*/
|
|
10
|
+
export const TRUST_LAYER_INTENTS = [
|
|
11
|
+
'block-completion',
|
|
12
|
+
'block-tool-use',
|
|
13
|
+
'inject-context',
|
|
14
|
+
'observe-only',
|
|
15
|
+
'secret-filter',
|
|
16
|
+
'forge-loop-state-inject',
|
|
17
|
+
'self-evidence-record',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* 런타임 assertion — host adapter 가 새 의도 추가를 누락한 경우 fail.
|
|
21
|
+
* 컴파일 타임 가드를 우회하는 동적 생성 코드를 위한 안전망.
|
|
22
|
+
*/
|
|
23
|
+
export function assertCapabilitiesComplete(caps) {
|
|
24
|
+
const declared = new Set(Object.keys(caps.intents));
|
|
25
|
+
const missing = TRUST_LAYER_INTENTS.filter((i) => !declared.has(i));
|
|
26
|
+
if (missing.length > 0) {
|
|
27
|
+
throw new Error(`HostCapabilities for "${caps.hostId}" missing intents: ${missing.join(', ')}. ` +
|
|
28
|
+
`All TrustLayerIntent values must be declared (spec §9.0).`);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ export type RuntimeHost = 'claude' | 'codex';
|
|
|
112
112
|
export interface LaunchContext {
|
|
113
113
|
runtime: RuntimeHost;
|
|
114
114
|
args: string[];
|
|
115
|
-
runtimeSource: 'flag' | 'env' | 'default';
|
|
115
|
+
runtimeSource: 'flag' | 'env' | 'profile' | 'default';
|
|
116
116
|
}
|
|
117
117
|
/** 훅 입력 이벤트 스키마 (버전 간 상위 호환용 최소 스펙) */
|
|
118
118
|
export interface HookEventInput {
|
package/dist/core/uninstall.js
CHANGED
|
@@ -321,7 +321,8 @@ export async function handleUninstall(cwd, options) {
|
|
|
321
321
|
cleanPluginArtifacts();
|
|
322
322
|
if (options.purge) {
|
|
323
323
|
try {
|
|
324
|
-
const
|
|
324
|
+
const { FORGEN_HOME } = await import('./paths.js');
|
|
325
|
+
const forgenHome = FORGEN_HOME;
|
|
325
326
|
if (fs.existsSync(forgenHome)) {
|
|
326
327
|
fs.rmSync(forgenHome, { recursive: true, force: true });
|
|
327
328
|
console.log(' ✓ Deleted ~/.forgen/ (all rules, state, solutions, behavior)');
|
|
@@ -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;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import * as fs from 'node:fs';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
|
+
import { execHost } from '../host/exec-host.js';
|
|
21
22
|
import { serializeSolutionV3, DEFAULT_EVIDENCE, extractTags } from './solution-format.js';
|
|
22
23
|
import { createLogger } from '../core/logger.js';
|
|
23
24
|
const log = createLogger('compound-extractor');
|
|
@@ -655,15 +656,12 @@ function enrichSolutionContent(solution, diffSnippet) {
|
|
|
655
656
|
'코드 변경 (일부):',
|
|
656
657
|
diffSnippet.slice(0, 2000),
|
|
657
658
|
].join('\n');
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (enriched.length > 30 && enriched.length < 1000) {
|
|
665
|
-
return enriched;
|
|
666
|
-
}
|
|
659
|
+
// feat/codex-support P2-2 — host-aware exec via profile.default_host.
|
|
660
|
+
// Codex 메인 사용자도 자동 추출 enrichment 가능 (해당 host CLI 호출).
|
|
661
|
+
// fail-open 정책 유지 — LLM enrichment 실패는 추출 자체를 막지 않음.
|
|
662
|
+
const { message } = execHost({ prompt, model: 'haiku', timeout: 15000 });
|
|
663
|
+
if (message.length > 30 && message.length < 1000)
|
|
664
|
+
return message;
|
|
667
665
|
return null;
|
|
668
666
|
}
|
|
669
667
|
catch {
|
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')
|
|
@@ -150,10 +147,12 @@ function runEvolve(args) {
|
|
|
150
147
|
const rollbackIdx = args.indexOf('--rollback');
|
|
151
148
|
const promoteIdx = args.indexOf('--promote');
|
|
152
149
|
if (rollbackIdx >= 0 && args[rollbackIdx + 1]) {
|
|
153
|
-
|
|
150
|
+
runEvolveRollback(args[rollbackIdx + 1]);
|
|
151
|
+
return;
|
|
154
152
|
}
|
|
155
153
|
if (promoteIdx >= 0 && args[promoteIdx + 1]) {
|
|
156
|
-
|
|
154
|
+
runEvolvePromote(args[promoteIdx + 1]);
|
|
155
|
+
return;
|
|
157
156
|
}
|
|
158
157
|
// Default: generate + optionally save weakness report, print proposer
|
|
159
158
|
// brief so the user can hand it to the ch-solution-evolver agent.
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* Rule.policy 자연어에서 "피해야 할 패턴" 을 추출하고, Write/Edit/Bash 도구
|
|
5
5
|
* 출력에서 해당 패턴을 찾아 BypassEntry 후보로 반환한다.
|
|
6
6
|
*
|
|
7
|
-
* Heuristic:
|
|
7
|
+
* Heuristic priority (most explicit first):
|
|
8
|
+
* 0) Parenthesized examples (e.g., "(rm -rf, DROP, force-push)") → tokens inside
|
|
8
9
|
* 1) "use X not Y" / "use X instead of Y" / "X over Y" → bypass = Y
|
|
9
10
|
* 2) "avoid X" / "don't use X" / "never use X" / "do not use X" → bypass = X
|
|
10
11
|
* 3) Korean: "X 말라" / "X 금지" / "X 하지 않" → bypass = X
|
|
11
12
|
* 4) 그 외: 빈 배열 (탐지 불가).
|
|
12
13
|
*
|
|
14
|
+
* Stop list filter: generic Korean verbs (실행/사용/선언/...) extracted by Korean
|
|
15
|
+
* heuristic are removed — they cause massive FP (RC5/E9: matched the word "실행"
|
|
16
|
+
* everywhere instead of "rm -rf"). 64 false-positive bypasses observed before fix.
|
|
17
|
+
*
|
|
13
18
|
* 반환된 패턴은 escape 된 정규식 문자열 — caller 가 `new RegExp(p)` 로 사용.
|
|
14
19
|
*/
|
|
15
20
|
import type { Rule } from '../../store/types.js';
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* Rule.policy 자연어에서 "피해야 할 패턴" 을 추출하고, Write/Edit/Bash 도구
|
|
5
5
|
* 출력에서 해당 패턴을 찾아 BypassEntry 후보로 반환한다.
|
|
6
6
|
*
|
|
7
|
-
* Heuristic:
|
|
7
|
+
* Heuristic priority (most explicit first):
|
|
8
|
+
* 0) Parenthesized examples (e.g., "(rm -rf, DROP, force-push)") → tokens inside
|
|
8
9
|
* 1) "use X not Y" / "use X instead of Y" / "X over Y" → bypass = Y
|
|
9
10
|
* 2) "avoid X" / "don't use X" / "never use X" / "do not use X" → bypass = X
|
|
10
11
|
* 3) Korean: "X 말라" / "X 금지" / "X 하지 않" → bypass = X
|
|
11
12
|
* 4) 그 외: 빈 배열 (탐지 불가).
|
|
12
13
|
*
|
|
14
|
+
* Stop list filter: generic Korean verbs (실행/사용/선언/...) extracted by Korean
|
|
15
|
+
* heuristic are removed — they cause massive FP (RC5/E9: matched the word "실행"
|
|
16
|
+
* everywhere instead of "rm -rf"). 64 false-positive bypasses observed before fix.
|
|
17
|
+
*
|
|
13
18
|
* 반환된 패턴은 escape 된 정규식 문자열 — caller 가 `new RegExp(p)` 로 사용.
|
|
14
19
|
*/
|
|
15
20
|
function escapeRegex(s) {
|
|
@@ -29,9 +34,50 @@ function trimPunct(s) {
|
|
|
29
34
|
out = out.replace(/^[,;:!?"'`(]+|[.,;:!?"'`)]+$/g, '');
|
|
30
35
|
return out;
|
|
31
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Generic Korean verbs/words that produce massive false positives if used as
|
|
39
|
+
* bypass patterns (RC5/E9 fix). Extending requires retro evidence.
|
|
40
|
+
*/
|
|
41
|
+
const KO_GENERIC_STOP_WORDS = new Set([
|
|
42
|
+
'실행', '사용', '선언', '수행', '처리', '작성', '호출', '적용',
|
|
43
|
+
'실행하지', '사용하지', '선언하지', '수행하지', '처리하지',
|
|
44
|
+
// English fallthroughs (already low value as bypass signals)
|
|
45
|
+
'use', 'do', 'execute',
|
|
46
|
+
]);
|
|
47
|
+
/** Korean markers that signal the parenthesized content is NOT an example list. */
|
|
48
|
+
const KO_NON_EXAMPLE_MARKERS = ['제외', '한정', '예외', '단서', 'except'];
|
|
49
|
+
/** Extract concrete tokens inside parenthesized example list. */
|
|
50
|
+
function extractParenthesizedExamples(p) {
|
|
51
|
+
const out = [];
|
|
52
|
+
// Match (...) groups; multiple groups in policy are uncommon but supported
|
|
53
|
+
const re = /\(([^)]+)\)/g;
|
|
54
|
+
let m;
|
|
55
|
+
while ((m = re.exec(p))) {
|
|
56
|
+
const inside = m[1];
|
|
57
|
+
// Skip if it looks like a path (contains "/" before any obvious separator commitment)
|
|
58
|
+
if (/[a-zA-Z]+\/[a-zA-Z]/.test(inside))
|
|
59
|
+
continue;
|
|
60
|
+
// Skip if it's an exclusion / scope-restriction note (Korean markers)
|
|
61
|
+
if (KO_NON_EXAMPLE_MARKERS.some((mk) => inside.includes(mk)))
|
|
62
|
+
continue;
|
|
63
|
+
// Skip if any single segment is suspiciously long (full sentence rather than token)
|
|
64
|
+
const segs = inside.split(/[,]|\s+(?:or|와|및)\s+/i).map((s) => s.trim());
|
|
65
|
+
if (segs.some((s) => s.length > 30))
|
|
66
|
+
continue;
|
|
67
|
+
const tokens = segs
|
|
68
|
+
.map((t) => trimPunct(t))
|
|
69
|
+
.filter((t) => t.length >= 2 && !KO_GENERIC_STOP_WORDS.has(t));
|
|
70
|
+
out.push(...tokens);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
32
74
|
export function extractBypassPatterns(rule) {
|
|
33
75
|
const patterns = [];
|
|
34
76
|
const p = rule.policy;
|
|
77
|
+
// 0) Parenthesized examples (highest priority — explicit signal)
|
|
78
|
+
for (const ex of extractParenthesizedExamples(p)) {
|
|
79
|
+
patterns.push(escapeRegex(ex));
|
|
80
|
+
}
|
|
35
81
|
// use X not Y / use X instead of Y / use X over Y
|
|
36
82
|
// X, Y may contain dots (e.g., ".then()", "vi.mock"). Strip trailing punctuation.
|
|
37
83
|
const useNot = p.match(/\b(?:use|prefer|choose)\s+(\S+?)\s+(?:not|instead\s+of|over|rather\s+than)\s+(\S+)/i);
|
|
@@ -43,10 +89,16 @@ export function extractBypassPatterns(rule) {
|
|
|
43
89
|
patterns.push(escapeRegex(trimPunct(avoid[1])));
|
|
44
90
|
// Korean: "X 말라" / "X 금지" / "X 하지 마"
|
|
45
91
|
const ko = p.match(/(\S+)\s*(?:말라|금지|하지\s*마|쓰지\s*마)/);
|
|
46
|
-
if (ko)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
92
|
+
if (ko) {
|
|
93
|
+
const candidate = trimPunct(ko[1]);
|
|
94
|
+
if (!KO_GENERIC_STOP_WORDS.has(candidate)) {
|
|
95
|
+
patterns.push(escapeRegex(candidate));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Dedupe + filter trivial + filter stop-words (defense in depth)
|
|
99
|
+
return [...new Set(patterns)]
|
|
100
|
+
.filter((pat) => pat.length >= 2)
|
|
101
|
+
.filter((pat) => !KO_GENERIC_STOP_WORDS.has(pat.replace(/\\/g, '')));
|
|
50
102
|
}
|
|
51
103
|
/**
|
|
52
104
|
* Pure — rules + tool output 으로 bypass candidates 추출.
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* default dry-run, --apply 시 rule 파일에 상태 전이 반영.
|
|
6
6
|
*/
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
-
import * as os from 'node:os';
|
|
9
8
|
import { loadAllRules, saveRule } from '../../store/rule-store.js';
|
|
9
|
+
import { STATE_DIR } from '../../core/paths.js';
|
|
10
10
|
import { collectSignals, readJsonlSafe } from './signals.js';
|
|
11
11
|
import { detect as detectT2 } from './trigger-t2-violation.js';
|
|
12
12
|
import { detect as detectT3 } from './trigger-t3-bypass.js';
|
|
@@ -14,7 +14,7 @@ import { detect as detectT4 } from './trigger-t4-decay.js';
|
|
|
14
14
|
import { detect as detectT5 } from './trigger-t5-conflict.js';
|
|
15
15
|
import { scanDriftForDemotion, applyDemotion, scanSignalsForPromotion, applyPromotion, readDriftEntries, appendLifecycleEvents, } from './meta-reclassifier.js';
|
|
16
16
|
import { foldEvents } from './orchestrator.js';
|
|
17
|
-
const LIFECYCLE_DIR = path.join(
|
|
17
|
+
const LIFECYCLE_DIR = path.join(STATE_DIR, 'lifecycle');
|
|
18
18
|
export async function handleLifecycleScan(args) {
|
|
19
19
|
const apply = args.includes('--apply');
|
|
20
20
|
const now = Date.now();
|
|
@@ -23,8 +23,8 @@ export async function handleLifecycleScan(args) {
|
|
|
23
23
|
console.log('\n No rules in ~/.forgen/me/rules. Nothing to scan.\n');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
|
-
const violations = readJsonlSafe(path.join(
|
|
27
|
-
const bypass = readJsonlSafe(path.join(
|
|
26
|
+
const violations = readJsonlSafe(path.join(STATE_DIR, 'enforcement', 'violations.jsonl'));
|
|
27
|
+
const bypass = readJsonlSafe(path.join(STATE_DIR, 'enforcement', 'bypass.jsonl'));
|
|
28
28
|
const drift = readDriftEntries();
|
|
29
29
|
const signals = new Map();
|
|
30
30
|
for (const r of rules)
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
* - 30 일 쿨다운.
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from 'node:fs';
|
|
19
|
-
import * as os from 'node:os';
|
|
20
19
|
import * as path from 'node:path';
|
|
21
20
|
import { initLifecycle } from '../../store/rule-lifecycle.js';
|
|
22
21
|
import { loadAllRules, saveRule } from '../../store/rule-store.js';
|
|
23
|
-
|
|
24
|
-
const
|
|
22
|
+
import { STATE_DIR } from '../../core/paths.js';
|
|
23
|
+
const DRIFT_LOG_PATH = path.join(STATE_DIR, 'enforcement', 'drift.jsonl');
|
|
24
|
+
const LIFECYCLE_DIR = path.join(STATE_DIR, 'lifecycle');
|
|
25
25
|
const DEFAULT_WINDOW_DAYS = 7;
|
|
26
26
|
const DEFAULT_THRESHOLD = 3;
|
|
27
27
|
/** R8-A1: Meta 재분류 쿨다운 (일). 최근 이 기간 내 변경된 rule 은 다시 재분류 금지. */
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
* promote/demote_mech → phase 유지, meta_promotions 는 meta-reclassifier 가 직접 기록
|
|
22
22
|
*/
|
|
23
23
|
import * as fs from 'node:fs';
|
|
24
|
-
import * as os from 'node:os';
|
|
25
24
|
import * as path from 'node:path';
|
|
25
|
+
import { STATE_DIR } from '../../core/paths.js';
|
|
26
26
|
/**
|
|
27
27
|
* R5-B1: rule 이 inactive 상태로 전이될 때 block-count 디렉터리의 잔여 파일 정리.
|
|
28
28
|
* phantom stuck-loop (retired 된 rule 이 다시 GC 전까지 counter 에 반영되는 문제) 차단.
|
|
29
29
|
*/
|
|
30
30
|
function sweepBlockCountsForRule(ruleId) {
|
|
31
31
|
try {
|
|
32
|
-
const dir = path.join(
|
|
32
|
+
const dir = path.join(STATE_DIR, 'enforcement', 'block-count');
|
|
33
33
|
if (!fs.existsSync(dir))
|
|
34
34
|
return;
|
|
35
35
|
const safeRuleId = String(ruleId).replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* 모든 IO 는 이 파일에 한정. 트리거들은 pure — collectSignals() 결과를 받아 detect().
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
|
-
import * as os from 'node:os';
|
|
13
12
|
import * as path from 'node:path';
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
13
|
+
import { STATE_DIR as FORGEN_STATE_DIR } from '../../core/paths.js';
|
|
14
|
+
const ENFORCEMENT_DIR = path.join(FORGEN_STATE_DIR, 'enforcement');
|
|
15
|
+
const VIOLATIONS_PATH = path.join(ENFORCEMENT_DIR, 'violations.jsonl');
|
|
16
|
+
const BYPASS_PATH = path.join(ENFORCEMENT_DIR, 'bypass.jsonl');
|
|
17
17
|
const ROLLING_N = 20;
|
|
18
18
|
const VIOLATION_WINDOW_DAYS = 30;
|
|
19
19
|
const BYPASS_WINDOW_DAYS = 7;
|
|
@@ -59,7 +59,7 @@ export function readJsonlSafe(p) {
|
|
|
59
59
|
}
|
|
60
60
|
export function recordViolation(entry) {
|
|
61
61
|
try {
|
|
62
|
-
fs.mkdirSync(
|
|
62
|
+
fs.mkdirSync(ENFORCEMENT_DIR, { recursive: true });
|
|
63
63
|
rotateIfBig(VIOLATIONS_PATH);
|
|
64
64
|
const full = { at: new Date().toISOString(), ...entry };
|
|
65
65
|
fs.appendFileSync(VIOLATIONS_PATH, JSON.stringify(full) + '\n');
|
|
@@ -73,7 +73,7 @@ export function recordViolation(entry) {
|
|
|
73
73
|
}
|
|
74
74
|
export function recordBypass(entry) {
|
|
75
75
|
try {
|
|
76
|
-
fs.mkdirSync(
|
|
76
|
+
fs.mkdirSync(ENFORCEMENT_DIR, { recursive: true });
|
|
77
77
|
rotateIfBig(BYPASS_PATH);
|
|
78
78
|
const full = { at: new Date().toISOString(), ...entry };
|
|
79
79
|
fs.appendFileSync(BYPASS_PATH, JSON.stringify(full) + '\n');
|