@wooojin/forgen 0.4.7 → 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 +40 -0
- package/dist/checks/self-score-deflation.js +6 -4
- package/dist/cli.js +5 -2
- 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/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/install-codex.js +1 -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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.8] — 2026-05-15 — Codex 동등화 마무리 + 잔재 청소
|
|
11
|
+
|
|
12
|
+
테마: v0.4.6 (Unattended Resilience) 이후 남아 있던 **Codex 동등화 마무리**
|
|
13
|
+
(A 묶음) 와 v0.4.7 매트릭스 첫 활성화에서 노출된 **사전 존재 결함 청소**
|
|
14
|
+
(E 묶음) 을 한 번에 정리.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **A1**: Codex transcript FTS5 인덱싱 — `session-store.ts:indexCodexSession()`
|
|
18
|
+
신설. 이전엔 spawn.ts 가 `runtime === 'claude'` 가드로 Codex 세션을 SQLite
|
|
19
|
+
/ FTS5 인덱싱에서 제외해 `session-search` MCP 도구가 Codex 대화를 회수
|
|
20
|
+
못 했음. Claude/Codex schema 별 함수 분기.
|
|
21
|
+
- **A2**: corrupt profile 자동 복구 — `profileExists()=true && loadProfile()=null`
|
|
22
|
+
케이스 (parse 실패 / v1 shape 위반) 에서 `profile.json.corrupt-<ts>` 로
|
|
23
|
+
자동 backup → `needsOnboarding=true` 흐름. 데이터 손실 없음. `harness.ts`
|
|
24
|
+
가 backup 경로를 user-visible warning 으로 표시.
|
|
25
|
+
- **A3**: `SESSIONS_DIR` (legacy session log) 와 `V1_SESSIONS_DIR` (v1 effective
|
|
26
|
+
state) 정합화 — `V1_DIRS` 에 `SESSIONS_DIR` 추가 (bootstrap early-return
|
|
27
|
+
경로에서도 보장), `forgen doctor` 가 두 dir 모두 노출, `paths.ts` 책임
|
|
28
|
+
주석 확장.
|
|
29
|
+
- **E3**: `forgen doctor --repair` — plugin cache / installPath 검사 실패 시
|
|
30
|
+
`npm run build` + `node scripts/postinstall.js` 를 forgen pkgRoot 안에서
|
|
31
|
+
자동 실행. fail-open (실패해도 doctor 진단 흐름은 계속).
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **E1**: `notify.ts` spawn 'error' event 핸들러 — headless CI / Docker /
|
|
35
|
+
notifier 미설치 환경에서 `osascript`/`notify-send` ENOENT 가 unhandled
|
|
36
|
+
로 caller process 를 죽이던 사전 존재 버그. v0.4.7 CI 매트릭스 첫
|
|
37
|
+
활성화에서 노출. 추가로 `rate-limit-spawn-integration` 의 v0.4.7 CI
|
|
38
|
+
skip 가드를 제거 (production fail-safe 보장).
|
|
39
|
+
- **E2**: biome lint warnings 24 건 → 0 — `biome --unsafe` 자동 fix 17
|
|
40
|
+
건 + 수동 fix 7 건 (`useTemplate`, `noAssignInExpressions`,
|
|
41
|
+
`noExplicitAny`, `noNonNullAssertion` 등 33 files touched).
|
|
42
|
+
|
|
43
|
+
### Verified
|
|
44
|
+
- vitest 2454 / 2454 PASS (이전 2442 + 새 회귀 가드 12: notify 2, profile-
|
|
45
|
+
corrupt 3, doctor-repair 5, codex-fts 2).
|
|
46
|
+
- 로컬 build / lint 0 warning.
|
|
47
|
+
- CI 매트릭스 (Linux x64 + arm64, macOS, Windows hooks-portability) 모두
|
|
48
|
+
PASS — 별도 PR 머지 (#31 부분) 후 본 PR 에서 다시 확인.
|
|
49
|
+
|
|
10
50
|
## [0.4.7] — 2026-05-15 — fgx --codex 권한 플래그 수정
|
|
11
51
|
|
|
12
52
|
### Fixed
|
|
@@ -50,12 +50,13 @@ const SELF_SCORE_PATTERNS = [
|
|
|
50
50
|
function extractDeltas(text) {
|
|
51
51
|
const re = /(\d+(?:\.\d+)?)\s*(?:→|->|–>|~>)\s*(\d+(?:\.\d+)?)/g;
|
|
52
52
|
const out = [];
|
|
53
|
-
let m;
|
|
54
|
-
while (
|
|
53
|
+
let m = re.exec(text);
|
|
54
|
+
while (m !== null) {
|
|
55
55
|
const from = Number(m[1]);
|
|
56
56
|
const to = Number(m[2]);
|
|
57
57
|
if (Number.isFinite(from) && Number.isFinite(to))
|
|
58
58
|
out.push({ from, to });
|
|
59
|
+
m = re.exec(text);
|
|
59
60
|
}
|
|
60
61
|
return out;
|
|
61
62
|
}
|
|
@@ -66,9 +67,10 @@ function findScoreSignals(text, max = 3) {
|
|
|
66
67
|
break;
|
|
67
68
|
// 각 호출마다 lastIndex 초기화를 위해 새 RegExp 생성
|
|
68
69
|
const re = new RegExp(p.source, p.flags);
|
|
69
|
-
let m;
|
|
70
|
-
while (
|
|
70
|
+
let m = re.exec(text);
|
|
71
|
+
while (m !== null && out.length < max) {
|
|
71
72
|
out.push(m[0]);
|
|
73
|
+
m = re.exec(text);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
return out;
|
package/dist/cli.js
CHANGED
|
@@ -237,10 +237,13 @@ const commands = [
|
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
239
|
name: 'doctor',
|
|
240
|
-
description: 'Diagnostics (--prune-state to GC stale session files)',
|
|
240
|
+
description: 'Diagnostics (--prune-state to GC stale session files, --repair to auto-fix plugin cache)',
|
|
241
241
|
handler: async (args) => {
|
|
242
242
|
const { runDoctor } = await import('./core/doctor.js');
|
|
243
|
-
await runDoctor({
|
|
243
|
+
await runDoctor({
|
|
244
|
+
pruneState: args.includes('--prune-state'),
|
|
245
|
+
repair: args.includes('--repair'),
|
|
246
|
+
});
|
|
244
247
|
},
|
|
245
248
|
},
|
|
246
249
|
// install --plugin 제거됨 — postinstall이 유일한 설치 경로
|
|
@@ -219,8 +219,12 @@ function validateSolutionFiles(dirBefore) {
|
|
|
219
219
|
function extractText(c) {
|
|
220
220
|
if (typeof c === 'string')
|
|
221
221
|
return c;
|
|
222
|
-
if (Array.isArray(c))
|
|
223
|
-
return c
|
|
222
|
+
if (Array.isArray(c)) {
|
|
223
|
+
return c
|
|
224
|
+
.filter((x) => typeof x === 'object' && x !== null && x.type === 'text')
|
|
225
|
+
.map((x) => (typeof x.text === 'string' ? x.text : ''))
|
|
226
|
+
.join('\n');
|
|
227
|
+
}
|
|
224
228
|
return '';
|
|
225
229
|
}
|
|
226
230
|
/**
|
package/dist/core/dashboard.js
CHANGED
|
@@ -40,13 +40,13 @@ function dim(s) { return `${DIM}${s}${RESET}`; }
|
|
|
40
40
|
function cyan(s) { return `${CYAN}${s}${RESET}`; }
|
|
41
41
|
// ── Box-drawing table helpers ──
|
|
42
42
|
function tableRow(cols, widths) {
|
|
43
|
-
return
|
|
43
|
+
return ` │ ${cols.map((c, i) => c.padEnd(widths[i])).join(' │ ')} │`;
|
|
44
44
|
}
|
|
45
45
|
function tableSep(widths, top = false, bottom = false) {
|
|
46
46
|
const left = top ? '┌' : bottom ? '└' : '├';
|
|
47
47
|
const mid = top ? '┬' : bottom ? '┴' : '┼';
|
|
48
48
|
const right = top ? '┐' : bottom ? '┘' : '┤';
|
|
49
|
-
return
|
|
49
|
+
return ` ${left}${widths.map(w => '─'.repeat(w + 2)).join(mid)}${right}`;
|
|
50
50
|
}
|
|
51
51
|
// ── Data Collection Functions ──
|
|
52
52
|
/** Read all .md files in a directory and return their frontmatter. */
|
package/dist/core/doctor.d.ts
CHANGED
|
@@ -2,5 +2,15 @@ export interface DoctorOptions {
|
|
|
2
2
|
/** When true, delete stale session-scoped state files instead of just
|
|
3
3
|
* reporting bloat. Triggered by `forgen doctor --prune-state`. */
|
|
4
4
|
pruneState?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* When true, auto-fix recoverable failures (e.g. missing plugin cache /
|
|
7
|
+
* stale installPath) by running `npm run build` + `node scripts/postinstall.js`
|
|
8
|
+
* inside the forgen install directory. Triggered by `forgen doctor --repair`.
|
|
9
|
+
*
|
|
10
|
+
* v0.4.8 (E3) — 이전엔 안내문 ("Fix: npm run build && node scripts/postinstall.js")
|
|
11
|
+
* 만 출력했고 사용자가 직접 실행해야 했음. fail-open: repair 실패해도
|
|
12
|
+
* doctor 흐름은 정상 종료.
|
|
13
|
+
*/
|
|
14
|
+
repair?: boolean;
|
|
5
15
|
}
|
|
6
16
|
export declare function runDoctor(opts?: DoctorOptions): Promise<void>;
|
package/dist/core/doctor.js
CHANGED
|
@@ -2,7 +2,8 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR, V1_SESSIONS_DIR } from './paths.js';
|
|
6
7
|
import { getTimingStats } from '../hooks/shared/hook-timing.js';
|
|
7
8
|
import { countSessionScopedFiles, pruneState } from './state-gc.js';
|
|
8
9
|
import { summarizeAllByHost } from '../store/host-mismatch.js';
|
|
@@ -87,6 +88,29 @@ function renderCodexParity() {
|
|
|
87
88
|
console.log(` ✓ Codex parity green (last run: ${timeStr},${version})`);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* v0.4.8 (E3): plugin cache / installPath 진단이 실패했을 때 자동 복구.
|
|
93
|
+
* forgen 패키지 디렉토리에서 `npm run build` + postinstall 을 차례로 실행.
|
|
94
|
+
* 실패해도 doctor 자체는 계속 진행 (fail-open).
|
|
95
|
+
*/
|
|
96
|
+
function attemptPluginRepair() {
|
|
97
|
+
try {
|
|
98
|
+
// forgen 패키지 루트 = 현재 파일에서 dist/core/doctor.js 위치 → pkgRoot.
|
|
99
|
+
// dev (src/) 와 prod (dist/) 양쪽 모두 path.resolve(...,'..','..') 로 도달.
|
|
100
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
101
|
+
const pkgRoot = path.resolve(here, '..', '..');
|
|
102
|
+
console.log(`\n [Repair] forgen 패키지 자가복구 시도 — ${pkgRoot}`);
|
|
103
|
+
execFileSync('npm', ['run', 'build'], { cwd: pkgRoot, stdio: 'inherit' });
|
|
104
|
+
execFileSync('node', ['scripts/postinstall.js'], { cwd: pkgRoot, stdio: 'inherit' });
|
|
105
|
+
console.log(' [Repair] 완료. 진단 재실행 권장: forgen doctor');
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
console.warn(` [Repair] 실패: ${e instanceof Error ? e.message : String(e)}`);
|
|
110
|
+
console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> && npm run build && node scripts/postinstall.js');
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
90
114
|
export async function runDoctor(opts = {}) {
|
|
91
115
|
failedChecks = [];
|
|
92
116
|
console.log('\n Forgen — Diagnostics\n');
|
|
@@ -114,7 +138,9 @@ export async function runDoctor(opts = {}) {
|
|
|
114
138
|
});
|
|
115
139
|
forgenPluginCacheOk = versions.length > 0;
|
|
116
140
|
}
|
|
117
|
-
check('forgen plugin cache', forgenPluginCacheOk,
|
|
141
|
+
check('forgen plugin cache', forgenPluginCacheOk, opts.repair
|
|
142
|
+
? 'Hook execution requires plugin cache. Attempting auto-repair (--repair)…'
|
|
143
|
+
: 'Hook execution requires plugin cache. Fix: npm run build && node scripts/postinstall.js (or rerun with --repair)');
|
|
118
144
|
// installed_plugins.json 정합성 확인
|
|
119
145
|
const installedPluginsPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
120
146
|
let pluginRegistered = false;
|
|
@@ -129,7 +155,15 @@ export async function runDoctor(opts = {}) {
|
|
|
129
155
|
}
|
|
130
156
|
catch { /* ignore */ }
|
|
131
157
|
}
|
|
132
|
-
check('forgen plugin registered & installPath exists', pluginRegistered,
|
|
158
|
+
check('forgen plugin registered & installPath exists', pluginRegistered, opts.repair
|
|
159
|
+
? 'Plugin registered but installPath missing on disk. Attempting auto-repair (--repair)…'
|
|
160
|
+
: 'Plugin registered but installPath missing on disk. Fix: npm run build && node scripts/postinstall.js (or rerun with --repair)');
|
|
161
|
+
// v0.4.8 (E3): plugin cache 또는 installPath 가 깨졌고 --repair 가 켜져
|
|
162
|
+
// 있으면 build + postinstall 자동 실행. doctor 진단 자체는 계속 진행하여
|
|
163
|
+
// 사용자가 다른 health 항목도 한 번에 확인 가능.
|
|
164
|
+
if (opts.repair && (!forgenPluginCacheOk || !pluginRegistered)) {
|
|
165
|
+
attemptPluginRepair();
|
|
166
|
+
}
|
|
133
167
|
console.log();
|
|
134
168
|
section('Directories');
|
|
135
169
|
check('~/.forgen/', exists(FORGEN_HOME));
|
|
@@ -138,7 +172,8 @@ export async function runDoctor(opts = {}) {
|
|
|
138
172
|
check('~/.forgen/me/behavior/', exists(ME_BEHAVIOR));
|
|
139
173
|
check('~/.forgen/me/rules/', exists(ME_RULES));
|
|
140
174
|
check('~/.forgen/packs/', exists(PACKS_DIR));
|
|
141
|
-
check('~/.forgen/sessions/', exists(SESSIONS_DIR));
|
|
175
|
+
check('~/.forgen/sessions/ (session logs)', exists(SESSIONS_DIR));
|
|
176
|
+
check('~/.forgen/state/sessions/ (v1 effective state)', exists(V1_SESSIONS_DIR));
|
|
142
177
|
// R9-IA5: warn if a user dropped rule files at ~/.forgen/rules/ by mistake.
|
|
143
178
|
// That path is NOT loaded — personal rules live at ~/.forgen/me/rules/.
|
|
144
179
|
const legacyRulesPath = path.join(FORGEN_HOME, 'rules');
|
|
@@ -194,10 +229,16 @@ export async function runDoctor(opts = {}) {
|
|
|
194
229
|
}
|
|
195
230
|
console.log();
|
|
196
231
|
console.log(' [Log Locations]');
|
|
197
|
-
console.log(` Session logs:
|
|
232
|
+
console.log(` Session logs: ${SESSIONS_DIR}`);
|
|
198
233
|
if (exists(SESSIONS_DIR)) {
|
|
199
234
|
const sessionCount = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json')).length;
|
|
200
|
-
console.log(` Saved sessions:
|
|
235
|
+
console.log(` Saved sessions: ${sessionCount}`);
|
|
236
|
+
}
|
|
237
|
+
// v0.4.8 (A3): v1 effective state directory 도 가시화 — 두 dir 책임 다름.
|
|
238
|
+
console.log(` V1 effective state: ${V1_SESSIONS_DIR}`);
|
|
239
|
+
if (exists(V1_SESSIONS_DIR)) {
|
|
240
|
+
const stateCount = fs.readdirSync(V1_SESSIONS_DIR).filter((f) => f.endsWith('.json')).length;
|
|
241
|
+
console.log(` V1 state count: ${stateCount}`);
|
|
201
242
|
}
|
|
202
243
|
console.log(` Claude Code sessions: ${CLAUDE_PROJECTS_DIR}`);
|
|
203
244
|
console.log();
|
|
@@ -244,7 +285,7 @@ export async function runDoctor(opts = {}) {
|
|
|
244
285
|
}
|
|
245
286
|
else {
|
|
246
287
|
console.log(' Hook Count p50ms p95ms max ms');
|
|
247
|
-
console.log(
|
|
288
|
+
console.log(` ${'-'.repeat(56)}`);
|
|
248
289
|
for (const s of timingStats) {
|
|
249
290
|
const hook = s.hook.padEnd(22);
|
|
250
291
|
const count = String(s.count).padStart(5);
|
|
@@ -499,7 +540,7 @@ export async function runDoctor(opts = {}) {
|
|
|
499
540
|
for (const f of failedChecks) {
|
|
500
541
|
if (!bySection.has(f.section))
|
|
501
542
|
bySection.set(f.section, []);
|
|
502
|
-
bySection.get(f.section)
|
|
543
|
+
bySection.get(f.section)?.push(f);
|
|
503
544
|
}
|
|
504
545
|
for (const [sec, items] of bySection) {
|
|
505
546
|
console.log(` [${sec}]`);
|
package/dist/core/harness.js
CHANGED
|
@@ -165,7 +165,7 @@ function ensureCompoundMemory(cwd) {
|
|
|
165
165
|
const content = fs.readFileSync(memoryMdPath, 'utf-8');
|
|
166
166
|
if (content.includes('compound-index.md'))
|
|
167
167
|
return;
|
|
168
|
-
fs.writeFileSync(memoryMdPath, content.trimEnd()
|
|
168
|
+
fs.writeFileSync(memoryMdPath, `${content.trimEnd()}\n${compoundPointer}\n`);
|
|
169
169
|
}
|
|
170
170
|
const indexPath = path.join(memoryDir, 'compound-index.md');
|
|
171
171
|
const solutionsDir = ME_SOLUTIONS;
|
|
@@ -262,7 +262,7 @@ function migrateToForgen() {
|
|
|
262
262
|
catch (e) {
|
|
263
263
|
log.debug(`migrateToForgen: ${legacyHome} 파일 복사 중 오류`, e);
|
|
264
264
|
}
|
|
265
|
-
const backupPath = legacyHome
|
|
265
|
+
const backupPath = `${legacyHome}.bak`;
|
|
266
266
|
try {
|
|
267
267
|
if (!fs.existsSync(backupPath)) {
|
|
268
268
|
fs.renameSync(legacyHome, backupPath);
|
|
@@ -348,6 +348,12 @@ export async function prepareHarness(cwd, options = {}) {
|
|
|
348
348
|
if (v1Result.legacyBackupPath) {
|
|
349
349
|
log.debug(`v1: 레거시 프로필 백업 완료 → ${v1Result.legacyBackupPath}`);
|
|
350
350
|
}
|
|
351
|
+
if (v1Result.corruptProfileBackupPath) {
|
|
352
|
+
// v0.4.8 — corrupt profile auto-repair 결과는 debug 가 아닌 user-visible warning.
|
|
353
|
+
// 사용자가 onboarding 으로 보내지는 이유를 알 수 있도록.
|
|
354
|
+
console.warn(` ⚠ forgen: profile.json 이 깨져 있어 옆에 백업해두고 onboarding 으로 보냅니다.`);
|
|
355
|
+
console.warn(` ⚠ backup: ${v1Result.corruptProfileBackupPath}`);
|
|
356
|
+
}
|
|
351
357
|
if (v1Result.session) {
|
|
352
358
|
const { session } = v1Result;
|
|
353
359
|
log.debug(`v1 세션 시작: ${session.quality_pack}/${session.autonomy_pack}, trust=${session.effective_trust_policy}`);
|
package/dist/core/inspect-cli.js
CHANGED
|
@@ -21,7 +21,7 @@ export async function handleInspect(args) {
|
|
|
21
21
|
console.log('\n No v1 profile found. Run onboarding first.\n');
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
console.log(
|
|
24
|
+
console.log(`\n${inspect.renderProfile(profile)}\n`);
|
|
25
25
|
// ── Learning Loop Status ──
|
|
26
26
|
const activeRules = loadActiveRules();
|
|
27
27
|
const rulesByScope = {
|
|
@@ -75,13 +75,13 @@ export async function handleInspect(args) {
|
|
|
75
75
|
}
|
|
76
76
|
if (sub === 'rules') {
|
|
77
77
|
const rules = loadAllRules();
|
|
78
|
-
console.log(
|
|
78
|
+
console.log(`\n${inspect.renderRules(rules)}\n`);
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
// R9-IA2: user-facing name is "corrections"; "evidence" kept as back-compat alias.
|
|
82
82
|
if (sub === 'corrections' || sub === 'evidence') {
|
|
83
83
|
const evidence = loadRecentEvidence(20);
|
|
84
|
-
console.log(
|
|
84
|
+
console.log(`\n${inspect.renderEvidence(evidence)}\n`);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
if (sub === 'session') {
|
|
@@ -90,7 +90,7 @@ export async function handleInspect(args) {
|
|
|
90
90
|
console.log('\n No session state found.\n');
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
|
-
console.log(
|
|
93
|
+
console.log(`\n${inspect.renderSession(sessions[0])}\n`);
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
// R5-G1: 2AM 디버깅용 jsonl tail — violations/bypass/drift
|
package/dist/core/notify.js
CHANGED
|
@@ -37,6 +37,9 @@ function notifyDarwin(title, body) {
|
|
|
37
37
|
try {
|
|
38
38
|
const script = `display notification "${escapeForOsascript(body)}" with title "${escapeForOsascript(title)}"`;
|
|
39
39
|
const child = spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' });
|
|
40
|
+
// ENOENT 등 spawn 실패는 동기 throw 가 아니라 'error' event 로 emit 됨.
|
|
41
|
+
// 핸들러 없으면 unhandled 로 process crash → headless CI 회귀 가드.
|
|
42
|
+
child.on('error', (e) => log.debug('osascript notification 실패 (event)', e));
|
|
40
43
|
child.unref();
|
|
41
44
|
}
|
|
42
45
|
catch (e) {
|
|
@@ -47,6 +50,10 @@ function notifyDarwin(title, body) {
|
|
|
47
50
|
function notifyLinux(title, body) {
|
|
48
51
|
try {
|
|
49
52
|
const child = spawn('notify-send', [title, body], { detached: true, stdio: 'ignore' });
|
|
53
|
+
// headless 환경 (CI, Docker) 에서 notify-send 부재 시 ENOENT 가 'error' event 로 emit.
|
|
54
|
+
// 핸들러 없으면 unhandled 로 caller 프로세스 죽음 — rate-limit-spawn-integration
|
|
55
|
+
// CI 실패의 사전 존재 원인.
|
|
56
|
+
child.on('error', (e) => log.debug('notify-send 실패 (event)', e));
|
|
50
57
|
child.unref();
|
|
51
58
|
}
|
|
52
59
|
catch (e) {
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -60,7 +60,15 @@ export declare const OUTCOMES_DIR: string;
|
|
|
60
60
|
export declare const CANDIDATES_DIR: string;
|
|
61
61
|
/** ~/.forgen/lab/archived/ — rollback destination for evolved solutions. */
|
|
62
62
|
export declare const ARCHIVED_DIR: string;
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* ~/.forgen/sessions/ — legacy session log directory (transcript-like).
|
|
65
|
+
*
|
|
66
|
+
* session-logger.ts 가 prepareHarness step 11 에서 한 줄짜리
|
|
67
|
+
* `{date}_{uuid}.json` 메타 파일을 기록. v1 의 SessionEffectiveState
|
|
68
|
+
* 와는 다른 책임이라 별 디렉토리 (V1_SESSIONS_DIR) 와 공존:
|
|
69
|
+
* - SESSIONS_DIR (여기): 세션 발생 사실 + 시작/종료 시각 + cwd
|
|
70
|
+
* - V1_SESSIONS_DIR : profile + pack 합성 결과 (effective state)
|
|
71
|
+
*/
|
|
64
72
|
export declare const SESSIONS_DIR: string;
|
|
65
73
|
/** ~/.forgen/config.json — 글로벌 설정 */
|
|
66
74
|
export declare const GLOBAL_CONFIG: string;
|
|
@@ -72,7 +80,13 @@ export declare const LAB_DIR: string;
|
|
|
72
80
|
export declare const FORGE_PROFILE: string;
|
|
73
81
|
/** ~/.forgen/me/recommendations/ — Pack Recommendation */
|
|
74
82
|
export declare const V1_RECOMMENDATIONS_DIR: string;
|
|
75
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* ~/.forgen/state/sessions/ — v1 Session Effective State.
|
|
85
|
+
*
|
|
86
|
+
* session-state-store.ts 가 매 세션마다 profile + active rules + pack
|
|
87
|
+
* 합성 결과를 `{sessionId}.json` 으로 기록. SESSIONS_DIR (legacy session
|
|
88
|
+
* log) 와는 다른 책임 (정합화는 v0.4.8 A3 참조).
|
|
89
|
+
*/
|
|
76
90
|
export declare const V1_SESSIONS_DIR: string;
|
|
77
91
|
/** ~/.forgen/state/raw-logs/ — Raw Log */
|
|
78
92
|
export declare const V1_RAW_LOGS_DIR: string;
|
package/dist/core/paths.js
CHANGED
|
@@ -65,7 +65,15 @@ export const OUTCOMES_DIR = path.join(STATE_DIR, 'outcomes');
|
|
|
65
65
|
export const CANDIDATES_DIR = path.join(FORGEN_HOME, 'lab', 'candidates');
|
|
66
66
|
/** ~/.forgen/lab/archived/ — rollback destination for evolved solutions. */
|
|
67
67
|
export const ARCHIVED_DIR = path.join(FORGEN_HOME, 'lab', 'archived');
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* ~/.forgen/sessions/ — legacy session log directory (transcript-like).
|
|
70
|
+
*
|
|
71
|
+
* session-logger.ts 가 prepareHarness step 11 에서 한 줄짜리
|
|
72
|
+
* `{date}_{uuid}.json` 메타 파일을 기록. v1 의 SessionEffectiveState
|
|
73
|
+
* 와는 다른 책임이라 별 디렉토리 (V1_SESSIONS_DIR) 와 공존:
|
|
74
|
+
* - SESSIONS_DIR (여기): 세션 발생 사실 + 시작/종료 시각 + cwd
|
|
75
|
+
* - V1_SESSIONS_DIR : profile + pack 합성 결과 (effective state)
|
|
76
|
+
*/
|
|
69
77
|
export const SESSIONS_DIR = path.join(FORGEN_HOME, 'sessions');
|
|
70
78
|
/** ~/.forgen/config.json — 글로벌 설정 */
|
|
71
79
|
export const GLOBAL_CONFIG = path.join(FORGEN_HOME, 'config.json');
|
|
@@ -77,7 +85,13 @@ export const LAB_DIR = path.join(FORGEN_HOME, 'lab');
|
|
|
77
85
|
export const FORGE_PROFILE = path.join(ME_DIR, 'forge-profile.json');
|
|
78
86
|
/** ~/.forgen/me/recommendations/ — Pack Recommendation */
|
|
79
87
|
export const V1_RECOMMENDATIONS_DIR = path.join(ME_DIR, 'recommendations');
|
|
80
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* ~/.forgen/state/sessions/ — v1 Session Effective State.
|
|
90
|
+
*
|
|
91
|
+
* session-state-store.ts 가 매 세션마다 profile + active rules + pack
|
|
92
|
+
* 합성 결과를 `{sessionId}.json` 으로 기록. SESSIONS_DIR (legacy session
|
|
93
|
+
* log) 와는 다른 책임 (정합화는 v0.4.8 A3 참조).
|
|
94
|
+
*/
|
|
81
95
|
export const V1_SESSIONS_DIR = path.join(STATE_DIR, 'sessions');
|
|
82
96
|
/** ~/.forgen/state/raw-logs/ — Raw Log */
|
|
83
97
|
export const V1_RAW_LOGS_DIR = path.join(STATE_DIR, 'raw-logs');
|
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
* 외부 의존성 없음 — Node.js 22+ 내장 node:sqlite 사용.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* v0.4.8 (A1): Codex transcript JSONL 을 SQLite 에 인덱싱.
|
|
10
|
+
*
|
|
11
|
+
* Codex schema (Claude 와 다름):
|
|
12
|
+
* {type: 'response_item', payload: {type: 'message', role: 'user'|'assistant',
|
|
13
|
+
* content: [{type: 'input_text'|'output_text', text: '...'}]}}
|
|
14
|
+
*
|
|
15
|
+
* 결정 (v0.4.8 A1): Claude/Codex 통합 abstraction 대신 분기 함수 두 개로
|
|
16
|
+
* 처리. 미래에 host 가 추가될 때 통합 추상화로 리팩터.
|
|
17
|
+
*/
|
|
18
|
+
export declare function indexCodexSession(cwd: string, transcriptPath: string, sessionId: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Transcript JSONL을 SQLite에 인덱싱. (Claude schema)
|
|
10
21
|
*/
|
|
11
22
|
export declare function indexSession(cwd: string, transcriptPath: string, sessionId: string): Promise<void>;
|
|
12
23
|
/**
|
|
@@ -69,8 +69,80 @@ function openDb() {
|
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/** Codex content array → flat string. content: [{type: 'input_text', text: ...}, ...] */
|
|
73
|
+
function extractCodexText(content) {
|
|
74
|
+
if (!Array.isArray(content))
|
|
75
|
+
return '';
|
|
76
|
+
const parts = [];
|
|
77
|
+
for (const item of content) {
|
|
78
|
+
if (item && typeof item === 'object' && 'text' in item && typeof item.text === 'string') {
|
|
79
|
+
parts.push(item.text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return parts.join('\n').trim();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* v0.4.8 (A1): Codex transcript JSONL 을 SQLite 에 인덱싱.
|
|
86
|
+
*
|
|
87
|
+
* Codex schema (Claude 와 다름):
|
|
88
|
+
* {type: 'response_item', payload: {type: 'message', role: 'user'|'assistant',
|
|
89
|
+
* content: [{type: 'input_text'|'output_text', text: '...'}]}}
|
|
90
|
+
*
|
|
91
|
+
* 결정 (v0.4.8 A1): Claude/Codex 통합 abstraction 대신 분기 함수 두 개로
|
|
92
|
+
* 처리. 미래에 host 가 추가될 때 통합 추상화로 리팩터.
|
|
93
|
+
*/
|
|
94
|
+
export async function indexCodexSession(cwd, transcriptPath, sessionId) {
|
|
95
|
+
const db = openDb();
|
|
96
|
+
if (!db)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
|
|
100
|
+
if (existing)
|
|
101
|
+
return;
|
|
102
|
+
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
103
|
+
const lines = content.split('\n').filter(Boolean);
|
|
104
|
+
db.prepare('INSERT INTO sessions (id, cwd, started_at, message_count) VALUES (?, ?, ?, 0)').run(sessionId, cwd, new Date().toISOString());
|
|
105
|
+
let messageCount = 0;
|
|
106
|
+
const insertMsg = db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)');
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
try {
|
|
109
|
+
const entry = JSON.parse(line);
|
|
110
|
+
if (entry.type !== 'response_item' || !entry.payload)
|
|
111
|
+
continue;
|
|
112
|
+
const role = entry.payload.role;
|
|
113
|
+
if (role !== 'user' && role !== 'assistant')
|
|
114
|
+
continue;
|
|
115
|
+
const text = extractCodexText(entry.payload.content);
|
|
116
|
+
if (!text)
|
|
117
|
+
continue;
|
|
118
|
+
const truncated = text.slice(0, 10000);
|
|
119
|
+
const ts = typeof entry.timestamp === 'string' ? entry.timestamp : '';
|
|
120
|
+
const result = insertMsg.run(sessionId, role, truncated, ts);
|
|
121
|
+
if (fts5Available) {
|
|
122
|
+
try {
|
|
123
|
+
db.prepare('INSERT INTO messages_fts(rowid, content) VALUES (?, ?)').run(result.lastInsertRowid, truncated);
|
|
124
|
+
}
|
|
125
|
+
catch { /* FTS sync failure */ }
|
|
126
|
+
}
|
|
127
|
+
messageCount++;
|
|
128
|
+
}
|
|
129
|
+
catch { /* skip malformed lines */ }
|
|
130
|
+
}
|
|
131
|
+
db.prepare('UPDATE sessions SET message_count = ? WHERE id = ?').run(messageCount, sessionId);
|
|
132
|
+
log.debug(`Codex 세션 인덱싱 완료: ${sessionId} (${messageCount} messages)`);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
log.debug('Codex 세션 인덱싱 실패', e);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
try {
|
|
139
|
+
db.close();
|
|
140
|
+
}
|
|
141
|
+
catch { /* ignore */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
72
144
|
/**
|
|
73
|
-
* Transcript JSONL을 SQLite에 인덱싱.
|
|
145
|
+
* Transcript JSONL을 SQLite에 인덱싱. (Claude schema)
|
|
74
146
|
*/
|
|
75
147
|
export async function indexSession(cwd, transcriptPath, sessionId) {
|
|
76
148
|
const db = openDb();
|
package/dist/core/spawn.js
CHANGED
|
@@ -160,11 +160,19 @@ async function runAutoCompound(cwd, transcriptPath, sessionId) {
|
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
162
162
|
* Transcript를 SQLite FTS5에 인덱싱 (추후 session-search MCP 도구용).
|
|
163
|
+
*
|
|
164
|
+
* v0.4.8 (A1): runtime 별 schema 차이로 분기. Claude 는 `entry.type === 'user'|
|
|
165
|
+
* 'assistant'`, Codex 는 `entry.type === 'response_item' && entry.payload.role`.
|
|
163
166
|
*/
|
|
164
|
-
async function indexTranscriptToFTS(cwd, transcriptPath, sessionId) {
|
|
167
|
+
async function indexTranscriptToFTS(cwd, transcriptPath, sessionId, runtime = 'claude') {
|
|
165
168
|
try {
|
|
166
|
-
const
|
|
167
|
-
|
|
169
|
+
const store = await import('./session-store.js');
|
|
170
|
+
if (runtime === 'codex') {
|
|
171
|
+
await store.indexCodexSession(cwd, transcriptPath, sessionId);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await store.indexSession(cwd, transcriptPath, sessionId);
|
|
175
|
+
}
|
|
168
176
|
}
|
|
169
177
|
catch (e) {
|
|
170
178
|
log.debug('FTS5 인덱싱 실패 (session-store 미구현 시 정상)', e);
|
|
@@ -225,10 +233,8 @@ export async function spawnClaude(args, context, runtime = 'claude') {
|
|
|
225
233
|
else {
|
|
226
234
|
sessionId = path.basename(transcript, '.jsonl');
|
|
227
235
|
}
|
|
228
|
-
// 1. FTS5 인덱싱
|
|
229
|
-
|
|
230
|
-
await indexTranscriptToFTS(context.cwd, transcript, sessionId);
|
|
231
|
-
}
|
|
236
|
+
// 1. FTS5 인덱싱 — v0.4.8 (A1) 부터 Claude/Codex 모두 지원.
|
|
237
|
+
await indexTranscriptToFTS(context.cwd, transcript, sessionId, runtime);
|
|
232
238
|
// 2. 자동 compound (10+ user 메시지인 경우만) — 양 runtime 호환
|
|
233
239
|
const userMsgCount = await countUserMessages(transcript);
|
|
234
240
|
if (userMsgCount >= 10) {
|
|
@@ -18,6 +18,13 @@ export declare function ensureV1Directories(): void;
|
|
|
18
18
|
export interface V1BootstrapResult {
|
|
19
19
|
needsOnboarding: boolean;
|
|
20
20
|
legacyBackupPath: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* profile.json 이 존재했지만 parse/shape 오류로 사용 불가하여 v0.4.8
|
|
23
|
+
* 의 auto-repair 가 timestamp 백업으로 치워둔 경로. legacyBackupPath
|
|
24
|
+
* 와 의미가 다름 (legacy = model_version 구 버전 cutover, corrupt =
|
|
25
|
+
* 깨진 파일 격리).
|
|
26
|
+
*/
|
|
27
|
+
corruptProfileBackupPath: string | null;
|
|
21
28
|
session: SessionEffectiveState | null;
|
|
22
29
|
renderedRules: string | null;
|
|
23
30
|
profile: Profile | null;
|
|
@@ -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/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
|
|
@@ -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 자체.
|
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