@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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +78 -0
  3. package/dist/checks/self-score-deflation.js +6 -4
  4. package/dist/cli.js +6 -3
  5. package/dist/core/auto-compound-runner.js +6 -2
  6. package/dist/core/dashboard.js +2 -2
  7. package/dist/core/doctor.d.ts +10 -0
  8. package/dist/core/doctor.js +49 -8
  9. package/dist/core/harness.js +8 -2
  10. package/dist/core/inspect-cli.js +4 -4
  11. package/dist/core/migrate-evidence-host.js +1 -1
  12. package/dist/core/notify.js +7 -0
  13. package/dist/core/paths.d.ts +16 -2
  14. package/dist/core/paths.js +16 -2
  15. package/dist/core/session-store.d.ts +12 -1
  16. package/dist/core/session-store.js +73 -1
  17. package/dist/core/spawn.js +13 -7
  18. package/dist/core/v1-bootstrap.d.ts +7 -0
  19. package/dist/core/v1-bootstrap.js +28 -6
  20. package/dist/engine/compound-extractor.js +1 -1
  21. package/dist/engine/learn-cli.js +2 -2
  22. package/dist/engine/lifecycle/bypass-detector.js +3 -2
  23. package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
  24. package/dist/engine/lifecycle/signals.js +2 -2
  25. package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
  26. package/dist/engine/solution-candidate.js +1 -1
  27. package/dist/engine/solution-outcomes.js +1 -1
  28. package/dist/engine/solution-quarantine.js +1 -1
  29. package/dist/engine/solution-weakness.js +8 -2
  30. package/dist/fgx.js +6 -5
  31. package/dist/forge/cli.js +1 -1
  32. package/dist/hooks/keyword-detector.js +1 -1
  33. package/dist/hooks/secret-filter.js +2 -2
  34. package/dist/hooks/shared/hook-response.js +1 -1
  35. package/dist/hooks/shared/hook-timing.js +3 -3
  36. package/dist/hooks/solution-injector.js +1 -1
  37. package/dist/hooks/stop-guard.js +3 -3
  38. package/dist/host/host-runtime.d.ts +6 -0
  39. package/dist/host/host-runtime.js +2 -0
  40. package/dist/host/install-codex.js +1 -1
  41. package/dist/host/install-orchestrator.js +2 -1
  42. package/dist/mcp/tools.js +1 -1
  43. package/dist/preset/facet-catalog.js +2 -2
  44. package/dist/renderer/rule-renderer.js +7 -7
  45. package/dist/store/compound-usage-store.js +1 -1
  46. package/dist/store/implicit-feedback-store.js +2 -2
  47. package/dist/store/profile-store.d.ts +11 -0
  48. package/dist/store/profile-store.js +23 -0
  49. package/package.json +1 -1
  50. package/plugin.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.6",
4
+ "version": "0.4.8",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,84 @@ 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
+
50
+ ## [0.4.7] — 2026-05-15 — fgx --codex 권한 플래그 수정
51
+
52
+ ### Fixed
53
+ - `fgx --codex` 실행 시 `error: unexpected argument '--dangerously-skip-permissions' found`로
54
+ 기동이 즉시 실패하던 버그를 수정. Codex CLI 는 동일 목적의 플래그가
55
+ `--dangerously-bypass-approvals-and-sandbox` 라 Claude 전용 플래그를 그대로
56
+ 주입하면 거부됨.
57
+ - `HostRuntime.dangerousSkipFlag` 추상화를 도입해 `src/fgx.ts` 가 런타임별로
58
+ 올바른 플래그를 선택하도록 변경 (claude 동작은 회귀 없음).
59
+ - 경고 배너와 `[forgen] Mode:` 라벨도 선택된 플래그/런타임에 맞춰 동적으로 출력.
60
+ - **Windows 빌드 깨짐 (v0.4.4부터 누적)** 수정: `scripts/copy-assets.js`,
61
+ `src/cli.ts`, `src/host/install-orchestrator.ts` 가 `new URL(...).pathname` 으로
62
+ 파일 경로를 만들었는데 Windows 에서 `/D:/...` 형태가 그대로 노출되어
63
+ `mkdirSync` 가 `D:\D:\...` 로 해석 → ENOENT. `fileURLToPath()` 로 일괄 교체.
64
+ - **Linux CI test 잡 회귀** 수정: `npm run build` 만으로는 `hooks/hooks.json` 이
65
+ 생성되지 않아 (prepack 시점에만 생성) `claude-code-compat.test.ts` 등이
66
+ 실패. `.github/workflows/ci.yml` 의 test 잡에 `node scripts/prepack-hooks.cjs`
67
+ 단계를 추가하여 CI 환경에서도 hooks.json 이 준비된 상태로 vitest 가 돌도록.
68
+
69
+ ### CI / Platform Coverage
70
+ - `ci.yml` test 잡을 OS × Node 매트릭스로 확장:
71
+ ubuntu-latest × {20, 22}, macos-latest × {20, 22}, ubuntu-24.04-arm × 22.
72
+ 이전엔 vitest 풀 매트릭스가 ubuntu-latest 만 돌았음.
73
+ - **Windows**: hooks-portability 잡 (Node 20.x, 22.x × windows-latest) 에서
74
+ 빌드 + postinstall + 21/21 hook 로드를 검증. 풀 vitest 는 다수 통합
75
+ 테스트가 `/tmp` / POSIX path / bash spawn 가정에 묶여 있어 cross-platform
76
+ 재작성이 별도 트랙. 현재 Windows 사용자의 실사용 경로 (`npm i -g` +
77
+ Claude/Codex hook 발동) 는 보장됨.
78
+ - `actions/checkout` 에 `fetch-depth: 0` 추가 — `tests/git-stats.test.ts`
79
+ 가 실 git log 30일 윈도우를 분석.
80
+ - CI 환경 의존 테스트 (`rate-limit-spawn-integration`, `claude-integration`
81
+ 의 6-live / 3-live) 에 `it.skipIf(!!process.env.CI)` 가드.
82
+
83
+ ### Verified
84
+ - vitest 2442/2442 PASS, Docker e2e 77/77 PASS.
85
+ - `node dist/fgx.js --codex` / `--claude` 직접 기동 확인 — Codex CLI 가 플래그 수용.
86
+ - `npm run build` + `prepack-hooks` 후 `hooks/hooks.json` 21/21 active 재생성 확인.
87
+
10
88
  ## [0.4.6] — 2026-05-14 — Unattended Execution Resilience
