@wooojin/forgen 0.4.6 → 0.4.8
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 +78 -0
- package/dist/checks/self-score-deflation.js +6 -4
- package/dist/cli.js +6 -3
- package/dist/core/auto-compound-runner.js +6 -2
- package/dist/core/dashboard.js +2 -2
- package/dist/core/doctor.d.ts +10 -0
- package/dist/core/doctor.js +49 -8
- package/dist/core/harness.js +8 -2
- package/dist/core/inspect-cli.js +4 -4
- package/dist/core/migrate-evidence-host.js +1 -1
- package/dist/core/notify.js +7 -0
- package/dist/core/paths.d.ts +16 -2
- package/dist/core/paths.js +16 -2
- package/dist/core/session-store.d.ts +12 -1
- package/dist/core/session-store.js +73 -1
- package/dist/core/spawn.js +13 -7
- package/dist/core/v1-bootstrap.d.ts +7 -0
- package/dist/core/v1-bootstrap.js +28 -6
- package/dist/engine/compound-extractor.js +1 -1
- package/dist/engine/learn-cli.js +2 -2
- package/dist/engine/lifecycle/bypass-detector.js +3 -2
- package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
- package/dist/engine/lifecycle/signals.js +2 -2
- package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
- package/dist/engine/solution-candidate.js +1 -1
- package/dist/engine/solution-outcomes.js +1 -1
- package/dist/engine/solution-quarantine.js +1 -1
- package/dist/engine/solution-weakness.js +8 -2
- package/dist/fgx.js +6 -5
- package/dist/forge/cli.js +1 -1
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/shared/hook-response.js +1 -1
- package/dist/hooks/shared/hook-timing.js +3 -3
- package/dist/hooks/solution-injector.js +1 -1
- package/dist/hooks/stop-guard.js +3 -3
- package/dist/host/host-runtime.d.ts +6 -0
- package/dist/host/host-runtime.js +2 -0
- package/dist/host/install-codex.js +1 -1
- package/dist/host/install-orchestrator.js +2 -1
- package/dist/mcp/tools.js +1 -1
- package/dist/preset/facet-catalog.js +2 -2
- package/dist/renderer/rule-renderer.js +7 -7
- package/dist/store/compound-usage-store.js +1 -1
- package/dist/store/implicit-feedback-store.js +2 -2
- package/dist/store/profile-store.d.ts +11 -0
- package/dist/store/profile-store.js +23 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import * as crypto from 'node:crypto';
|
|
18
|
-
import { FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, STATE_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS } from './paths.js';
|
|
18
|
+
import { FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, STATE_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS, SESSIONS_DIR } from './paths.js';
|
|
19
19
|
import { checkLegacyProfile, runLegacyCutover } from './legacy-detector.js';
|
|
20
20
|
import { detectRuntimeCapability } from './runtime-detector.js';
|
|
21
|
-
import { loadProfile, profileExists } from '../store/profile-store.js';
|
|
21
|
+
import { backupCorruptProfile, loadProfile, profileExists } from '../store/profile-store.js';
|
|
22
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';
|
|
@@ -27,7 +27,14 @@ import { loadEvidenceBySession } from '../store/evidence-store.js';
|
|
|
27
27
|
import { computeSessionSignals, detectMismatch } from '../forge/mismatch-detector.js';
|
|
28
28
|
import { createRecommendation, saveRecommendation } from '../store/recommendation-store.js';
|
|
29
29
|
// ── Directory Initialization ──
|
|
30
|
-
|
|
30
|
+
// v0.4.8 (A3): SESSIONS_DIR (~/.forgen/sessions/) 도 v1 bootstrap 보장 대상.
|
|
31
|
+
// 이전엔 V1_DIRS 에 누락되어 있어, prepareHarness step 11 (startSessionLog)
|
|
32
|
+
// 에 도달 못 하는 코드 경로 (예: forgen install 직접 실행) 에선 디렉토리가
|
|
33
|
+
// 끝까지 생성되지 않았음. legacy session log 와 v1 effective state 는
|
|
34
|
+
// 서로 다른 저장소 책임이라 두 dir 모두 명시 보장.
|
|
35
|
+
// - SESSIONS_DIR = ~/.forgen/sessions/ ← legacy session log (transcript-like)
|
|
36
|
+
// - V1_SESSIONS_DIR = ~/.forgen/state/sessions/ ← v1 effective state per session
|
|
37
|
+
const V1_DIRS = [FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, STATE_DIR, V1_SESSIONS_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS, SESSIONS_DIR];
|
|
31
38
|
export function ensureV1Directories() {
|
|
32
39
|
for (const dir of V1_DIRS) {
|
|
33
40
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -48,6 +55,7 @@ export function bootstrapV1Session() {
|
|
|
48
55
|
return {
|
|
49
56
|
needsOnboarding: true,
|
|
50
57
|
legacyBackupPath,
|
|
58
|
+
corruptProfileBackupPath: null,
|
|
51
59
|
session: null,
|
|
52
60
|
renderedRules: null,
|
|
53
61
|
profile: null,
|
|
@@ -56,7 +64,20 @@ export function bootstrapV1Session() {
|
|
|
56
64
|
}
|
|
57
65
|
const profile = loadProfile();
|
|
58
66
|
if (!profile) {
|
|
59
|
-
|
|
67
|
+
// v0.4.8 — corrupt/invalid profile auto-repair.
|
|
68
|
+
// profileExists()=true && loadProfile()=null 은 parse 실패 또는 v1
|
|
69
|
+
// shape 위반. 그대로 두면 다음 실행에서도 같은 분기로 빠지므로
|
|
70
|
+
// backup 후 새 onboarding 흐름을 강제.
|
|
71
|
+
const corruptProfileBackupPath = backupCorruptProfile();
|
|
72
|
+
return {
|
|
73
|
+
needsOnboarding: true,
|
|
74
|
+
legacyBackupPath,
|
|
75
|
+
corruptProfileBackupPath,
|
|
76
|
+
session: null,
|
|
77
|
+
renderedRules: null,
|
|
78
|
+
profile: null,
|
|
79
|
+
mismatch: null,
|
|
80
|
+
};
|
|
60
81
|
}
|
|
61
82
|
// 3. Runtime capability 감지
|
|
62
83
|
const runtime = detectRuntimeCapability();
|
|
@@ -133,7 +154,7 @@ export function bootstrapV1Session() {
|
|
|
133
154
|
try {
|
|
134
155
|
// 세션 시작 로그
|
|
135
156
|
const rawLogPath = path.join(V1_RAW_LOGS_DIR, `${sessionId}.jsonl`);
|
|
136
|
-
fs.appendFileSync(rawLogPath, JSON.stringify({
|
|
157
|
+
fs.appendFileSync(rawLogPath, `${JSON.stringify({
|
|
137
158
|
event: 'session-started',
|
|
138
159
|
session_id: sessionId,
|
|
139
160
|
timestamp: new Date().toISOString(),
|
|
@@ -142,7 +163,7 @@ export function bootstrapV1Session() {
|
|
|
142
163
|
judgment_pack: profile.base_packs.judgment_pack,
|
|
143
164
|
communication_pack: profile.base_packs.communication_pack,
|
|
144
165
|
effective_trust: session.effective_trust_policy,
|
|
145
|
-
})
|
|
166
|
+
})}\n`);
|
|
146
167
|
// TTL sweep: 7일 이상 된 raw log 파일 삭제
|
|
147
168
|
const TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
148
169
|
const now = Date.now();
|
|
@@ -163,6 +184,7 @@ export function bootstrapV1Session() {
|
|
|
163
184
|
return {
|
|
164
185
|
needsOnboarding: false,
|
|
165
186
|
legacyBackupPath,
|
|
187
|
+
corruptProfileBackupPath: null,
|
|
166
188
|
session,
|
|
167
189
|
renderedRules,
|
|
168
190
|
profile,
|
|
@@ -557,7 +557,7 @@ function findCommonPrefix(strings) {
|
|
|
557
557
|
return prefix.replace(/-$/, '');
|
|
558
558
|
}
|
|
559
559
|
/** Save an extracted solution as experiment */
|
|
560
|
-
function saveExtractedSolution(sol,
|
|
560
|
+
function saveExtractedSolution(sol, _sessionId) {
|
|
561
561
|
const today = new Date().toISOString().split('T')[0];
|
|
562
562
|
const slugName = sol.name.toLowerCase()
|
|
563
563
|
.replace(/[^a-z0-9가-힣\s-]/g, '')
|
package/dist/engine/learn-cli.js
CHANGED
|
@@ -136,7 +136,7 @@ function runFitness(args) {
|
|
|
136
136
|
console.log(` ${'name'.padEnd(48)} ${'state'.padEnd(14)} ${'inj'.padStart(4)} ${'acc/cor/err'.padStart(11)} ${'fit'.padStart(6)}`);
|
|
137
137
|
console.log(` ${'-'.repeat(48)} ${'-'.repeat(14)} ${'-'.repeat(4)} ${'-'.repeat(11)} ${'-'.repeat(6)}`);
|
|
138
138
|
for (const r of records) {
|
|
139
|
-
const name = r.solution.length > 47 ? r.solution.slice(0, 45)
|
|
139
|
+
const name = r.solution.length > 47 ? `${r.solution.slice(0, 45)}..` : r.solution;
|
|
140
140
|
const acr = `${r.accepted}/${r.corrected}/${r.errored}`;
|
|
141
141
|
console.log(` ${name.padEnd(48)} ${r.state.padEnd(14)} ${String(r.injected).padStart(4)} ${acr.padStart(11)} ${r.fitness.toFixed(2).padStart(6)}`);
|
|
142
142
|
}
|
|
@@ -222,7 +222,7 @@ function runEvolvePromote(candidateNameOrList) {
|
|
|
222
222
|
}
|
|
223
223
|
const result = promoteCandidate(candidateNameOrList);
|
|
224
224
|
if (result.ok) {
|
|
225
|
-
console.log(`\n ✓ Promoted: ${path.basename(result.dest)}`);
|
|
225
|
+
console.log(`\n ✓ Promoted: ${result.dest ? path.basename(result.dest) : '(unknown)'}`);
|
|
226
226
|
console.log(` from: ${result.source}`);
|
|
227
227
|
console.log(` to: ${result.dest}`);
|
|
228
228
|
console.log(` Cold-start bonus active until 5 injections accumulate (auto-promotes to verified).\n`);
|
|
@@ -51,9 +51,10 @@ function extractParenthesizedExamples(p) {
|
|
|
51
51
|
const out = [];
|
|
52
52
|
// Match (...) groups; multiple groups in policy are uncommon but supported
|
|
53
53
|
const re = /\(([^)]+)\)/g;
|
|
54
|
-
let m;
|
|
55
|
-
while (
|
|
54
|
+
let m = re.exec(p);
|
|
55
|
+
while (m !== null) {
|
|
56
56
|
const inside = m[1];
|
|
57
|
+
m = re.exec(p);
|
|
57
58
|
// Skip if it looks like a path (contains "/" before any obvious separator commitment)
|
|
58
59
|
if (/[a-zA-Z]+\/[a-zA-Z]/.test(inside))
|
|
59
60
|
continue;
|
|
@@ -294,7 +294,7 @@ export function appendLifecycleEvents(events, now = Date.now()) {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
catch { /* missing → no rotate */ }
|
|
297
|
-
const body = events.map((e) => JSON.stringify(e)).join('\n')
|
|
297
|
+
const body = `${events.map((e) => JSON.stringify(e)).join('\n')}\n`;
|
|
298
298
|
fs.appendFileSync(logPath, body);
|
|
299
299
|
}
|
|
300
300
|
catch (e) {
|
|
@@ -62,7 +62,7 @@ export function recordViolation(entry) {
|
|
|
62
62
|
fs.mkdirSync(ENFORCEMENT_DIR, { recursive: true });
|
|
63
63
|
rotateIfBig(VIOLATIONS_PATH);
|
|
64
64
|
const full = { at: new Date().toISOString(), ...entry };
|
|
65
|
-
fs.appendFileSync(VIOLATIONS_PATH, JSON.stringify(full)
|
|
65
|
+
fs.appendFileSync(VIOLATIONS_PATH, `${JSON.stringify(full)}\n`);
|
|
66
66
|
}
|
|
67
67
|
catch (e) {
|
|
68
68
|
// best-effort, 실패 시 debug 로그 (silent swallow 방지)
|
|
@@ -76,7 +76,7 @@ export function recordBypass(entry) {
|
|
|
76
76
|
fs.mkdirSync(ENFORCEMENT_DIR, { recursive: true });
|
|
77
77
|
rotateIfBig(BYPASS_PATH);
|
|
78
78
|
const full = { at: new Date().toISOString(), ...entry };
|
|
79
|
-
fs.appendFileSync(BYPASS_PATH, JSON.stringify(full)
|
|
79
|
+
fs.appendFileSync(BYPASS_PATH, `${JSON.stringify(full)}\n`);
|
|
80
80
|
}
|
|
81
81
|
catch (e) {
|
|
82
82
|
if (process.env.FORGEN_DEBUG_SIGNALS === '1') {
|
|
@@ -39,7 +39,7 @@ function matchesRule(evidence, rule) {
|
|
|
39
39
|
.toLowerCase();
|
|
40
40
|
return keyTokens.some((t) => {
|
|
41
41
|
const tokLower = t.toLowerCase();
|
|
42
|
-
return summaryLower.includes(tokLower) || (targetToken
|
|
42
|
+
return summaryLower.includes(tokLower) || (targetToken?.includes(tokLower));
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
export function detect(input) {
|
|
@@ -100,7 +100,7 @@ export function rollbackSince(epochMs) {
|
|
|
100
100
|
continue;
|
|
101
101
|
try {
|
|
102
102
|
fs.mkdirSync(archiveDir, { recursive: true });
|
|
103
|
-
const destName = path.basename(dir)
|
|
103
|
+
const destName = `${path.basename(dir)}__${file}`;
|
|
104
104
|
fs.renameSync(filePath, path.join(archiveDir, destName));
|
|
105
105
|
archived.push(filePath);
|
|
106
106
|
}
|
|
@@ -30,7 +30,7 @@ function writePending(sessionId, state) {
|
|
|
30
30
|
}
|
|
31
31
|
function appendOutcome(event) {
|
|
32
32
|
fs.mkdirSync(OUTCOMES_DIR, { recursive: true });
|
|
33
|
-
fs.appendFileSync(outcomesPath(event.session_id), JSON.stringify(event)
|
|
33
|
+
fs.appendFileSync(outcomesPath(event.session_id), `${JSON.stringify(event)}\n`);
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Run a read-modify-write pending-state mutation under a file lock
|
|
@@ -49,7 +49,7 @@ export function recordQuarantine(filePath, errors) {
|
|
|
49
49
|
at: new Date().toISOString(),
|
|
50
50
|
errors,
|
|
51
51
|
};
|
|
52
|
-
fs.appendFileSync(SOLUTION_QUARANTINE_PATH, JSON.stringify(entry)
|
|
52
|
+
fs.appendFileSync(SOLUTION_QUARANTINE_PATH, `${JSON.stringify(entry)}\n`);
|
|
53
53
|
}
|
|
54
54
|
catch (e) {
|
|
55
55
|
log.debug(`quarantine write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -97,14 +97,20 @@ function findConflictClusters(rows, fitnessByName) {
|
|
|
97
97
|
const underperformers = rows.filter((r) => fitnessByName.get(r.name)?.state === 'underperform');
|
|
98
98
|
const clusters = [];
|
|
99
99
|
for (const ch of champions) {
|
|
100
|
+
const chFitness = fitnessByName.get(ch.name)?.fitness;
|
|
101
|
+
if (chFitness === undefined)
|
|
102
|
+
continue;
|
|
100
103
|
for (const up of underperformers) {
|
|
104
|
+
const upFitness = fitnessByName.get(up.name)?.fitness;
|
|
105
|
+
if (upFitness === undefined)
|
|
106
|
+
continue;
|
|
101
107
|
const shared = ch.tags.filter((t) => up.tags.includes(t));
|
|
102
108
|
if (shared.length < 2)
|
|
103
109
|
continue;
|
|
104
110
|
clusters.push({
|
|
105
111
|
shared_tags: shared,
|
|
106
|
-
champion: { name: ch.name, fitness:
|
|
107
|
-
underperform: { name: up.name, fitness:
|
|
112
|
+
champion: { name: ch.name, fitness: chFitness },
|
|
113
|
+
underperform: { name: up.name, fitness: upFitness },
|
|
108
114
|
});
|
|
109
115
|
}
|
|
110
116
|
}
|
package/dist/fgx.js
CHANGED
|
@@ -11,9 +11,10 @@ const args = process.argv.slice(2);
|
|
|
11
11
|
// 이미 포함되어 있으면 중복 추가하지 않음
|
|
12
12
|
const launchContext = resolveLaunchContext(args);
|
|
13
13
|
const runtime = launchContext.runtime;
|
|
14
|
+
const skipFlag = getHostRuntime(runtime).dangerousSkipFlag;
|
|
14
15
|
const launchArgs = [...launchContext.args];
|
|
15
|
-
if (!launchArgs.includes(
|
|
16
|
-
launchArgs.unshift(
|
|
16
|
+
if (!launchArgs.includes(skipFlag)) {
|
|
17
|
+
launchArgs.unshift(skipFlag);
|
|
17
18
|
}
|
|
18
19
|
async function main() {
|
|
19
20
|
// Security warning — fgx bypasses all Claude Code permission checks.
|
|
@@ -23,8 +24,8 @@ async function main() {
|
|
|
23
24
|
// alias `fgx` unknowingly run with zero guardrails. Users who rely on
|
|
24
25
|
// the profile trust policy should NOT use `fgx`. Surface the mismatch
|
|
25
26
|
// loudly (harness.ts also prints the Trust 상승 warning downstream).
|
|
26
|
-
console.warn(
|
|
27
|
-
console.warn(
|
|
27
|
+
console.warn(`\n ⚠ fgx: ALL permission checks are disabled (${skipFlag})`);
|
|
28
|
+
console.warn(` ⚠ ${getHostRuntime(runtime).displayName} will execute tools without asking for confirmation.`);
|
|
28
29
|
console.warn(' ⚠ Use only in trusted environments. If your profile trust policy is');
|
|
29
30
|
console.warn(' ⚠ "가드레일 우선" or "승인 완화", consider `forgen` (no flag) instead.\n');
|
|
30
31
|
// fgx는 서브커맨드 없이 바로 Claude Code 실행 전용
|
|
@@ -43,7 +44,7 @@ async function main() {
|
|
|
43
44
|
if (v1.session) {
|
|
44
45
|
console.log(`[forgen] Trust: ${v1.session.effective_trust_policy}`);
|
|
45
46
|
}
|
|
46
|
-
console.log(
|
|
47
|
+
console.log(`[forgen] Mode: ${skipFlag.replace(/^--/, '')}`);
|
|
47
48
|
const runtimeLabel = getHostRuntime(runtime).displayName;
|
|
48
49
|
console.log(`[forgen] Starting ${runtimeLabel}...\n`);
|
|
49
50
|
await spawnClaude(launchArgs, context, runtime);
|
package/dist/forge/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ function handleShowProfile() {
|
|
|
39
39
|
console.log('\n No v1 profile found. Run `forgen forge` or `forgen onboarding`.\n');
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
console.log(
|
|
42
|
+
console.log(`\n${renderProfile(profile)}\n`);
|
|
43
43
|
}
|
|
44
44
|
function handleExport() {
|
|
45
45
|
const profile = loadProfile();
|
|
@@ -192,7 +192,7 @@ async function main() {
|
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
const match = detectKeyword(input.prompt);
|
|
195
|
-
const
|
|
195
|
+
const _sessionId = input.session_id ?? 'unknown';
|
|
196
196
|
// v1: regex 기반 prompt 학습 제거. Evidence 기반으로 전환됨.
|
|
197
197
|
if (!match) {
|
|
198
198
|
console.log(approve());
|
|
@@ -34,10 +34,10 @@ export function redactSecrets(text) {
|
|
|
34
34
|
let out = text;
|
|
35
35
|
for (const sp of SECRET_PATTERNS) {
|
|
36
36
|
// regex 복제 (global flag 없이 repeated test 되는 경우 lastIndex 안전)
|
|
37
|
-
const re = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : sp.pattern.flags
|
|
37
|
+
const re = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : `${sp.pattern.flags}g`));
|
|
38
38
|
if (re.test(out)) {
|
|
39
39
|
hits.push(sp);
|
|
40
|
-
const re2 = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : sp.pattern.flags
|
|
40
|
+
const re2 = new RegExp(sp.pattern.source, (sp.pattern.flags.includes('g') ? sp.pattern.flags : `${sp.pattern.flags}g`));
|
|
41
41
|
out = out.replace(re2, `[REDACTED:${sp.name}]`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -146,7 +146,7 @@ export function failOpenWithTracking(hookName, err) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
const entry = JSON.stringify(payload);
|
|
149
|
-
fs.appendFileSync(logPath, entry
|
|
149
|
+
fs.appendFileSync(logPath, `${entry}\n`);
|
|
150
150
|
}
|
|
151
151
|
catch { /* fail-open: tracking itself must not throw */ }
|
|
152
152
|
return JSON.stringify({ continue: true });
|
|
@@ -19,7 +19,7 @@ export function recordHookTiming(hookName, durationMs, event) {
|
|
|
19
19
|
try {
|
|
20
20
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
21
21
|
const entry = JSON.stringify({ hook: hookName, ms: durationMs, event, at: Date.now() });
|
|
22
|
-
fs.appendFileSync(TIMING_LOG, entry
|
|
22
|
+
fs.appendFileSync(TIMING_LOG, `${entry}\n`);
|
|
23
23
|
// Rotate if too large — size-gated (statSync only, skip read/write 대부분의 호출)
|
|
24
24
|
try {
|
|
25
25
|
const size = fs.statSync(TIMING_LOG).size;
|
|
@@ -28,7 +28,7 @@ export function recordHookTiming(hookName, durationMs, event) {
|
|
|
28
28
|
const content = fs.readFileSync(TIMING_LOG, 'utf-8');
|
|
29
29
|
const lines = content.trim().split('\n');
|
|
30
30
|
if (lines.length > MAX_LINES) {
|
|
31
|
-
fs.writeFileSync(TIMING_LOG, lines.slice(-MAX_LINES).join('\n')
|
|
31
|
+
fs.writeFileSync(TIMING_LOG, `${lines.slice(-MAX_LINES).join('\n')}\n`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
catch { /* skip rotation on error */ }
|
|
@@ -52,7 +52,7 @@ export function getTimingStats() {
|
|
|
52
52
|
for (const e of entries) {
|
|
53
53
|
if (!byHook.has(e.hook))
|
|
54
54
|
byHook.set(e.hook, []);
|
|
55
|
-
byHook.get(e.hook)
|
|
55
|
+
byHook.get(e.hook)?.push(e.ms);
|
|
56
56
|
}
|
|
57
57
|
const stats = [];
|
|
58
58
|
for (const [hook, times] of byHook) {
|
|
@@ -409,7 +409,7 @@ async function main() {
|
|
|
409
409
|
.filter(l => l.length > 0);
|
|
410
410
|
contentSnippet = lines.slice(0, 3).join('\n');
|
|
411
411
|
if (contentSnippet.length > SUMMARY_MAX_CHARS) {
|
|
412
|
-
contentSnippet = contentSnippet.slice(0, SUMMARY_MAX_CHARS - 3)
|
|
412
|
+
contentSnippet = `${contentSnippet.slice(0, SUMMARY_MAX_CHARS - 3)}...`;
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
}
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -370,14 +370,14 @@ export function acknowledgeSessionBlocks(sessionId) {
|
|
|
370
370
|
try {
|
|
371
371
|
fs.mkdirSync(path.dirname(ACK_LOG), { recursive: true });
|
|
372
372
|
rotateIfBig(ACK_LOG);
|
|
373
|
-
fs.appendFileSync(ACK_LOG, JSON.stringify({
|
|
373
|
+
fs.appendFileSync(ACK_LOG, `${JSON.stringify({
|
|
374
374
|
at: now,
|
|
375
375
|
session_id: state.sessionId,
|
|
376
376
|
rule_id: state.ruleId,
|
|
377
377
|
block_count: state.count,
|
|
378
378
|
first_block_at: state.firstBlockAt,
|
|
379
379
|
last_block_at: state.lastBlockAt,
|
|
380
|
-
})
|
|
380
|
+
})}\n`);
|
|
381
381
|
acked += 1;
|
|
382
382
|
}
|
|
383
383
|
catch { /* append failure: still try cleanup */ }
|
|
@@ -396,7 +396,7 @@ export function logDriftEvent(event) {
|
|
|
396
396
|
try {
|
|
397
397
|
fs.mkdirSync(path.dirname(DRIFT_LOG), { recursive: true });
|
|
398
398
|
rotateIfBig(DRIFT_LOG);
|
|
399
|
-
fs.appendFileSync(DRIFT_LOG, JSON.stringify({ at: new Date().toISOString(), ...event })
|
|
399
|
+
fs.appendFileSync(DRIFT_LOG, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`);
|
|
400
400
|
}
|
|
401
401
|
catch {
|
|
402
402
|
// best-effort
|
|
@@ -33,5 +33,11 @@ export interface HostRuntime {
|
|
|
33
33
|
* - 'pre-baked-file': pkgRoot/hooks/hooks.json 읽고 ${CLAUDE_PLUGIN_ROOT} 치환 (Claude — 빌드 산출물 재사용)
|
|
34
34
|
*/
|
|
35
35
|
readonly hookInjectionStrategy: 'generate' | 'pre-baked-file';
|
|
36
|
+
/**
|
|
37
|
+
* 권한 전수 우회용 CLI 플래그 (fgx 등 dangerously-skip 모드에서 사용).
|
|
38
|
+
* Claude: --dangerously-skip-permissions
|
|
39
|
+
* Codex: --dangerously-bypass-approvals-and-sandbox
|
|
40
|
+
*/
|
|
41
|
+
readonly dangerousSkipFlag: string;
|
|
36
42
|
}
|
|
37
43
|
export declare function getHostRuntime(runtime: RuntimeHost): HostRuntime;
|
|
@@ -25,6 +25,7 @@ const claudeRuntime = {
|
|
|
25
25
|
return args ? `node ${quoteArg(fullScript)} ${args}` : `node ${quoteArg(fullScript)}`;
|
|
26
26
|
},
|
|
27
27
|
hookInjectionStrategy: 'pre-baked-file',
|
|
28
|
+
dangerousSkipFlag: '--dangerously-skip-permissions',
|
|
28
29
|
};
|
|
29
30
|
const codexRuntime = {
|
|
30
31
|
id: 'codex',
|
|
@@ -38,6 +39,7 @@ const codexRuntime = {
|
|
|
38
39
|
return args ? `${base} ${args}` : base;
|
|
39
40
|
},
|
|
40
41
|
hookInjectionStrategy: 'generate',
|
|
42
|
+
dangerousSkipFlag: '--dangerously-bypass-approvals-and-sandbox',
|
|
41
43
|
};
|
|
42
44
|
const RUNTIMES = {
|
|
43
45
|
claude: claudeRuntime,
|
|
@@ -202,7 +202,7 @@ function installCodexSkills(opts) {
|
|
|
202
202
|
return { installed: count };
|
|
203
203
|
}
|
|
204
204
|
// ── P3-3: AGENTS.md inject ────────────────────────────────────────────
|
|
205
|
-
function resolveAgentsMdPath(
|
|
205
|
+
function resolveAgentsMdPath(_pkgRoot) {
|
|
206
206
|
// Phase 3 critic fix: pkgRoot 기반 walk-up 은 `npm install -g` 시 시스템 디렉토리
|
|
207
207
|
// (예: /usr/local/lib/node_modules/forgen) 에 fallback AGENTS.md 작성 위험.
|
|
208
208
|
// *cwd 기반* 으로 변경 — 사용자 작업 디렉토리의 git root, 없으면 cwd 자체.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as readline from 'node:readline';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
15
16
|
import { detectAvailableHosts } from '../core/host-detect.js';
|
|
16
17
|
import { planClaudeInstall } from './install-claude.js';
|
|
17
18
|
import { planCodexInstall } from './install-codex.js';
|
|
@@ -121,6 +122,6 @@ export function renderResult(result, dryRun) {
|
|
|
121
122
|
}
|
|
122
123
|
/** pkgRoot resolve from binary location (dist/cli.js → pkgRoot). */
|
|
123
124
|
export function resolvePkgRootFromBinary(metaUrl) {
|
|
124
|
-
const here = path.dirname(
|
|
125
|
+
const here = path.dirname(fileURLToPath(metaUrl));
|
|
125
126
|
return path.resolve(here, '..');
|
|
126
127
|
}
|
package/dist/mcp/tools.js
CHANGED
|
@@ -29,8 +29,8 @@ export const COMMUNICATION_CENTROIDS = {
|
|
|
29
29
|
'상세형': { verbosity: 0.85, structure: 0.80, teaching_bias: 0.80 },
|
|
30
30
|
};
|
|
31
31
|
// ── Defaults (backward compat) ──
|
|
32
|
-
export const DEFAULT_JUDGMENT_FACETS = JUDGMENT_CENTROIDS
|
|
33
|
-
export const DEFAULT_COMMUNICATION_FACETS = COMMUNICATION_CENTROIDS
|
|
32
|
+
export const DEFAULT_JUDGMENT_FACETS = JUDGMENT_CENTROIDS.균형형;
|
|
33
|
+
export const DEFAULT_COMMUNICATION_FACETS = COMMUNICATION_CENTROIDS.균형형;
|
|
34
34
|
// ── Utilities ──
|
|
35
35
|
export function qualityCentroid(pack) {
|
|
36
36
|
return { ...QUALITY_CENTROIDS[pack] };
|
|
@@ -171,28 +171,28 @@ export function renderRules(rules, state, profile, ctx = DEFAULT_CONTEXT) {
|
|
|
171
171
|
for (const name of SECTION_ORDER)
|
|
172
172
|
sections.set(name, []);
|
|
173
173
|
for (const rule of hardRules) {
|
|
174
|
-
sections.get('Must Not')
|
|
174
|
+
sections.get('Must Not')?.push(ruleToText(rule));
|
|
175
175
|
}
|
|
176
176
|
for (const rule of otherRules) {
|
|
177
177
|
const section = CATEGORY_TO_SECTION[rule.category] ?? 'Working Defaults';
|
|
178
|
-
sections.get(section)
|
|
178
|
+
sections.get(section)?.push(ruleToText(rule));
|
|
179
179
|
}
|
|
180
180
|
// 5. trust policy + pack 기본 규칙 주입
|
|
181
181
|
if (ctx.include_pack_summary) {
|
|
182
|
-
sections.get('Working Defaults')
|
|
182
|
+
sections.get('Working Defaults')?.unshift(`Trust: ${trustPolicySummary(state.effective_trust_policy)}`);
|
|
183
183
|
// judgment pack 기본 규칙
|
|
184
184
|
for (const rule of judgmentPackRules(state.judgment_pack)) {
|
|
185
|
-
sections.get('Working Defaults')
|
|
185
|
+
sections.get('Working Defaults')?.push(rule);
|
|
186
186
|
}
|
|
187
187
|
// communication pack 기본 규칙
|
|
188
188
|
for (const rule of communicationPackRules(state.communication_pack)) {
|
|
189
|
-
sections.get('How To Report')
|
|
189
|
+
sections.get('How To Report')?.push(rule);
|
|
190
190
|
}
|
|
191
191
|
// 4축 facet 극단값 → 추가 규칙 (12-bucket pack 위에 연속 값 차별화).
|
|
192
192
|
// 각 facet 0.5 default 이고 자동 갱신 ±0.1 단위이므로, 0.85/0.15 임계값은
|
|
193
193
|
// 여러 세션에 걸친 강한 신호 누적 후에만 발화한다.
|
|
194
194
|
for (const fr of facetDrivenRules(profile)) {
|
|
195
|
-
sections.get(fr.section)
|
|
195
|
+
sections.get(fr.section)?.push(fr.rule);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
// 6. 섹션 조립 (AI-optimized: 간결한 태그 형식)
|
|
@@ -200,7 +200,7 @@ export function renderRules(rules, state, profile, ctx = DEFAULT_CONTEXT) {
|
|
|
200
200
|
let totalChars = 0;
|
|
201
201
|
let totalRules = 0;
|
|
202
202
|
for (const name of SECTION_ORDER) {
|
|
203
|
-
const items = sections.get(name);
|
|
203
|
+
const items = sections.get(name) ?? [];
|
|
204
204
|
if (items.length === 0)
|
|
205
205
|
continue;
|
|
206
206
|
const header = `## ${name}`;
|
|
@@ -27,7 +27,7 @@ export function recordUsage(name, via = 'mcp') {
|
|
|
27
27
|
try {
|
|
28
28
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
29
29
|
const entry = { at: new Date().toISOString(), name, via };
|
|
30
|
-
fs.appendFileSync(COMPOUND_USAGE_LOG, JSON.stringify(entry)
|
|
30
|
+
fs.appendFileSync(COMPOUND_USAGE_LOG, `${JSON.stringify(entry)}\n`);
|
|
31
31
|
}
|
|
32
32
|
catch {
|
|
33
33
|
// fail-open: 신호 수집 실패가 사용자 경험을 방해하면 안 됨
|
|
@@ -67,7 +67,7 @@ export function appendImplicitFeedback(entry) {
|
|
|
67
67
|
return false;
|
|
68
68
|
try {
|
|
69
69
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
70
|
-
fs.appendFileSync(IMPLICIT_FEEDBACK_LOG, JSON.stringify(normalized)
|
|
70
|
+
fs.appendFileSync(IMPLICIT_FEEDBACK_LOG, `${JSON.stringify(normalized)}\n`);
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
@@ -147,7 +147,7 @@ export function migrateImplicitFeedbackLog() {
|
|
|
147
147
|
}
|
|
148
148
|
// atomic replace via temp file
|
|
149
149
|
const tmp = `${IMPLICIT_FEEDBACK_LOG}.migrate.${process.pid}`;
|
|
150
|
-
fs.writeFileSync(tmp, out.length > 0 ? out.join('\n')
|
|
150
|
+
fs.writeFileSync(tmp, out.length > 0 ? `${out.join('\n')}\n` : '');
|
|
151
151
|
fs.renameSync(tmp, IMPLICIT_FEEDBACK_LOG);
|
|
152
152
|
return { migrated, dropped };
|
|
153
153
|
}
|
|
@@ -18,6 +18,17 @@ export declare function saveProfile(profile: Profile): void;
|
|
|
18
18
|
* whether to run `runLegacyCutover`).
|
|
19
19
|
*/
|
|
20
20
|
export declare function profileExists(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* profile.json 이 존재하지만 parse 실패 / v1 shape 위반인 경우, 사용자가
|
|
23
|
+
* 다음 onboarding 으로 매끄럽게 복구되도록 corrupt 파일을 timestamp 백업
|
|
24
|
+
* 으로 옆에 치워둔다. 백업 경로를 반환.
|
|
25
|
+
*
|
|
26
|
+
* v0.4.8 — bootstrapV1Session 의 loadProfile()=null early-return 보강.
|
|
27
|
+
* 이전엔 needsOnboarding=true 만 반환했고 corrupt 파일이 그대로 남아
|
|
28
|
+
* 다음 실행 때도 동일 분기로 빠지면서 사용자가 정체 원인을 모른 채
|
|
29
|
+
* onboarding 안내만 반복 받는 패턴이었음.
|
|
30
|
+
*/
|
|
31
|
+
export declare function backupCorruptProfile(): string | null;
|
|
21
32
|
export declare function isV1Profile(data: unknown): data is Profile;
|
|
22
33
|
/**
|
|
23
34
|
* D2 fix (2026-04-27): explicit_correction 누적 시 해당 축의 confidence 를 점진
|
|
@@ -66,6 +66,29 @@ export function saveProfile(profile) {
|
|
|
66
66
|
export function profileExists() {
|
|
67
67
|
return fs.existsSync(FORGE_PROFILE);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* profile.json 이 존재하지만 parse 실패 / v1 shape 위반인 경우, 사용자가
|
|
71
|
+
* 다음 onboarding 으로 매끄럽게 복구되도록 corrupt 파일을 timestamp 백업
|
|
72
|
+
* 으로 옆에 치워둔다. 백업 경로를 반환.
|
|
73
|
+
*
|
|
74
|
+
* v0.4.8 — bootstrapV1Session 의 loadProfile()=null early-return 보강.
|
|
75
|
+
* 이전엔 needsOnboarding=true 만 반환했고 corrupt 파일이 그대로 남아
|
|
76
|
+
* 다음 실행 때도 동일 분기로 빠지면서 사용자가 정체 원인을 모른 채
|
|
77
|
+
* onboarding 안내만 반복 받는 패턴이었음.
|
|
78
|
+
*/
|
|
79
|
+
export function backupCorruptProfile() {
|
|
80
|
+
if (!fs.existsSync(FORGE_PROFILE))
|
|
81
|
+
return null;
|
|
82
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
83
|
+
const backupPath = `${FORGE_PROFILE}.corrupt-${ts}`;
|
|
84
|
+
try {
|
|
85
|
+
fs.renameSync(FORGE_PROFILE, backupPath);
|
|
86
|
+
return backupPath;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
69
92
|
export function isV1Profile(data) {
|
|
70
93
|
if (!data || typeof data !== 'object')
|
|
71
94
|
return false;
|
package/package.json
CHANGED
package/plugin.json
CHANGED