@wooojin/forgen 0.4.5 → 0.4.7
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 +270 -0
- package/dist/cli.js +1 -1
- package/dist/core/auto-compound-runner.d.ts +13 -1
- package/dist/core/auto-compound-runner.js +93 -1
- package/dist/core/doctor.js +9 -0
- package/dist/core/harness.js +60 -1
- package/dist/core/notify.d.ts +18 -0
- package/dist/core/notify.js +93 -0
- package/dist/core/spawn.d.ts +13 -2
- package/dist/core/spawn.js +154 -32
- package/dist/core/state-gc.d.ts +10 -0
- package/dist/core/state-gc.js +71 -0
- package/dist/core/statusline-cli.js +55 -0
- package/dist/core/usage-telemetry.d.ts +34 -0
- package/dist/core/usage-telemetry.js +101 -0
- package/dist/fgx.js +6 -5
- package/dist/hooks/context-guard.d.ts +13 -0
- package/dist/hooks/context-guard.js +155 -4
- package/dist/hooks/post-tool-use.js +3 -0
- package/dist/hooks/pre-tool-use.js +31 -0
- package/dist/hooks/session-recovery.js +11 -0
- package/dist/hooks/shared/read-stdin.js +44 -29
- package/dist/host/codex-adapter.js +5 -0
- package/dist/host/host-runtime.d.ts +6 -0
- package/dist/host/host-runtime.js +2 -0
- package/dist/host/install-codex.js +7 -1
- package/dist/host/install-orchestrator.js +2 -1
- package/dist/services/session.js +13 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,276 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.7] — 2026-05-15 — fgx --codex 권한 플래그 수정
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `fgx --codex` 실행 시 `error: unexpected argument '--dangerously-skip-permissions' found`로
|
|
14
|
+
기동이 즉시 실패하던 버그를 수정. Codex CLI 는 동일 목적의 플래그가
|
|
15
|
+
`--dangerously-bypass-approvals-and-sandbox` 라 Claude 전용 플래그를 그대로
|
|
16
|
+
주입하면 거부됨.
|
|
17
|
+
- `HostRuntime.dangerousSkipFlag` 추상화를 도입해 `src/fgx.ts` 가 런타임별로
|
|
18
|
+
올바른 플래그를 선택하도록 변경 (claude 동작은 회귀 없음).
|
|
19
|
+
- 경고 배너와 `[forgen] Mode:` 라벨도 선택된 플래그/런타임에 맞춰 동적으로 출력.
|
|
20
|
+
- **Windows 빌드 깨짐 (v0.4.4부터 누적)** 수정: `scripts/copy-assets.js`,
|
|
21
|
+
`src/cli.ts`, `src/host/install-orchestrator.ts` 가 `new URL(...).pathname` 으로
|
|
22
|
+
파일 경로를 만들었는데 Windows 에서 `/D:/...` 형태가 그대로 노출되어
|
|
23
|
+
`mkdirSync` 가 `D:\D:\...` 로 해석 → ENOENT. `fileURLToPath()` 로 일괄 교체.
|
|
24
|
+
- **Linux CI test 잡 회귀** 수정: `npm run build` 만으로는 `hooks/hooks.json` 이
|
|
25
|
+
생성되지 않아 (prepack 시점에만 생성) `claude-code-compat.test.ts` 등이
|
|
26
|
+
실패. `.github/workflows/ci.yml` 의 test 잡에 `node scripts/prepack-hooks.cjs`
|
|
27
|
+
단계를 추가하여 CI 환경에서도 hooks.json 이 준비된 상태로 vitest 가 돌도록.
|
|
28
|
+
|
|
29
|
+
### CI / Platform Coverage
|
|
30
|
+
- `ci.yml` test 잡을 OS × Node 매트릭스로 확장:
|
|
31
|
+
ubuntu-latest × {20, 22}, macos-latest × {20, 22}, ubuntu-24.04-arm × 22.
|
|
32
|
+
이전엔 vitest 풀 매트릭스가 ubuntu-latest 만 돌았음.
|
|
33
|
+
- **Windows**: hooks-portability 잡 (Node 20.x, 22.x × windows-latest) 에서
|
|
34
|
+
빌드 + postinstall + 21/21 hook 로드를 검증. 풀 vitest 는 다수 통합
|
|
35
|
+
테스트가 `/tmp` / POSIX path / bash spawn 가정에 묶여 있어 cross-platform
|
|
36
|
+
재작성이 별도 트랙. 현재 Windows 사용자의 실사용 경로 (`npm i -g` +
|
|
37
|
+
Claude/Codex hook 발동) 는 보장됨.
|
|
38
|
+
- `actions/checkout` 에 `fetch-depth: 0` 추가 — `tests/git-stats.test.ts`
|
|
39
|
+
가 실 git log 30일 윈도우를 분석.
|
|
40
|
+
- CI 환경 의존 테스트 (`rate-limit-spawn-integration`, `claude-integration`
|
|
41
|
+
의 6-live / 3-live) 에 `it.skipIf(!!process.env.CI)` 가드.
|
|
42
|
+
|
|
43
|
+
### Verified
|
|
44
|
+
- vitest 2442/2442 PASS, Docker e2e 77/77 PASS.
|
|
45
|
+
- `node dist/fgx.js --codex` / `--claude` 직접 기동 확인 — Codex CLI 가 플래그 수용.
|
|
46
|
+
- `npm run build` + `prepack-hooks` 후 `hooks/hooks.json` 21/21 active 재생성 확인.
|
|
47
|
+
|
|
48
|
+
## [0.4.6] — 2026-05-14 — Unattended Execution Resilience
|
|
49
|
+
|
|
50
|
+
긴 무인 실행 (`forge-loop --goal-only` 새벽 실행, eval N=33+ sequential measurement)
|
|
51
|
+
이 API rate-limit 을 만나도 자동 sleep + reset 후 재기동하도록 한 테마. 동시에
|
|
52
|
+
Codex hook side-effect 갭 (PermissionRequest dispatch 누락) 보완 + dead writer
|
|
53
|
+
복구 + 성능 3종 단축.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
#### Theme F — Codex/Claude 동등 사용 (사용자 요청)
|
|
58
|
+
|
|
59
|
+
claude 와 codex 를 일상에서 동등하게 사용할 수 있게 forgen 측 갭 메움.
|
|
60
|
+
|
|
61
|
+
- **session-recovery hook 에서 v1-bootstrap 호출** (src/hooks/session-recovery.ts)
|
|
62
|
+
- 이전엔 prepareHarness (fgx/forgen wrapper) 만 bootstrapV1Session 호출 →
|
|
63
|
+
직접 codex/claude 호출 시 `~/.forgen/state/sessions/<id>.json` 미생성.
|
|
64
|
+
SessionStart hook 에서도 호출하여 양쪽 진입 경로 모두에서 session state 박제
|
|
65
|
+
(단 profile 정상 시 — onboarding 안 한 사용자는 둘 다 동등하게 skip).
|
|
66
|
+
|
|
67
|
+
- **Codex transcript 위치 인식** (src/core/spawn.ts `transcriptProjectDir` 분기)
|
|
68
|
+
- claude: `~/.claude/projects/<sanitized-cwd>/<session>.jsonl`
|
|
69
|
+
- codex: `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<sid>.jsonl`
|
|
70
|
+
- 위치 인식 + snapshot-diff 기반 attribution 양쪽 작동.
|
|
71
|
+
- **auto-compound input parsing — codex JSONL schema 호환 추가**:
|
|
72
|
+
- `countUserMessages` (spawn.ts) + `extractSummary` (auto-compound-runner.ts)
|
|
73
|
+
양쪽 모두 claude (`type='user'|'queue-operation'`) + codex
|
|
74
|
+
(`type='response_item'` with `payload.role='user'|'assistant'`,
|
|
75
|
+
`content[].text`) 자동 감지 처리
|
|
76
|
+
- codex transcript 도 user message ≥10 시 auto-compound 자동 트리거
|
|
77
|
+
- sessionId 는 rollout 파일명 패턴 (`rollout-<ts>-<sid>.jsonl`) 에서 추출
|
|
78
|
+
- FTS5 인덱싱은 claude 한정 (codex schema FTS 매핑 micro-issue, 추후)
|
|
79
|
+
|
|
80
|
+
- **동등 작동 항목** (라운드 25+ clean container 검증 합산):
|
|
81
|
+
- Hook 발화 (8 hook 종, codex 0.128.x): 동등
|
|
82
|
+
- prompt-history 기록: 동등 (secret redaction 포함)
|
|
83
|
+
- permissions-<id>.jsonl 기록: 동등 (codex 는 `source: 'pre-tool-use'` 경유)
|
|
84
|
+
- usage-telemetry `rt` 필드: 동등 (#17 fix 후)
|
|
85
|
+
- statusline usage 라인: 동등
|
|
86
|
+
- rate-limit detector + spawn loop: 코드 공유, runtime 분기 1줄
|
|
87
|
+
- `fgx --<runtime>` 단축 + auto-reconcile-on-launch: 동등
|
|
88
|
+
|
|
89
|
+
- **비동등 잔존 (codex CLI 측 구조적 한계)**:
|
|
90
|
+
- PermissionRequest 이벤트: codex `approval_policy=auto` 에서 미dispatch
|
|
91
|
+
(codex CLI 한계) — PreToolUse supplement 로 결과 데이터는 동등화
|
|
92
|
+
- Subagent dispatch: codex CLI 의 subagent 개념 미지원 — hooks 등록만 존재
|
|
93
|
+
- Codex 0.130.0 hooks dispatch 회귀: codex CLI 0.130 binary 안에 hook event
|
|
94
|
+
schema (HookSpecificOutputWire) 와 `hooks: stable: true` feature flag 모두
|
|
95
|
+
있는데도 dispatch 안 됨. config.toml 의 hooks 경로 명시도 무효. codex CLI
|
|
96
|
+
내부 변경으로 forgen 측에서 worktime 짧게 fix 불가. 0.128.x 권장 + 0.4.7 에
|
|
97
|
+
codex GitHub issue 제기 예정
|
|
98
|
+
|
|
99
|
+
#### Theme E — Codex auto-onboarding (사용자 요청)
|
|
100
|
+
|
|
101
|
+
- **`fgx --codex` / `fgx --claude` 단축 플래그** (src/services/session.ts)
|
|
102
|
+
- 기존 `--runtime codex` 의 짧은 alias. 일상 진입이 더 빠름.
|
|
103
|
+
|
|
104
|
+
- **Codex hooks 자동 reconcile-on-launch** (src/core/harness.ts `ensureCodexHooksFresh`)
|
|
105
|
+
- 매 codex runtime 진입 (fgx --codex / forgen --runtime codex) 시 ~/.codex/hooks.json
|
|
106
|
+
의 forgen entry 가 현재 pkgRoot 와 일치하는지 fast staleness check.
|
|
107
|
+
- 부재/stale → planCodexInstall silent 재실행. 일치 → no-op (1ms).
|
|
108
|
+
- 사용자가 `forgen install codex` 를 명시 호출 안 해도 매번 정합성 보장.
|
|
109
|
+
Cross-machine sync, npm 글로벌 path 변경, forgen 버전 업그레이드 후
|
|
110
|
+
자동 회복.
|
|
111
|
+
- **검증**: clean container 3 시나리오 — A (clean state → 자동 생성),
|
|
112
|
+
B (동일 상태 → no-op idempotent), C (stale `/FAKE/PATH` → 자동 정정).
|
|
113
|
+
모두 통과.
|
|
114
|
+
|
|
115
|
+
#### Theme A — Unattended Execution Resilience
|
|
116
|
+
|
|
117
|
+
- **Rate-limit auto-resume** (ADR-008, `commit TBD`)
|
|
118
|
+
- `context-guard.ts` Stop hook 에 `RATE_LIMIT_REGEX` + 5 패턴 reset 시각 파서
|
|
119
|
+
추가 (Resets at HH:MM, in Nh Nm, in N seconds, available again at ISO,
|
|
120
|
+
try again in N min). 22 unit tests.
|
|
121
|
+
- `pending-resume.json` schema 확장: `reason: 'rate-limit' | 'token-limit'`,
|
|
122
|
+
`resetAt?: ISO`, `runtime: 'claude' | 'codex'`. 기존 token-limit 호환 유지.
|
|
123
|
+
- `spawnClaudeWithResume` (src/core/spawn.ts) rate-limit 분기:
|
|
124
|
+
`resetAt` 정확하면 정밀 sleep + 60s 버퍼, 실패 시 exponential backoff
|
|
125
|
+
(1m → 5m → 15m → 30m → 1h → 2h cap). hard cap 6h. foreground countdown
|
|
126
|
+
(30s 갱신, Ctrl+C abort). `MAX_RESUMES`: token=3 (현행), rate-limit=10.
|
|
127
|
+
- **Fix-forward 정책**: detector regex 가 실 메시지와 어긋나면 `~/.forgen/state/
|
|
128
|
+
rate-limit-misses.jsonl` 에 raw 누적 → patch release 로 hotfix.
|
|
129
|
+
- **알려진 한계**: weekly limit (최대 7일) > 6h hard cap, abort + 명시 메시지.
|
|
130
|
+
"Resets at 14:30 PST" TZ 무시 (UTC 가정) — 첫 실 트리거 후 hotfix 예정.
|
|
131
|
+
|
|
132
|
+
- **Usage telemetry** (src/core/usage-telemetry.ts)
|
|
133
|
+
- 5h / weekly window sliding count. `recordToolCall` 가 PostToolUse 마다
|
|
134
|
+
append-only `~/.forgen/state/usage-telemetry.jsonl` 에 timestamp 기록.
|
|
135
|
+
- 10K 엔트리 누적 시 weekly cap 밖 자동 prune. claude/codex 별도 카운트.
|
|
136
|
+
- 의도적으로 "limit prediction" 제외 — Anthropic 의 실 limit 가 계정/플랜별
|
|
137
|
+
가변. raw count 만 노출하고 사용자가 판단.
|
|
138
|
+
|
|
139
|
+
- **Statusline usage 라인** (src/core/statusline-cli.ts)
|
|
140
|
+
- 새 라인: `📊 87/5h · 412/wk · (claude)` — 5h/weekly 추세 노출.
|
|
141
|
+
|
|
142
|
+
- **Notification 모듈** (src/core/notify.ts)
|
|
143
|
+
- macOS: `osascript display notification`, Linux: `notify-send`,
|
|
144
|
+
Windows: 생략. webhook (Slack/Discord 호환) 지원: `~/.forgen/config.json`
|
|
145
|
+
의 `notifyWebhookUrl`. rate-limit auto-resume 이 sleep 끝나고 재기동 시점에
|
|
146
|
+
발송 — 노트북 닫고 잘 때 끝났는지 알 수 있음.
|
|
147
|
+
|
|
148
|
+
#### Theme B — Codex hook 갭 보완
|
|
149
|
+
|
|
150
|
+
- **Codex hooks dispatch 죽음 root-cause 박제** (docs/codex-integration.md)
|
|
151
|
+
- 사용자 보고된 "Codex hooks 죽음" 의 진짜 원인 3가지 분리:
|
|
152
|
+
1. `prompt-history.jsonl` writer 부재 (dead code 잔재) — 0.4.6 writer 신설
|
|
153
|
+
2. `context-signals.json` 정상 동작 (도구 실패 시에만 write — 의도)
|
|
154
|
+
3. `permissions-<id>.jsonl` 미생성 — Codex `approval_policy=auto`/
|
|
155
|
+
`workspace-write` 에서 PermissionRequest hook 자체가 dispatch 안 됨
|
|
156
|
+
(forgen 측 버그 아님 — Codex CLI 정책)
|
|
157
|
+
- 기각: projection.ts side-effect 손실, STATE_DIR 미스매치 (모두 정상 검증)
|
|
158
|
+
|
|
159
|
+
- **PreToolUse permission supplement** (src/hooks/pre-tool-use.ts)
|
|
160
|
+
- Codex `auto` 환경에서도 권한 흐름 가시화 — 모든 PreToolUse 가
|
|
161
|
+
`permissions-<sessionId>.jsonl` 에 `source: 'pre-tool-use'` entry append.
|
|
162
|
+
Claude permission-handler 와 source 필드로 reader 측 dedup.
|
|
163
|
+
|
|
164
|
+
- **prompt-history writer 신설** (src/hooks/context-guard.ts)
|
|
165
|
+
- UserPromptSubmit 마다 truncated (1KB) prompt 를 `~/.forgen/state/
|
|
166
|
+
prompt-history.jsonl` append. **secret-filter 거쳐 redact** —
|
|
167
|
+
password/api_key/token/AWS/GitHub 등 평문 leak 차단. compound-extractor.ts
|
|
168
|
+
의 dead read 코드가 의미를 가짐.
|
|
169
|
+
|
|
170
|
+
#### Theme C — Performance
|
|
171
|
+
|
|
172
|
+
- **Hook stdin idle-resolve + initial-wait fallback** (src/hooks/shared/read-stdin.ts) — perf #11
|
|
173
|
+
- hook-timing.jsonl 측정 결과 pre-tool-use **p95 = 2003ms** (정확히
|
|
174
|
+
timeout 값) — Claude/Codex CLI 가 stdin EOF 안 닫거나 stdin 자체 안 보내는
|
|
175
|
+
케이스. 두 단계 fix:
|
|
176
|
+
1. `IDLE_RESOLVE_MS=100`: 'data' 받은 후 추가 chunk 없으면 early resolve
|
|
177
|
+
2. `INITIAL_WAIT_MS=300`: 'data' 자체가 안 오는 케이스 (codex 일부 hook
|
|
178
|
+
event) 대비 300ms 후 빈 데이터로 fallback resolve
|
|
179
|
+
- 합법적 데이터 손실 위험 없음 — payload 는 호출 즉시 (≤ 300ms) 첫 chunk 도착
|
|
180
|
+
- **검증**: 실 codex 2-라운드 e2e — 1라운드 (idle 만): pre-tool-use 4건 중
|
|
181
|
+
1건이 ms=2004 잔존. 2라운드 (initial-wait 추가 후): 9 hook entry 모두 < 50ms,
|
|
182
|
+
2003ms tail 완전 제거.
|
|
183
|
+
|
|
184
|
+
- **Auto-compound adaptive cooldown** (src/hooks/context-guard.ts) — perf #12
|
|
185
|
+
- Last run 이 0건 추출 (barren) 했으면 다음 cooldown 5min → 30min.
|
|
186
|
+
`extractedSolutions + promotedRules + userPatternFound` 합산 = 0 판정.
|
|
187
|
+
- Wasted background runs 6x 감소. 일반 case (추출 있음) 5분 유지.
|
|
188
|
+
- Full LLM 호출 parallelization 은 0.4.7 로 분리 — `execClaudeRetryAsync`
|
|
189
|
+
foundation 만 박제 (refactor surface 큼, sandbox + file-write order
|
|
190
|
+
invariant 검증 필요).
|
|
191
|
+
|
|
192
|
+
- **Statusline 5초 캐싱** (src/core/statusline-cli.ts) — perf #13
|
|
193
|
+
- `~/.forgen/state/statusline-cache.txt` mtime 기반. CACHE_TTL_MS=5_000.
|
|
194
|
+
- **결과**: 160ms → 101ms (37%). Node 시작 50ms 가 floor.
|
|
195
|
+
|
|
196
|
+
#### Theme D — Maintenance
|
|
197
|
+
|
|
198
|
+
- **Append-only jsonl 회전** (src/core/state-gc.ts `rotateAppendOnlyLogs`) — #14
|
|
199
|
+
- state-gc 의 SESSION_SCOPED_PREFIXES 가 단일 aggregate jsonl
|
|
200
|
+
(hook-timing, prompt-history, usage-telemetry 등) 무한 grow 미커버 갭 수정.
|
|
201
|
+
- 10MB cap 초과 시 `<name>.1` rotate, `<name>.2` 삭제 (한 단계 보존).
|
|
202
|
+
- `forgen doctor --prune-state` 와 함께 자동 실행. 5 unit tests.
|
|
203
|
+
|
|
204
|
+
### Verification
|
|
205
|
+
|
|
206
|
+
- **vitest**: 2441/2441 PASS (225 files), 0 regression
|
|
207
|
+
- **Docker e2e (existing)**: 77/77 PASS, 6 warnings (pre-existing)
|
|
208
|
+
- **신규 unit tests**: rate-limit-detection (22) + rate-limit-backoff (9) +
|
|
209
|
+
log-rotation (5) = 36
|
|
210
|
+
|
|
211
|
+
#### Clean-container e2e (`tests/e2e/docker/Dockerfile.v046`)
|
|
212
|
+
|
|
213
|
+
새 Docker e2e 가 host 권한 없이 깨끗한 container 안에서 실 claude + codex 호출
|
|
214
|
+
하여 hook side-effect 를 검증. host UID/GID 매핑 + ~/.codex auth 마운트.
|
|
215
|
+
|
|
216
|
+
**최종 결과: 17/18 PASS** (1 fail = claude OAuth keychain 미전달 — 환경 한계)
|
|
217
|
+
|
|
218
|
+
- ✅ **Codex 측 100% 검증** (codex 0.128.0):
|
|
219
|
+
- hook-timing.jsonl +9 entries
|
|
220
|
+
- **permissions-<codex-id>.jsonl 생성** with `source: 'pre-tool-use'` —
|
|
221
|
+
사용자 보고된 "Codex hooks 죽음" 갭 fix 가 진짜 e2e 환경에서 작동 확인
|
|
222
|
+
- usage-telemetry +1 (PostToolUse hook 발화)
|
|
223
|
+
- prompt-history +1 (UserPromptSubmit hook 발화)
|
|
224
|
+
- **pre-tool-use ms=10ms** (no 2003ms tail — #11 fix 실 codex 환경 검증)
|
|
225
|
+
- Secret redaction (clean container 에서 GitHub Token redact 확인)
|
|
226
|
+
- Rate-limit detector synthetic Stop event → marker 정상 작성
|
|
227
|
+
|
|
228
|
+
- ⚠️ **Claude 측 부분 검증**: hook 발화 +5 (UserPromptSubmit 만), 하지만
|
|
229
|
+
"Not logged in" 으로 도구 호출까지 안 감 → PostToolUse / PermissionRequest
|
|
230
|
+
미발화. **원인은 환경 (macOS Keychain 의 OAuth 토큰을 container 로 전달
|
|
231
|
+
불가)**, 코드 문제 아님.
|
|
232
|
+
|
|
233
|
+
### Discovered + Fixed during e2e
|
|
234
|
+
|
|
235
|
+
- **#17 — codex-adapter.ts FORGEN_RUNTIME 미주입** (FIXED)
|
|
236
|
+
- 검증 라운드 20-2 에서 `usage-telemetry.jsonl` 직접 inspect 시 codex 호출
|
|
237
|
+
인데 `{"rt":"claude"}` 발견. `recordToolCall` 가 `process.env.FORGEN_RUNTIME`
|
|
238
|
+
fall-through 로 'claude' 판정.
|
|
239
|
+
- Fix: codex-adapter.ts 의 `spawnSync` 에 `env: { ...process.env,
|
|
240
|
+
FORGEN_RUNTIME: 'codex' }` 명시 주입.
|
|
241
|
+
- 영향: statusline 의 "(codex)" 표시 + telemetry 분리 정상화.
|
|
242
|
+
|
|
243
|
+
- **#15 — install-codex.ts isForgenManagedHook 버그** (FIXED)
|
|
244
|
+
- Root cause: `command.includes(pkgRoot)` exact match 만 체크 → 다른 머신에서
|
|
245
|
+
install 한 hooks.json 이 마운트되면 stale path entry 가 "user entry" 로
|
|
246
|
+
오분류 → 보존 + 새 entry 누적 → codex 가 stale path 로 dispatch 시도 →
|
|
247
|
+
silent fail
|
|
248
|
+
- Fix: `FORGEN_HOOK_SCRIPT_MARKER` regex (`/dist\/(host\/codex-adapter|hooks
|
|
249
|
+
\/[a-z][a-z0-9-]+)\.js/`) fallback. 사용자 보고된 "Codex hooks 죽음" 의
|
|
250
|
+
더 깊은 원인이었음 — PermissionRequest skip 갭 (#9) 외에 path mismatch 도
|
|
251
|
+
동시 작용하던 것
|
|
252
|
+
- **#16 — Codex 0.130.0 hooks dispatch 회귀** (별도 task)
|
|
253
|
+
- Bisect: 0.128.0 dispatch 정상 (9 entries), 0.130.0 silent fail
|
|
254
|
+
(~/.forgen/state/ 자체 미생성)
|
|
255
|
+
- forgen 측 코드 동일 — codex-cli 0.130.0 의 hooks API/schema 변경 의심
|
|
256
|
+
- 임시 권장: codex 0.128.x 사용. 0.4.7 에서 0.130.0 호환 조사
|
|
257
|
+
|
|
258
|
+
### Known first-run UX gap (verified, not a code bug)
|
|
259
|
+
|
|
260
|
+
- `npm i -g forgen` 후 **한 번도** `fgx` 또는 `forgen install <claude|codex>` 을
|
|
261
|
+
거치지 않고 `claude` / `codex` 를 직접 호출 → forgen hooks 비활성 (settings.json
|
|
262
|
+
/ hooks.json 미등록).
|
|
263
|
+
- 첫 `fgx --claude` / `fgx --codex` 한 번 (또는 명시 install) 이면 settings.json /
|
|
264
|
+
hooks.json 이 영구 저장되어 이후 직접 호출도 hooks 발화 (clean container e2e
|
|
265
|
+
Scenario A/B/C 로 검증). v0.4.6 의 ensureCodexHooksFresh 가 fgx 진입을 더
|
|
266
|
+
부드럽게 만듦.
|
|
267
|
+
- README / onboarding 에 "first command: `fgx`" 명시 권장. 자동화 fix (예: npm
|
|
268
|
+
postinstall 에서 install both) 는 사용자 권한 가정 위반이라 0.4.7 검토.
|
|
269
|
+
|
|
270
|
+
### 미검증 항목 (실 트리거 자연 발생 대기)
|
|
271
|
+
|
|
272
|
+
- **Rate-limit auto-resume**: synthetic Stop event 로 marker 작성 검증.
|
|
273
|
+
실 limit hit 은 자연 발생 시 fix-forward 정책 (rate-limit-misses.jsonl 누적
|
|
274
|
+
→ patch hotfix) 으로 보강.
|
|
275
|
+
- **Notification 발송**: rate-limit 도달 의존. notify 모듈 syntax 검증 완료.
|
|
276
|
+
- **Claude full chain (PostToolUse 등)**: macOS Keychain 토큰 container 전달
|
|
277
|
+
불가 — `claude /login` 별도 인증 필요한 환경 한계.
|
|
278
|
+
|
|
279
|
+
|
|
10
280
|
### Fixed — forgen-eval testbed 측정 결함 (ADR-007)
|
|
11
281
|
|
|
12
282
|
`forgen-eval` ψ-stat 측정의 두 구조적 결함 식별 + 수정. 본 fix 이전 모든
|
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 후 실행하세요.');
|
|
@@ -9,4 +9,16 @@
|
|
|
9
9
|
* 호출: session-recovery hook 또는 spawn.ts에서 detached spawn
|
|
10
10
|
* 인자: [cwd] [transcriptPath] [sessionId]
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
import { type ExecFileOptions } from 'node:child_process';
|
|
13
|
+
/**
|
|
14
|
+
* 0.4.6 perf #12 — async 변형. 3 LLM 호출 (solution / user-pattern / learning) 을
|
|
15
|
+
* Promise.allSettled 로 병렬 실행하여 wall-clock 을 sum → max 로 단축 (~3x).
|
|
16
|
+
*
|
|
17
|
+
* 0.4.6 에서는 adaptive cooldown 으로 wasted runs 차단을 우선 적용했고, 호출부
|
|
18
|
+
* full parallelization 은 0.4.7 로 분리 (refactor surface 큼 — solution call 의
|
|
19
|
+
* `--allowedTools` sandbox 와 file-write 순서 invariant 검증 필요).
|
|
20
|
+
*
|
|
21
|
+
* 본 함수는 0.4.7 작업의 foundation 으로 박제 — export 하여 unused warning 회피.
|
|
22
|
+
* 동작은 sync 버전과 동일: Claude/Codex 분기 + retry on transient.
|
|
23
|
+
*/
|
|
24
|
+
export declare function execClaudeRetryAsync(args: string[], opts: ExecFileOptions): Promise<string>;
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from 'node:fs';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
|
-
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { execFileSync, execFile } from 'node:child_process';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
15
17
|
import { createRequire } from 'node:module';
|
|
16
18
|
import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
|
|
17
19
|
import { redactSecrets } from '../hooks/secret-filter.js';
|
|
@@ -78,6 +80,58 @@ function execClaudeRetry(args, opts) {
|
|
|
78
80
|
});
|
|
79
81
|
return r.message;
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 0.4.6 perf #12 — async 변형. 3 LLM 호출 (solution / user-pattern / learning) 을
|
|
85
|
+
* Promise.allSettled 로 병렬 실행하여 wall-clock 을 sum → max 로 단축 (~3x).
|
|
86
|
+
*
|
|
87
|
+
* 0.4.6 에서는 adaptive cooldown 으로 wasted runs 차단을 우선 적용했고, 호출부
|
|
88
|
+
* full parallelization 은 0.4.7 로 분리 (refactor surface 큼 — solution call 의
|
|
89
|
+
* `--allowedTools` sandbox 와 file-write 순서 invariant 검증 필요).
|
|
90
|
+
*
|
|
91
|
+
* 본 함수는 0.4.7 작업의 foundation 으로 박제 — export 하여 unused warning 회피.
|
|
92
|
+
* 동작은 sync 버전과 동일: Claude/Codex 분기 + retry on transient.
|
|
93
|
+
*/
|
|
94
|
+
export async function execClaudeRetryAsync(args, opts) {
|
|
95
|
+
const mod = createRequire(import.meta.url)('../host/exec-host.js');
|
|
96
|
+
const profileMod = createRequire(import.meta.url)('../store/profile-store.js');
|
|
97
|
+
const resolved = profileMod.resolveDefaultHost();
|
|
98
|
+
const host = resolved === 'codex' ? 'codex' : 'claude';
|
|
99
|
+
if (host === 'claude') {
|
|
100
|
+
const TRANSIENT = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE/;
|
|
101
|
+
const MAX_ATTEMPTS = 2;
|
|
102
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
103
|
+
try {
|
|
104
|
+
const { stdout } = await execFileAsync('claude', args, opts);
|
|
105
|
+
return typeof stdout === 'string' ? stdout : stdout.toString();
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
109
|
+
const match = msg.match(TRANSIENT);
|
|
110
|
+
if (attempt < MAX_ATTEMPTS && match) {
|
|
111
|
+
process.stderr.write(`[forgen-auto-compound] ${match[0]} on attempt ${attempt}/${MAX_ATTEMPTS}, retrying in 3s (auto-recovery)...\n`);
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw new Error('unreachable');
|
|
119
|
+
}
|
|
120
|
+
// codex 분기 (동기 execHost 라 그냥 호출 — 이미 빠름)
|
|
121
|
+
const pIdx = args.indexOf('-p');
|
|
122
|
+
if (pIdx === -1 || !args[pIdx + 1]) {
|
|
123
|
+
throw new Error('execClaudeRetryAsync: codex host requires -p prompt argument');
|
|
124
|
+
}
|
|
125
|
+
const prompt = args[pIdx + 1];
|
|
126
|
+
const modelIdx = args.indexOf('--model');
|
|
127
|
+
const model = modelIdx !== -1 ? args[modelIdx + 1] : undefined;
|
|
128
|
+
const r = mod.execHost({
|
|
129
|
+
prompt, model, host: 'codex',
|
|
130
|
+
timeout: typeof opts.timeout === 'number' ? opts.timeout : 60000,
|
|
131
|
+
cwd: typeof opts.cwd === 'string' ? opts.cwd : undefined,
|
|
132
|
+
});
|
|
133
|
+
return r.message;
|
|
134
|
+
}
|
|
81
135
|
const [, , cwd, transcriptPath, sessionId] = process.argv;
|
|
82
136
|
if (!cwd || !transcriptPath || !sessionId) {
|
|
83
137
|
process.exit(1);
|
|
@@ -169,6 +223,12 @@ function extractText(c) {
|
|
|
169
223
|
return c.filter((x) => x?.type === 'text').map((x) => x.text ?? '').join('\n');
|
|
170
224
|
return '';
|
|
171
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* 0.4.6 — claude/codex JSONL 양 schema 호환.
|
|
228
|
+
*
|
|
229
|
+
* Claude: {type: 'user'|'assistant', content: ...}
|
|
230
|
+
* Codex: {type: 'response_item', payload: {type: 'message', role: 'user'|'assistant', content: [{type: 'input_text', text: ...}]}}
|
|
231
|
+
*/
|
|
172
232
|
function extractSummary(filePath, maxChars = 8000) {
|
|
173
233
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
174
234
|
const lines = content.split('\n').filter(Boolean);
|
|
@@ -177,6 +237,7 @@ function extractSummary(filePath, maxChars = 8000) {
|
|
|
177
237
|
for (const line of lines) {
|
|
178
238
|
try {
|
|
179
239
|
const entry = JSON.parse(line);
|
|
240
|
+
// Claude schema
|
|
180
241
|
if (entry.type === 'user' || entry.type === 'queue-operation') {
|
|
181
242
|
const text = extractText(entry.content);
|
|
182
243
|
if (text) {
|
|
@@ -191,6 +252,21 @@ function extractSummary(filePath, maxChars = 8000) {
|
|
|
191
252
|
totalChars += text.length;
|
|
192
253
|
}
|
|
193
254
|
}
|
|
255
|
+
// Codex schema
|
|
256
|
+
else if (entry.type === 'response_item' && entry.payload?.role === 'user') {
|
|
257
|
+
const text = extractCodexText(entry.payload.content);
|
|
258
|
+
if (text) {
|
|
259
|
+
messages.push(`[User] ${text.slice(0, 500)}`);
|
|
260
|
+
totalChars += text.length;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (entry.type === 'response_item' && entry.payload?.role === 'assistant') {
|
|
264
|
+
const text = extractCodexText(entry.payload.content);
|
|
265
|
+
if (text) {
|
|
266
|
+
messages.push(`[Assistant] ${text.slice(0, 500)}`);
|
|
267
|
+
totalChars += text.length;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
194
270
|
}
|
|
195
271
|
catch { /* skip */ }
|
|
196
272
|
if (totalChars > maxChars)
|
|
@@ -198,6 +274,18 @@ function extractSummary(filePath, maxChars = 8000) {
|
|
|
198
274
|
}
|
|
199
275
|
return messages.join('\n\n');
|
|
200
276
|
}
|
|
277
|
+
/** Codex content array → flat string. content: [{type: 'input_text', text: ...}] */
|
|
278
|
+
function extractCodexText(content) {
|
|
279
|
+
if (!Array.isArray(content))
|
|
280
|
+
return '';
|
|
281
|
+
const parts = [];
|
|
282
|
+
for (const item of content) {
|
|
283
|
+
if (item && typeof item === 'object' && 'text' in item && typeof item.text === 'string') {
|
|
284
|
+
parts.push(item.text);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return parts.join('\n').trim();
|
|
288
|
+
}
|
|
201
289
|
/**
|
|
202
290
|
* 기존 behavior 파일에 유사 패턴이 있으면 observedCount를 +1 증가.
|
|
203
291
|
* 유사도는 같은 kind + 내용 키워드 50%+ 겹침으로 판단.
|
|
@@ -242,6 +330,8 @@ function mergeOrCreateBehavior(dir, newContent, kind, today) {
|
|
|
242
330
|
}
|
|
243
331
|
return false;
|
|
244
332
|
}
|
|
333
|
+
// 0.4.6 perf #12 — adaptive cooldown 시그널 (declared early for hoisting).
|
|
334
|
+
let userPatternFound = false;
|
|
245
335
|
try {
|
|
246
336
|
const rawSummary = extractSummary(transcriptPath);
|
|
247
337
|
if (rawSummary.length < 200)
|
|
@@ -375,6 +465,7 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
375
465
|
process.stderr.write(`[forgen-auto-compound] behavior: injection detected in LLM output, skipping write\n`);
|
|
376
466
|
}
|
|
377
467
|
if (userResult && !isInjection && !userResult.includes('관찰된 패턴 없음') && userResult.trim().length > 10) {
|
|
468
|
+
userPatternFound = true; // 0.4.6 perf #12 — adaptive cooldown 시그널
|
|
378
469
|
fs.mkdirSync(BEHAVIOR_DIR, { recursive: true });
|
|
379
470
|
const today = new Date().toISOString().split('T')[0];
|
|
380
471
|
const trimmed = userResult.trim();
|
|
@@ -625,6 +716,7 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
625
716
|
completedAt: new Date().toISOString(),
|
|
626
717
|
extractedSolutions: extractedSolutionsCount,
|
|
627
718
|
promotedRules: promotedCount,
|
|
719
|
+
userPatternFound, // 0.4.6 perf #12 — adaptive cooldown 시그널
|
|
628
720
|
noticeShown: false,
|
|
629
721
|
}));
|
|
630
722
|
}
|
package/dist/core/doctor.js
CHANGED
|
@@ -418,6 +418,15 @@ export async function runDoctor(opts = {}) {
|
|
|
418
418
|
const report = pruneState({ dryRun: false });
|
|
419
419
|
const mb = (report.bytesFreed / 1024 / 1024).toFixed(2);
|
|
420
420
|
console.log(` → Pruned ${report.pruned}/${report.scanned} files (${mb} MB freed, >${report.retentionDays}d old)`);
|
|
421
|
+
// 0.4.6 #14 — append-only jsonl 회전 (10MB cap)
|
|
422
|
+
try {
|
|
423
|
+
const { rotateAppendOnlyLogs } = await import('./state-gc.js');
|
|
424
|
+
const rot = rotateAppendOnlyLogs();
|
|
425
|
+
if (rot.rotated > 0) {
|
|
426
|
+
console.log(` → Rotated ${rot.rotated}/${rot.scanned} append-only log(s): ${rot.sample.join(', ')}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch { /* fail-open */ }
|
|
421
430
|
// ADR-002 T4 — 90d 미주입 rule retire. pruneState 와 함께 "하루 한번 정돈" 의미 공유.
|
|
422
431
|
try {
|
|
423
432
|
const { runDailyT4Decay } = await import('./state-gc.js');
|
package/dist/core/harness.js
CHANGED
|
@@ -22,6 +22,52 @@ import { bootstrapV1Session, ensureV1Directories } from './v1-bootstrap.js';
|
|
|
22
22
|
import { injectSettings } from './settings-injector.js';
|
|
23
23
|
import { installAgents, installSlashCommands } from './installer.js';
|
|
24
24
|
const log = createLogger('harness');
|
|
25
|
+
/**
|
|
26
|
+
* 0.4.6 — Codex hooks fast staleness check + auto-reconcile.
|
|
27
|
+
*
|
|
28
|
+
* 매 codex 진입 시 hooks.json 의 forgen entry 가 현재 pkgRoot 와 일치하는지
|
|
29
|
+
* 검사. 불일치 또는 부재 시 planCodexInstall 을 silently 재실행.
|
|
30
|
+
*
|
|
31
|
+
* Staleness 판정: hooks.json 안의 첫 codex-adapter.js 경로가 현재 pkgRoot 의
|
|
32
|
+
* dist/host/codex-adapter.js 와 일치 여부.
|
|
33
|
+
*
|
|
34
|
+
* Performance: hot-path 회피를 위해 lazy import + 단순 string match. 정상
|
|
35
|
+
* 케이스에서 1ms 이내. 불일치 시 planCodexInstall (~50ms).
|
|
36
|
+
*/
|
|
37
|
+
async function ensureCodexHooksFresh(pkgRoot) {
|
|
38
|
+
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
|
|
39
|
+
const hooksPath = path.join(codexHome, 'hooks.json');
|
|
40
|
+
const expectedAdapterPath = path.join(pkgRoot, 'dist', 'host', 'codex-adapter.js');
|
|
41
|
+
let needsReinstall = false;
|
|
42
|
+
if (!fs.existsSync(hooksPath)) {
|
|
43
|
+
needsReinstall = true;
|
|
44
|
+
log.debug('codex hooks.json 부재 — install 필요');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(hooksPath, 'utf-8');
|
|
49
|
+
// 가장 간단한 staleness signal: 현재 pkgRoot 의 adapter 경로가 hooks.json 에 나타나는가?
|
|
50
|
+
if (!raw.includes(expectedAdapterPath)) {
|
|
51
|
+
needsReinstall = true;
|
|
52
|
+
log.debug(`codex hooks.json stale (expected ${expectedAdapterPath} 미발견)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
needsReinstall = true;
|
|
57
|
+
log.debug('codex hooks.json 읽기 실패 — reinstall', e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!needsReinstall)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
const { planCodexInstall } = await import('../host/install-codex.js');
|
|
64
|
+
planCodexInstall({ pkgRoot, registerMcp: true });
|
|
65
|
+
log.debug('codex hooks 자동 reconcile 완료');
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
log.debug('codex hooks reconcile 실패', e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
25
71
|
/** forgen 패키지 루트 */
|
|
26
72
|
function getPackageRoot() {
|
|
27
73
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
@@ -356,8 +402,21 @@ export async function prepareHarness(cwd, options = {}) {
|
|
|
356
402
|
// 7. 슬래시 명령 설치
|
|
357
403
|
installSlashCommands(cwd, pkgRoot);
|
|
358
404
|
}
|
|
405
|
+
else if (runtime === 'codex') {
|
|
406
|
+
// 0.4.6 — Codex hooks 자동 reconcile-on-launch.
|
|
407
|
+
// 매 fgx --codex / forgen --runtime codex 호출 시 hooks.json 의 forgen entry 가
|
|
408
|
+
// 현재 pkgRoot 와 일치하는지 fast check. 불일치 (다른 머신/버전 install,
|
|
409
|
+
// pkg path 변경, 신규 설치) → silent reinstall.
|
|
410
|
+
// 사용자가 `forgen install codex` 를 명시 호출 안 해도 매번 정합성 보장.
|
|
411
|
+
try {
|
|
412
|
+
await ensureCodexHooksFresh(pkgRoot);
|
|
413
|
+
}
|
|
414
|
+
catch (e) {
|
|
415
|
+
log.debug('Codex hooks reconcile 실패 (fail-open)', e);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
359
418
|
else {
|
|
360
|
-
log.debug(`prepareHarness: runtime=${runtime} —
|
|
419
|
+
log.debug(`prepareHarness: runtime=${runtime} — artifact prep skipped`);
|
|
361
420
|
}
|
|
362
421
|
// 8. tmux 바인딩 등록
|
|
363
422
|
if (inTmux) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Desktop / webhook notification helper (ADR-008 §"테마 A 알림").
|
|
3
|
+
*
|
|
4
|
+
* 0.4.6 신설 — rate-limit auto-resume 이 sleep 끝나고 재기동될 때 사용자에게
|
|
5
|
+
* 알림을 보냄 (노트북 닫고 잘 때 끝났는지 알 수 있어야 함).
|
|
6
|
+
*
|
|
7
|
+
* 정책:
|
|
8
|
+
* - macOS: osascript 'display notification'
|
|
9
|
+
* - Linux: notify-send (있으면)
|
|
10
|
+
* - Windows: 생략 (PowerShell BurntToast 의존성 회피)
|
|
11
|
+
* - webhook: ~/.forgen/config.json 의 notifyWebhookUrl 설정 시 POST
|
|
12
|
+
* - fail-open: 모든 실패는 silent log
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 통합 알림 진입점. desktop + webhook 둘 다 시도.
|
|
16
|
+
* 모든 실패는 silent — 호출 측 차단 안 함.
|
|
17
|
+
*/
|
|
18
|
+
export declare function sendNotification(title: string, body: string): void;
|