11
89
 
12
90
  긴 무인 실행 (`forge-loop --goal-only` 새벽 실행, eval N=33+ sequential measurement)
@@ -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 ((m = re.exec(text)) !== null) {
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 ((m = re.exec(text)) !== null && out.length < max) {
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
@@ -195,7 +195,7 @@ const commands = [
195
195
  console.log('Usage:\n forgen parity codex [--dry-run]\n\nNotes:\n - source 체크아웃에서만 작동합니다 (tests/ 디렉토리 필요).\n - npm install 로 설치된 패키지에서는 run-parity.sh 가 없습니다.');
196
196
  return;
197
197
  }
198
- const here = path.dirname(new URL(import.meta.url).pathname);
198
+ const here = path.dirname(fileURLToPath(import.meta.url));
199
199
  const scriptPath = path.resolve(here, '..', 'tests', 'e2e', 'codex', 'run-parity.sh');
200
200
  if (!fs.existsSync(scriptPath)) {
201
201
  console.error('[forgen] run-parity.sh 는 source 체크아웃에서만 작동. 직접 git clone 후 실행하세요.');
@@ -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({ pruneState: args.includes('--prune-state') });
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.filter((x) => x?.type === 'text').map((x) => x.text ?? '').join('\n');
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
  /**
@@ -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 '' + cols.map((c, i) => c.padEnd(widths[i])).join(' │ ') + ' │';
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 ' ' + left + widths.map(w => '─'.repeat(w + 2)).join(mid) + right;
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. */
@@ -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>;
@@ -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 { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
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, 'Hook execution requires plugin cache. Fix: npm run build && node scripts/postinstall.js');
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, 'Plugin registered but installPath missing on disk. Fix: npm run build && node scripts/postinstall.js');
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: ${SESSIONS_DIR}`);
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: ${sessionCount}`);
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(' ' + '-'.repeat(56));
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).push(f);
543
+ bySection.get(f.section)?.push(f);
503
544
  }
504
545
  for (const [sec, items] of bySection) {
505
546
  console.log(` [${sec}]`);
@@ -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() + '\n' + compoundPointer + '\n');
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 + '.bak';
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}`);
@@ -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('\n' + inspect.renderProfile(profile) + '\n');
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('\n' + inspect.renderRules(rules) + '\n');
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('\n' + inspect.renderEvidence(evidence) + '\n');
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('\n' + inspect.renderSession(sessions[0]) + '\n');
93
+ console.log(`\n${inspect.renderSession(sessions[0])}\n`);
94
94
  return;
95
95
  }
96
96
  // R5-G1: 2AM 디버깅용 jsonl tail — violations/bypass/drift
@@ -35,7 +35,7 @@ export function migrateEvidenceHost(options) {
35
35
  skipped++;
36
36
  continue;
37
37
  }
38
- const host = data['host'];
38
+ const host = data.host;
39
39
  if (host === 'claude' || host === 'codex') {
40
40
  skipped++;
41
41
  continue;
@@ -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) {
@@ -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
- /** ~/.forgen/sessions/ — 세션 로그 */
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
- /** ~/.forgen/state/sessions/ — Session Effective State */
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;
@@ -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
- /** ~/.forgen/sessions/ — 세션 로그 */
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
- /** ~/.forgen/state/sessions/ — Session Effective State */
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
- * Transcript JSONL을 SQLite에 인덱싱.
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();
@@ -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 { indexSession } = await import('./session-store.js');
167
- await indexSession(cwd, transcriptPath, sessionId);
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 인덱싱 (claude only codex schema FTS 호환은 미검증, 별도 작업)
229
- if (runtime === 'claude') {
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;