@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
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 (
|
|
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
|
@@ -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(
|
|
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({
|
|
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;
|