@wooojin/forgen 0.3.1 → 0.4.0
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 +7 -2
- package/CHANGELOG.md +164 -0
- package/README.ja.md +90 -7
- package/README.ko.md +44 -1
- package/README.md +128 -9
- package/README.zh.md +90 -7
- package/dist/cli.js +140 -8
- package/dist/core/auto-compound-runner.js +16 -5
- package/dist/core/dashboard.js +11 -4
- package/dist/core/doctor.d.ts +6 -1
- package/dist/core/doctor.js +85 -11
- package/dist/core/global-config.d.ts +2 -2
- package/dist/core/global-config.js +6 -14
- package/dist/core/harness.d.ts +3 -5
- package/dist/core/harness.js +34 -338
- package/dist/core/inspect-cli.js +65 -5
- package/dist/core/installer.d.ts +10 -0
- package/dist/core/installer.js +185 -0
- package/dist/core/paths.d.ts +0 -34
- package/dist/core/paths.js +0 -35
- package/dist/core/settings-injector.d.ts +13 -0
- package/dist/core/settings-injector.js +167 -0
- package/dist/core/settings-lock.d.ts +35 -2
- package/dist/core/settings-lock.js +65 -7
- package/dist/core/spawn.js +100 -39
- package/dist/core/state-gc.d.ts +49 -0
- package/dist/core/state-gc.js +163 -0
- package/dist/core/stats-cli.d.ts +15 -0
- package/dist/core/stats-cli.js +143 -0
- package/dist/core/uninstall.d.ts +1 -0
- package/dist/core/uninstall.js +36 -5
- package/dist/core/v1-bootstrap.js +11 -3
- package/dist/engine/classify-enforce-cli.d.ts +8 -0
- package/dist/engine/classify-enforce-cli.js +61 -0
- package/dist/engine/compound-cli.d.ts +27 -2
- package/dist/engine/compound-cli.js +69 -16
- package/dist/engine/compound-export.d.ts +15 -0
- package/dist/engine/compound-export.js +32 -5
- package/dist/engine/compound-loop.js +3 -2
- package/dist/engine/enforce-classifier.d.ts +31 -0
- package/dist/engine/enforce-classifier.js +123 -0
- package/dist/engine/learn-cli.js +52 -0
- 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/match-eval-log.js +45 -0
- package/dist/engine/rule-toggle-cli.d.ts +13 -0
- package/dist/engine/rule-toggle-cli.js +76 -0
- package/dist/engine/solution-format.d.ts +0 -2
- package/dist/engine/solution-format.js +0 -4
- package/dist/engine/solution-matcher.d.ts +8 -0
- package/dist/engine/solution-matcher.js +7 -4
- package/dist/engine/solution-outcomes.d.ts +4 -0
- package/dist/engine/solution-outcomes.js +174 -97
- package/dist/engine/solution-writer.d.ts +8 -5
- package/dist/engine/solution-writer.js +43 -19
- package/dist/fgx.js +9 -2
- package/dist/forge/cli.js +7 -7
- package/dist/forge/evidence-processor.js +10 -2
- package/dist/hooks/context-guard.js +86 -1
- package/dist/hooks/hook-config.d.ts +9 -1
- package/dist/hooks/hook-config.js +25 -3
- package/dist/hooks/internal/run-lifecycle-check.d.ts +2 -0
- package/dist/hooks/internal/run-lifecycle-check.js +32 -0
- package/dist/hooks/notepad-injector.js +6 -3
- package/dist/hooks/permission-handler.d.ts +10 -2
- package/dist/hooks/permission-handler.js +31 -12
- package/dist/hooks/post-tool-use.js +62 -0
- package/dist/hooks/pre-tool-use.js +67 -5
- package/dist/hooks/secret-filter.d.ts +10 -0
- package/dist/hooks/secret-filter.js +26 -0
- package/dist/hooks/session-recovery.js +15 -7
- package/dist/hooks/shared/atomic-write.d.ts +8 -1
- package/dist/hooks/shared/atomic-write.js +17 -3
- package/dist/hooks/shared/hook-response.d.ts +11 -2
- package/dist/hooks/shared/hook-response.js +20 -7
- package/dist/hooks/shared/hook-timing.js +10 -1
- 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/solution-injector.d.ts +21 -0
- package/dist/hooks/solution-injector.js +60 -1
- package/dist/hooks/stop-guard.d.ts +84 -0
- package/dist/hooks/stop-guard.js +482 -0
- package/dist/mcp/solution-reader.d.ts +2 -0
- package/dist/mcp/solution-reader.js +28 -1
- package/dist/mcp/tools.js +24 -4
- package/dist/preset/preset-manager.js +12 -2
- package/dist/store/evidence-store.d.ts +15 -0
- package/dist/store/evidence-store.js +55 -6
- package/dist/store/profile-store.d.ts +9 -0
- package/dist/store/profile-store.js +25 -4
- 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 +133 -13
- 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 +10 -2
- package/plugin.json +7 -2
- package/scripts/postinstall.js +52 -5
|
@@ -2,6 +2,8 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { OUTCOMES_DIR, STATE_DIR } from '../core/paths.js';
|
|
4
4
|
import { sanitizeId } from '../hooks/shared/sanitize-id.js';
|
|
5
|
+
import { withFileLockSync, FileLockError } from '../hooks/shared/file-lock.js';
|
|
6
|
+
import { atomicWriteJSON } from '../hooks/shared/atomic-write.js';
|
|
5
7
|
import { createLogger } from '../core/logger.js';
|
|
6
8
|
const log = createLogger('solution-outcomes');
|
|
7
9
|
function pendingPath(sessionId) {
|
|
@@ -24,12 +26,41 @@ function readPending(sessionId) {
|
|
|
24
26
|
function writePending(sessionId, state) {
|
|
25
27
|
const p = pendingPath(sessionId);
|
|
26
28
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
27
|
-
|
|
29
|
+
atomicWriteJSON(p, state, { mode: 0o600, dirMode: 0o700 });
|
|
28
30
|
}
|
|
29
31
|
function appendOutcome(event) {
|
|
30
32
|
fs.mkdirSync(OUTCOMES_DIR, { recursive: true });
|
|
31
33
|
fs.appendFileSync(outcomesPath(event.session_id), JSON.stringify(event) + '\n');
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Run a read-modify-write pending-state mutation under a file lock
|
|
37
|
+
* (2026-04-21 audit fix #9).
|
|
38
|
+
*
|
|
39
|
+
* Prior code (`readPending` → in-memory mutate → `writePending`) was not
|
|
40
|
+
* serialised, so inject + correction + error hooks racing on the same
|
|
41
|
+
* session could lose or duplicate outcome events. The entire critical
|
|
42
|
+
* section now sits inside `withFileLockSync`; the lock file lives next
|
|
43
|
+
* to the pending file itself. A lock-acquire failure falls through
|
|
44
|
+
* fail-open (outcome tracking must never block the user).
|
|
45
|
+
*/
|
|
46
|
+
function mutatePending(sessionId, fn) {
|
|
47
|
+
const p = pendingPath(sessionId);
|
|
48
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
49
|
+
try {
|
|
50
|
+
let result = null;
|
|
51
|
+
withFileLockSync(p, () => {
|
|
52
|
+
result = fn();
|
|
53
|
+
});
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (e instanceof FileLockError) {
|
|
58
|
+
log.debug(`pending lock 실패 — skip (fail-open): ${e.message}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
33
64
|
/**
|
|
34
65
|
* Record that solutions were injected. Called from solution-injector right
|
|
35
66
|
* after `approveWithContext` is emitted. Fails silently — outcome tracking
|
|
@@ -39,12 +70,14 @@ export function appendPending(sessionId, injections) {
|
|
|
39
70
|
if (!sessionId || injections.length === 0)
|
|
40
71
|
return;
|
|
41
72
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
mutatePending(sessionId, () => {
|
|
74
|
+
const state = readPending(sessionId);
|
|
75
|
+
const ts = Date.now();
|
|
76
|
+
for (const inj of injections) {
|
|
77
|
+
state.pending.push({ ...inj, ts });
|
|
78
|
+
}
|
|
79
|
+
writePending(sessionId, state);
|
|
80
|
+
});
|
|
48
81
|
}
|
|
49
82
|
catch (e) {
|
|
50
83
|
log.debug(`appendPending failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -62,29 +95,37 @@ export function flushAccept(sessionId, excludeSolutions = new Set()) {
|
|
|
62
95
|
if (!sessionId)
|
|
63
96
|
return 0;
|
|
64
97
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
continue
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
const result = mutatePending(sessionId, () => {
|
|
99
|
+
const state = readPending(sessionId);
|
|
100
|
+
if (state.pending.length === 0)
|
|
101
|
+
return 0;
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const kept = [];
|
|
104
|
+
let flushed = 0;
|
|
105
|
+
for (const p of state.pending) {
|
|
106
|
+
// P1-L1 fix (2026-04-20): 이전에는 excluded pending을 `continue`로 건너뛰면서
|
|
107
|
+
// `kept`에도 push 안 하고 appendOutcome도 안 해서 증거 없이 사라졌다.
|
|
108
|
+
// 이미 correct/error로 attribute된 항목은 보존 (나중 prompt에서 재처리 방지).
|
|
109
|
+
if (excludeSolutions.has(p.solution)) {
|
|
110
|
+
kept.push(p);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
appendOutcome({
|
|
114
|
+
ts: now,
|
|
115
|
+
session_id: sessionId,
|
|
116
|
+
solution: p.solution,
|
|
117
|
+
match_score: p.match_score,
|
|
118
|
+
injected_chars: p.injected_chars,
|
|
119
|
+
outcome: 'accept',
|
|
120
|
+
outcome_lag_ms: now - p.ts,
|
|
121
|
+
attribution: 'default',
|
|
122
|
+
});
|
|
123
|
+
flushed++;
|
|
124
|
+
}
|
|
125
|
+
writePending(sessionId, { pending: kept, last_prompt_ts: now });
|
|
126
|
+
return flushed;
|
|
127
|
+
});
|
|
128
|
+
return result ?? 0;
|
|
88
129
|
}
|
|
89
130
|
catch (e) {
|
|
90
131
|
log.debug(`flushAccept failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -105,38 +146,64 @@ export function attributeCorrection(sessionId) {
|
|
|
105
146
|
if (!sessionId)
|
|
106
147
|
return [];
|
|
107
148
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
149
|
+
const result = mutatePending(sessionId, () => {
|
|
150
|
+
const state = readPending(sessionId);
|
|
151
|
+
if (state.pending.length === 0)
|
|
152
|
+
return [];
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const attributed = [];
|
|
155
|
+
for (const p of state.pending) {
|
|
156
|
+
appendOutcome({
|
|
157
|
+
ts: now,
|
|
158
|
+
session_id: sessionId,
|
|
159
|
+
solution: p.solution,
|
|
160
|
+
match_score: p.match_score,
|
|
161
|
+
injected_chars: p.injected_chars,
|
|
162
|
+
outcome: 'correct',
|
|
163
|
+
outcome_lag_ms: now - p.ts,
|
|
164
|
+
attribution: 'explicit',
|
|
165
|
+
});
|
|
166
|
+
attributed.push(p.solution);
|
|
167
|
+
}
|
|
168
|
+
writePending(sessionId, { pending: [], last_prompt_ts: state.last_prompt_ts });
|
|
169
|
+
return attributed;
|
|
170
|
+
});
|
|
171
|
+
return result ?? [];
|
|
128
172
|
}
|
|
129
173
|
catch (e) {
|
|
130
174
|
log.debug(`attributeCorrection failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
131
175
|
return [];
|
|
132
176
|
}
|
|
133
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Attribution gates for error events (2026-04-21 fix):
|
|
180
|
+
* - MIN_ERROR_MATCH_SCORE: solutions injected on thin relevance (< 0.3)
|
|
181
|
+
* are unlikely to have caused unrelated tool failures. Attributing
|
|
182
|
+
* error to them distorts fitness and poisons the Phase 4 evolver.
|
|
183
|
+
* - MAX_ATTRIBUTION_LAG_MS: errors more than 5 minutes after injection
|
|
184
|
+
* are too temporally distant — the user has likely moved on to work
|
|
185
|
+
* the solution has no bearing on.
|
|
186
|
+
* - MAX_ATTRIBUTED_PER_ERROR: cap at the top 3 most-relevant pending
|
|
187
|
+
* solutions so a broad inject batch doesn't scatter false blame.
|
|
188
|
+
*
|
|
189
|
+
* Data audit (2026-04-21, ~/.forgen/state/outcomes/ 260 sessions):
|
|
190
|
+
* Top-3 "error" solutions = 80% of all error events, injected at score
|
|
191
|
+
* 0.15/0.21 nearly every session. Without these gates, fitness
|
|
192
|
+
* classification drags good-but-low-score solutions into underperform.
|
|
193
|
+
*/
|
|
194
|
+
const MIN_ERROR_MATCH_SCORE = 0.3;
|
|
195
|
+
const MAX_ATTRIBUTION_LAG_MS = 5 * 60 * 1000;
|
|
196
|
+
const MAX_ATTRIBUTED_PER_ERROR = 3;
|
|
134
197
|
/**
|
|
135
198
|
* Attribute a tool error to pending solutions in this session. Called from
|
|
136
199
|
* post-tool-failure hook. Unlike corrections, errors do not clear pending
|
|
137
200
|
* — an error is a weaker signal and the next user prompt can still produce
|
|
138
201
|
* a correct/accept decision.
|
|
139
202
|
*
|
|
203
|
+
* Only the top-K most-relevant, recent, above-threshold pending solutions
|
|
204
|
+
* are attributed (see gates above). Below-threshold or stale pending
|
|
205
|
+
* entries are left untouched — they will resolve via accept/unknown later.
|
|
206
|
+
*
|
|
140
207
|
* To avoid flooding the log with duplicate errors for the same pending
|
|
141
208
|
* batch, we cap at one `error` event per (session, solution) pair per
|
|
142
209
|
* pending-cycle by tracking a `error_flagged` set in the pending state.
|
|
@@ -145,33 +212,40 @@ export function attributeError(sessionId) {
|
|
|
145
212
|
if (!sessionId)
|
|
146
213
|
return [];
|
|
147
214
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
215
|
+
const result = mutatePending(sessionId, () => {
|
|
216
|
+
const state = readPending(sessionId);
|
|
217
|
+
if (state.pending.length === 0)
|
|
218
|
+
return [];
|
|
219
|
+
const flaggedKey = `__error_flagged`;
|
|
220
|
+
const existing = state[flaggedKey];
|
|
221
|
+
const flagged = new Set(Array.isArray(existing) ? existing : []);
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const eligible = state.pending
|
|
224
|
+
.filter((p) => !flagged.has(p.solution))
|
|
225
|
+
.filter((p) => p.match_score >= MIN_ERROR_MATCH_SCORE)
|
|
226
|
+
.filter((p) => now - p.ts <= MAX_ATTRIBUTION_LAG_MS)
|
|
227
|
+
.sort((a, b) => b.match_score - a.match_score)
|
|
228
|
+
.slice(0, MAX_ATTRIBUTED_PER_ERROR);
|
|
229
|
+
const flaggedThisCall = [];
|
|
230
|
+
for (const p of eligible) {
|
|
231
|
+
appendOutcome({
|
|
232
|
+
ts: now,
|
|
233
|
+
session_id: sessionId,
|
|
234
|
+
solution: p.solution,
|
|
235
|
+
match_score: p.match_score,
|
|
236
|
+
injected_chars: p.injected_chars,
|
|
237
|
+
outcome: 'error',
|
|
238
|
+
outcome_lag_ms: now - p.ts,
|
|
239
|
+
attribution: 'window',
|
|
240
|
+
});
|
|
241
|
+
flagged.add(p.solution);
|
|
242
|
+
flaggedThisCall.push(p.solution);
|
|
243
|
+
}
|
|
244
|
+
state[flaggedKey] = Array.from(flagged);
|
|
245
|
+
writePending(sessionId, state);
|
|
246
|
+
return flaggedThisCall;
|
|
247
|
+
});
|
|
248
|
+
return result ?? [];
|
|
175
249
|
}
|
|
176
250
|
catch (e) {
|
|
177
251
|
log.debug(`attributeError failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -187,26 +261,29 @@ export function finalizeSession(sessionId) {
|
|
|
187
261
|
if (!sessionId)
|
|
188
262
|
return 0;
|
|
189
263
|
try {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
fs.
|
|
209
|
-
|
|
264
|
+
const result = mutatePending(sessionId, () => {
|
|
265
|
+
const state = readPending(sessionId);
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
let finalized = 0;
|
|
268
|
+
for (const p of state.pending) {
|
|
269
|
+
appendOutcome({
|
|
270
|
+
ts: now,
|
|
271
|
+
session_id: sessionId,
|
|
272
|
+
solution: p.solution,
|
|
273
|
+
match_score: p.match_score,
|
|
274
|
+
injected_chars: p.injected_chars,
|
|
275
|
+
outcome: 'unknown',
|
|
276
|
+
outcome_lag_ms: now - p.ts,
|
|
277
|
+
attribution: 'session_end',
|
|
278
|
+
});
|
|
279
|
+
finalized++;
|
|
280
|
+
}
|
|
281
|
+
const pendingFile = pendingPath(sessionId);
|
|
282
|
+
if (fs.existsSync(pendingFile))
|
|
283
|
+
fs.unlinkSync(pendingFile);
|
|
284
|
+
return finalized;
|
|
285
|
+
});
|
|
286
|
+
return result ?? 0;
|
|
210
287
|
}
|
|
211
288
|
catch (e) {
|
|
212
289
|
log.debug(`finalizeSession failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -71,11 +71,14 @@ export declare function mutateSolutionByName(name: string, mutator: SolutionMuta
|
|
|
71
71
|
}): boolean;
|
|
72
72
|
/**
|
|
73
73
|
* Evidence 카운터 단일 증가 helper.
|
|
74
|
-
* mutateSolutionByName + 카운터 증가 패턴을 한 줄로.
|
|
75
74
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
75
|
+
* Invariant: status/confidence 같은 lifecycle 필드는 건드리지 않는다.
|
|
76
|
+
* 모든 status 전이는 compound-lifecycle.ts::runLifecycleCheck(자동, reflected
|
|
77
|
+
* /sessions/reExtracted + age-gate 기반)과 verifySolution(수동 명령)이라는
|
|
78
|
+
* 단일 경로로만 일어난다. dual-path 금지.
|
|
79
|
+
*
|
|
80
|
+
* 과거에는 inject≥5 자동 verified 승급이 이 함수 안에 있었는데, 그것은 outcome
|
|
81
|
+
* 증거 없이도 candidate를 승급시켜 self-rewarding 편향을 만들었다. 2026-04-20
|
|
82
|
+
* 제거 (feedback_core_loop_invariant 참고).
|
|
80
83
|
*/
|
|
81
84
|
export declare function incrementEvidence(solutionName: string, field: 'reflected' | 'negative' | 'injected' | 'sessions' | 'reExtracted'): boolean;
|
|
@@ -31,6 +31,7 @@ import { atomicWriteText } from '../hooks/shared/atomic-write.js';
|
|
|
31
31
|
import { parseFrontmatterOnly, parseSolutionV3, serializeSolutionV3, } from './solution-format.js';
|
|
32
32
|
import { ME_SOLUTIONS, ME_RULES } from '../core/paths.js';
|
|
33
33
|
import { createLogger } from '../core/logger.js';
|
|
34
|
+
import { getOrBuildIndex } from './solution-index.js';
|
|
34
35
|
const log = createLogger('solution-writer');
|
|
35
36
|
/**
|
|
36
37
|
* 단일 .md 파일에 lock 보호된 read-modify-write 수행.
|
|
@@ -96,7 +97,40 @@ export function mutateSolutionFile(filePath, mutator) {
|
|
|
96
97
|
* symlink는 보안상 무시 (lstatSync 가드).
|
|
97
98
|
*/
|
|
98
99
|
export function mutateSolutionByName(name, mutator, options) {
|
|
99
|
-
const
|
|
100
|
+
const extraDirs = options?.extraDirs ?? [];
|
|
101
|
+
const dirs = [ME_SOLUTIONS, ME_RULES, ...extraDirs];
|
|
102
|
+
// P0-3 fast path (2026-04-20): solution-index의 캐시에서 name→filePath를
|
|
103
|
+
// O(1) 조회. 과거에는 매 호출마다 dir readdir + N번 readFileSync + YAML parse
|
|
104
|
+
// (N=500일 때 hook당 최대 1500회 I/O)로 5초 timeout을 위협했다.
|
|
105
|
+
// 인덱스 miss/stale 시 아래 기존 O(N) 스캔 경로로 fallback한다.
|
|
106
|
+
try {
|
|
107
|
+
const indexDirs = [
|
|
108
|
+
{ dir: ME_SOLUTIONS, scope: 'me' },
|
|
109
|
+
{ dir: ME_RULES, scope: 'me' },
|
|
110
|
+
...extraDirs.map((d) => ({ dir: d, scope: 'project' })),
|
|
111
|
+
];
|
|
112
|
+
const idx = getOrBuildIndex(indexDirs);
|
|
113
|
+
const entry = idx.entries.find(e => e.name === name);
|
|
114
|
+
if (entry?.filePath && fs.existsSync(entry.filePath)) {
|
|
115
|
+
let isSymlink = true;
|
|
116
|
+
try {
|
|
117
|
+
isSymlink = fs.lstatSync(entry.filePath).isSymbolicLink();
|
|
118
|
+
}
|
|
119
|
+
catch { /* fall through to full scan */ }
|
|
120
|
+
if (!isSymlink) {
|
|
121
|
+
const result = mutateSolutionFile(entry.filePath, sol => {
|
|
122
|
+
if (sol.frontmatter.name !== name)
|
|
123
|
+
return false;
|
|
124
|
+
return mutator(sol);
|
|
125
|
+
});
|
|
126
|
+
if (result)
|
|
127
|
+
return true;
|
|
128
|
+
// 인덱스가 stale(이름이 바뀌었거나 파일이 재생성됨)이면 fallback 경로로.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { /* 인덱스 빌드 실패 — fallback */ }
|
|
133
|
+
// Fallback: 전체 디렉터리 스캔 (인덱스 miss / stale 시)
|
|
100
134
|
for (const dir of dirs) {
|
|
101
135
|
if (!fs.existsSync(dir))
|
|
102
136
|
continue;
|
|
@@ -142,22 +176,17 @@ export function mutateSolutionByName(name, mutator, options) {
|
|
|
142
176
|
}
|
|
143
177
|
return false;
|
|
144
178
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Phase 4 candidate promotion threshold: a `status: candidate` solution
|
|
147
|
-
* automatically graduates to `status: verified` once its injected count
|
|
148
|
-
* crosses this cutoff. At that point the cold-start exploration bonus
|
|
149
|
-
* (solution-matcher.ts) disappears naturally, since the bonus keys off
|
|
150
|
-
* `candidate` status.
|
|
151
|
-
*/
|
|
152
|
-
const CANDIDATE_PROMOTION_INJECTIONS = 5;
|
|
153
179
|
/**
|
|
154
180
|
* Evidence 카운터 단일 증가 helper.
|
|
155
|
-
* mutateSolutionByName + 카운터 증가 패턴을 한 줄로.
|
|
156
181
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
182
|
+
* Invariant: status/confidence 같은 lifecycle 필드는 건드리지 않는다.
|
|
183
|
+
* 모든 status 전이는 compound-lifecycle.ts::runLifecycleCheck(자동, reflected
|
|
184
|
+
* /sessions/reExtracted + age-gate 기반)과 verifySolution(수동 명령)이라는
|
|
185
|
+
* 단일 경로로만 일어난다. dual-path 금지.
|
|
186
|
+
*
|
|
187
|
+
* 과거에는 inject≥5 자동 verified 승급이 이 함수 안에 있었는데, 그것은 outcome
|
|
188
|
+
* 증거 없이도 candidate를 승급시켜 self-rewarding 편향을 만들었다. 2026-04-20
|
|
189
|
+
* 제거 (feedback_core_loop_invariant 참고).
|
|
161
190
|
*/
|
|
162
191
|
export function incrementEvidence(solutionName, field) {
|
|
163
192
|
return mutateSolutionByName(solutionName, sol => {
|
|
@@ -165,11 +194,6 @@ export function incrementEvidence(solutionName, field) {
|
|
|
165
194
|
if (!(field in ev))
|
|
166
195
|
return false;
|
|
167
196
|
ev[field] = (ev[field] ?? 0) + 1;
|
|
168
|
-
if (field === 'injected' &&
|
|
169
|
-
sol.frontmatter.status === 'candidate' &&
|
|
170
|
-
ev.injected >= CANDIDATE_PROMOTION_INJECTIONS) {
|
|
171
|
-
sol.frontmatter.status = 'verified';
|
|
172
|
-
}
|
|
173
197
|
return true;
|
|
174
198
|
});
|
|
175
199
|
}
|
package/dist/fgx.js
CHANGED
|
@@ -15,10 +15,17 @@ if (!launchArgs.includes('--dangerously-skip-permissions')) {
|
|
|
15
15
|
launchArgs.unshift('--dangerously-skip-permissions');
|
|
16
16
|
}
|
|
17
17
|
async function main() {
|
|
18
|
-
// Security warning — fgx bypasses all Claude Code permission checks
|
|
18
|
+
// Security warning — fgx bypasses all Claude Code permission checks.
|
|
19
|
+
//
|
|
20
|
+
// Audit fix #3 (2026-04-21): The warning banner is shown regardless of
|
|
21
|
+
// the user's profile trust policy, which means "가드레일 우선" users who
|
|
22
|
+
// alias `fgx` unknowingly run with zero guardrails. Users who rely on
|
|
23
|
+
// the profile trust policy should NOT use `fgx`. Surface the mismatch
|
|
24
|
+
// loudly (harness.ts also prints the Trust 상승 warning downstream).
|
|
19
25
|
console.warn('\n ⚠ fgx: ALL permission checks are disabled (--dangerously-skip-permissions)');
|
|
20
26
|
console.warn(' ⚠ Claude Code will execute tools without asking for confirmation.');
|
|
21
|
-
console.warn(' ⚠ Use only in trusted environments
|
|
27
|
+
console.warn(' ⚠ Use only in trusted environments. If your profile trust policy is');
|
|
28
|
+
console.warn(' ⚠ "가드레일 우선" or "승인 완화", consider `forgen` (no flag) instead.\n');
|
|
22
29
|
// fgx는 서브커맨드 없이 바로 Claude Code 실행 전용
|
|
23
30
|
const firstRun = isFirstRun();
|
|
24
31
|
if (firstRun) {
|
package/dist/forge/cli.js
CHANGED
|
@@ -58,7 +58,7 @@ async function handleReset(level) {
|
|
|
58
58
|
}
|
|
59
59
|
// 동적 import로 store 모듈 로드
|
|
60
60
|
const fs = await import('node:fs');
|
|
61
|
-
const {
|
|
61
|
+
const { FORGE_PROFILE, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS } = await import('../core/paths.js');
|
|
62
62
|
const deleteDirs = (dirs) => {
|
|
63
63
|
for (const dir of dirs) {
|
|
64
64
|
try {
|
|
@@ -75,18 +75,18 @@ async function handleReset(level) {
|
|
|
75
75
|
catch { /* ignore */ }
|
|
76
76
|
};
|
|
77
77
|
if (level === 'soft') {
|
|
78
|
-
deleteFile(
|
|
79
|
-
deleteDirs([
|
|
78
|
+
deleteFile(FORGE_PROFILE);
|
|
79
|
+
deleteDirs([ME_RULES, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR]);
|
|
80
80
|
console.log('\n Soft reset 완료. Profile + Rule + Recommendation + Session 초기화.');
|
|
81
81
|
}
|
|
82
82
|
else if (level === 'learning') {
|
|
83
|
-
deleteFile(
|
|
84
|
-
deleteDirs([
|
|
83
|
+
deleteFile(FORGE_PROFILE);
|
|
84
|
+
deleteDirs([ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, V1_RAW_LOGS_DIR]);
|
|
85
85
|
console.log('\n Learning reset 완료. 개인 학습 전체 초기화.');
|
|
86
86
|
}
|
|
87
87
|
else if (level === 'full') {
|
|
88
|
-
deleteFile(
|
|
89
|
-
deleteDirs([
|
|
88
|
+
deleteFile(FORGE_PROFILE);
|
|
89
|
+
deleteDirs([ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS]);
|
|
90
90
|
console.log('\n Full reset 완료. Compound 포함 전체 초기화.');
|
|
91
91
|
}
|
|
92
92
|
// Reset 후 자동 온보딩 (interactive 환경에서만)
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Migration Plan §5.4: 이 모듈의 "해석" 계층은 AI(Claude 세션)가 채운다.
|
|
6
6
|
* 여기서는 구조화된 입출력 인터페이스와 알고리즘 적용 함수만 정의.
|
|
7
7
|
*/
|
|
8
|
-
import { createEvidence,
|
|
8
|
+
import { createEvidence, appendEvidence } from '../store/evidence-store.js';
|
|
9
9
|
import { createRule, saveRule } from '../store/rule-store.js';
|
|
10
|
+
import { classify, applyProposal } from '../engine/enforce-classifier.js';
|
|
10
11
|
// ── Correction → Evidence + Temporary Rule ──
|
|
11
12
|
/**
|
|
12
13
|
* 사용자 교정을 Evidence로 기록하고, 필요 시 temporary rule 생성.
|
|
@@ -29,7 +30,7 @@ export function processCorrection(req) {
|
|
|
29
30
|
direction: req.kind === 'avoid-this' ? 'opposite' : 'same',
|
|
30
31
|
},
|
|
31
32
|
});
|
|
32
|
-
|
|
33
|
+
appendEvidence(evidence); // T1 lifecycle trigger fires here for explicit_correction
|
|
33
34
|
// fix-now, avoid-this → temporary session rule
|
|
34
35
|
let temporaryRule = null;
|
|
35
36
|
if (req.kind === 'fix-now' || req.kind === 'avoid-this') {
|
|
@@ -45,6 +46,13 @@ export function processCorrection(req) {
|
|
|
45
46
|
evidence_refs: [evidence.evidence_id],
|
|
46
47
|
render_key: `${req.axis_hint ?? 'workflow'}.${req.target.toLowerCase().replace(/\s+/g, '-').slice(0, 30)}`,
|
|
47
48
|
});
|
|
49
|
+
// ADR-001 auto-classify on creation — 교정 즉시 enforce_via 가 붙어야 다음 턴부터 Mech-A/B 발화.
|
|
50
|
+
// 기존 `forgen classify-enforce --apply` 수동 경로를 유지하되, 신규 rule 은 창조 시점에 자동 populate.
|
|
51
|
+
try {
|
|
52
|
+
const proposal = classify(temporaryRule);
|
|
53
|
+
temporaryRule = applyProposal(temporaryRule, proposal);
|
|
54
|
+
}
|
|
55
|
+
catch { /* fail-open: classify 실패는 rule 저장 자체를 막지 않음 */ }
|
|
48
56
|
saveRule(temporaryRule);
|
|
49
57
|
}
|
|
50
58
|
return {
|