@wooojin/forgen 0.4.11 → 0.4.13
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 +31 -0
- package/dist/core/spawn.d.ts +13 -0
- package/dist/core/spawn.js +36 -8
- package/dist/fgx.js +12 -0
- package/dist/hooks/secret-filter.d.ts +1 -0
- package/dist/hooks/secret-filter.js +17 -7
- package/package.json +1 -1
- package/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.13] — 2026-06-02 — Hotfix: fgx 세션 종료 시 터미널 물림 방지
|
|
11
|
+
|
|
12
|
+
핫픽스 한 건.
|
|
13
|
+
|
|
14
|
+
- **fix(fgx)**: `fgx` 가 대화형 세션 성공 경로에서 `process.exit` 를 호출하지 않고 Node
|
|
15
|
+
이벤트 루프 자연 배수에 의존해 종료해 왔다. `spawnClaude` 는 세션 종료 시
|
|
16
|
+
`auto-compound-runner` 를 detached + unref 로 띄우고 즉시 resolve 하므로(0.4.12 에서
|
|
17
|
+
비차단 전환) 현재는 미해제 핸들이 없어 정상 종료한다. 그러나 향후 post-session 경로
|
|
18
|
+
(`indexTranscriptToFTS`·rate-limit scan 등)에 닫히지 않은 핸들(SQLite 커넥션·타이머·소켓)이
|
|
19
|
+
하나라도 생기면 fgx 가 종료하지 못하고 셸 프롬프트가 돌아오지 않는다(= 터미널 물림).
|
|
20
|
+
`runClaudeLauncher()` 직후 명시적 `process.exit(0)` 추가로 이 종류의 회귀를 원천 차단.
|
|
21
|
+
cli.js 위임 경로(`watch`/`dashboard`/`workflows` 등 long-running)는 자체 exit 를 책임지므로
|
|
22
|
+
손대지 않았다. detached 자식은 unref 되어 독립 세션에서 실행되므로 `process.exit(0)` 후에도
|
|
23
|
+
생존(PID 1 로 reparent 확인) — 백그라운드 compound 는 영향받지 않는다.
|
|
24
|
+
|
|
25
|
+
## [0.4.12] — 2026-06-01 — Hotfix: 훅 stdout 누출 + 세션 종료 블로킹
|
|
26
|
+
|
|
27
|
+
핫픽스 두 건.
|
|
28
|
+
|
|
29
|
+
- **fix(hooks)**: `secret-filter` 에 ESM main-guard 누락 → `context-guard` 가 `redactSecrets`
|
|
30
|
+
를 import 할 때 `secret-filter.main()` 이 import 부작용으로 실행되어 유령 `{"continue":true}`
|
|
31
|
+
를 1줄 추가 emit. 그 결과 context-guard 의 stdout 이 JSON 2줄이 되어 Claude Code 파싱
|
|
32
|
+
실패 → raw `{"continue":true}` 가 매 프롬프트/세션 종료마다 사용자 터미널에 노출되었다.
|
|
33
|
+
표준 main-guard 추가로 수정. (등록 훅 11개 중 context-guard 만 영향)
|
|
34
|
+
- **perf(compound)**: 세션 종료 시 `runAutoCompound` 가 execFileSync 로 동기 실행되어 최대
|
|
35
|
+
~210초(haiku LLM 3회 순차) 블록되었다. 같은 작업을 Stop 훅·session-recovery 가 이미
|
|
36
|
+
detached 로 spawn 하므로(dedup 마커 공유), 동기 경로를 detached + unref 로 전환해 세션
|
|
37
|
+
종료를 막지 않게 했다. 결과는 다음 세션 시작 시 surface.
|
|
38
|
+
- **test**: `hook-single-line-output` (훅당 stdout 1줄 + secret-filter import 무부작용),
|
|
39
|
+
`auto-compound-detached` (detached/unref/dedup 분기) 회귀 테스트 추가.
|
|
40
|
+
|
|
10
41
|
## [0.4.11] — 2026-05-29 — Opus 4.8 + Dynamic Workflows 대응
|
|
11
42
|
|
|
12
43
|
테마: Claude Opus 4.8(2026-05-28 GA)의 **dynamic workflows**(최대 1,000 서브에이전트,
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { V1HarnessContext } from './harness.js';
|
|
2
2
|
import type { RuntimeHost } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 세션 종료 후 자동 compound 추출을 백그라운드(detached)로 시작.
|
|
5
|
+
*
|
|
6
|
+
* 과거에는 execFileSync 로 동기 실행하여 세션 종료가 최대 ~210초(haiku LLM 3회
|
|
7
|
+
* 순차: 솔루션 추출 90s + behavior 60s + 세션 학습 60s) 동안 블록되었다. 같은
|
|
8
|
+
* 작업을 context-guard Stop 훅(maybeSpawnAutoCompound)과 session-recovery 가 이미
|
|
9
|
+
* detached 로 spawn 하므로, 여기서도 detached + unref 로 전환해 세션 종료를 막지
|
|
10
|
+
* 않는다. 결과(추출 솔루션/패턴)는 다음 세션 시작 시 session-recovery 가 surface 한다.
|
|
11
|
+
*
|
|
12
|
+
* dedup: last-auto-compound.json 을 Stop 훅과 공유 — 같은 세션의 최근 run 이 있으면
|
|
13
|
+
* skip 하여 detached spawn 의 double-run 을 방지한다.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runAutoCompound(cwd: string, transcriptPath: string, sessionId: string): void;
|
|
3
16
|
/**
|
|
4
17
|
* Plan B-1: 세션 transcript 사후 스캔으로 rate-limit 감지.
|
|
5
18
|
*
|
package/dist/core/spawn.js
CHANGED
|
@@ -223,19 +223,47 @@ async function scanCommitDiffForActedOn(sessionId, cwd, sessionStartTime) {
|
|
|
223
223
|
* 세션 종료 후 자동 compound 추출 + USER.md 업데이트.
|
|
224
224
|
* auto-compound-runner.ts를 동기 실행하여 솔루션 추출 + 사용자 패턴 관찰.
|
|
225
225
|
*/
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
// Stop 훅(maybeSpawnAutoCompound)/session-recovery 와 공유하는 dedup cooldown.
|
|
227
|
+
const AUTO_COMPOUND_DEDUP_COOLDOWN_MS = 5 * 60 * 1000; // 5min
|
|
228
|
+
/**
|
|
229
|
+
* 세션 종료 후 자동 compound 추출을 백그라운드(detached)로 시작.
|
|
230
|
+
*
|
|
231
|
+
* 과거에는 execFileSync 로 동기 실행하여 세션 종료가 최대 ~210초(haiku LLM 3회
|
|
232
|
+
* 순차: 솔루션 추출 90s + behavior 60s + 세션 학습 60s) 동안 블록되었다. 같은
|
|
233
|
+
* 작업을 context-guard Stop 훅(maybeSpawnAutoCompound)과 session-recovery 가 이미
|
|
234
|
+
* detached 로 spawn 하므로, 여기서도 detached + unref 로 전환해 세션 종료를 막지
|
|
235
|
+
* 않는다. 결과(추출 솔루션/패턴)는 다음 세션 시작 시 session-recovery 가 surface 한다.
|
|
236
|
+
*
|
|
237
|
+
* dedup: last-auto-compound.json 을 Stop 훅과 공유 — 같은 세션의 최근 run 이 있으면
|
|
238
|
+
* skip 하여 detached spawn 의 double-run 을 방지한다.
|
|
239
|
+
*/
|
|
240
|
+
export function runAutoCompound(cwd, transcriptPath, sessionId) {
|
|
241
|
+
const markerPath = path.join(STATE_DIR, 'last-auto-compound.json');
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(fs.readFileSync(markerPath, 'utf-8'));
|
|
244
|
+
if (parsed.sessionId === sessionId) {
|
|
245
|
+
const last = parsed.completedAt ? Date.parse(parsed.completedAt) : 0;
|
|
246
|
+
if (Number.isFinite(last) && Date.now() - last < AUTO_COMPOUND_DEDUP_COOLDOWN_MS) {
|
|
247
|
+
log.debug('auto-compound skip: 최근 동일 세션 run 존재 (dedup)');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// 마커 없음(최초)/손상 — 진행 (fail-open).
|
|
254
|
+
}
|
|
228
255
|
const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'auto-compound-runner.js');
|
|
229
256
|
try {
|
|
230
|
-
|
|
257
|
+
const child = spawn('node', [runnerPath, cwd, transcriptPath, sessionId], {
|
|
231
258
|
cwd,
|
|
232
|
-
|
|
233
|
-
stdio:
|
|
259
|
+
detached: true,
|
|
260
|
+
stdio: 'ignore',
|
|
234
261
|
});
|
|
235
|
-
|
|
262
|
+
child.unref();
|
|
263
|
+
console.log('\n[forgen] 세션 분석을 백그라운드로 시작했습니다 (자동 compound) — 결과는 다음 세션 시작 시 반영됩니다.\n');
|
|
236
264
|
}
|
|
237
265
|
catch (e) {
|
|
238
|
-
log.debug('auto-compound 실패', e);
|
|
266
|
+
log.debug('auto-compound 시작 실패', e);
|
|
239
267
|
}
|
|
240
268
|
}
|
|
241
269
|
/**
|
|
@@ -415,7 +443,7 @@ export async function spawnClaude(args, context, runtime = 'claude') {
|
|
|
415
443
|
// 4. 자동 compound (10+ user 메시지인 경우만) — 양 runtime 호환
|
|
416
444
|
const userMsgCount = await countUserMessages(transcript);
|
|
417
445
|
if (userMsgCount >= 10) {
|
|
418
|
-
|
|
446
|
+
runAutoCompound(context.cwd, transcript, sessionId);
|
|
419
447
|
}
|
|
420
448
|
else {
|
|
421
449
|
console.log(`[forgen] 세션이 짧아 auto-compound 생략 (${userMsgCount} messages)`);
|
package/dist/fgx.js
CHANGED
|
@@ -80,10 +80,22 @@ async function runClaudeLauncher() {
|
|
|
80
80
|
async function main() {
|
|
81
81
|
const sub = findFirstSubcommand(args);
|
|
82
82
|
if (sub !== null) {
|
|
83
|
+
// cli.js 위임 경로는 건드리지 않는다. watch/dashboard/workflows 등 long-running
|
|
84
|
+
// 서브커맨드가 여기서 강제 종료되면 잘린다. cli.js 가 자체 process.exit 를 책임진다.
|
|
83
85
|
await routeToCli();
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
88
|
await runClaudeLauncher();
|
|
89
|
+
// 대화형 세션 종료 후 fgx 프로세스 종료를 명시적으로 보장한다.
|
|
90
|
+
//
|
|
91
|
+
// WHY: spawnClaude 는 세션 종료 시 auto-compound-runner 를 detached + unref 로 띄우고
|
|
92
|
+
// 즉시 resolve 한다(설계상 비차단). 그런데 fgx 는 성공 경로에서 process.exit 를 호출하지
|
|
93
|
+
// 않고 Node 이벤트 루프 자연 배수에 의존해 왔다. 지금은 미해제 핸들이 없어 정상 종료하지만,
|
|
94
|
+
// 향후 post-session 경로에 닫히지 않은 핸들(SQLite 커넥션, 타이머, 소켓 등)이 하나라도
|
|
95
|
+
// 생기면 fgx 가 종료하지 못하고 셸 프롬프트가 돌아오지 않는다(= 터미널 물림). 명시 종료로
|
|
96
|
+
// 이 종류의 회귀를 원천 차단한다. detached 자식은 unref 되어 독립 실행되므로 백그라운드
|
|
97
|
+
// compound 는 영향받지 않고 계속된다.
|
|
98
|
+
process.exit(0);
|
|
87
99
|
}
|
|
88
100
|
main().catch((err) => {
|
|
89
101
|
console.error('[forgen] Error:', err instanceof Error ? err.message : err);
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* 도구 실행 결과에서 API 키, 토큰, 비밀번호 등 민감 정보 노출을 감지합니다.
|
|
6
6
|
* 차단하지 않고 경고 메시지만 출력합니다.
|
|
7
7
|
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
8
11
|
import { HookError } from '../core/errors.js';
|
|
9
12
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
10
13
|
import { isHookEnabled } from './hook-config.js';
|
|
@@ -53,7 +56,7 @@ export function detectSecrets(text) {
|
|
|
53
56
|
}
|
|
54
57
|
return found;
|
|
55
58
|
}
|
|
56
|
-
async function main() {
|
|
59
|
+
export async function main() {
|
|
57
60
|
const data = await readStdinJSON();
|
|
58
61
|
if (!isHookEnabled('secret-filter')) {
|
|
59
62
|
console.log(approve());
|
|
@@ -82,10 +85,17 @@ async function main() {
|
|
|
82
85
|
}
|
|
83
86
|
console.log(approve());
|
|
84
87
|
}
|
|
85
|
-
main()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
// ESM main guard: import 시 main() 실행 방지 (context-guard 와 동일 패턴).
|
|
89
|
+
// secret-filter 는 redactSecrets 등을 다른 hook (context-guard) 이 import 하므로,
|
|
90
|
+
// guard 없이 top-level main() 을 호출하면 import 부작용으로 main() 이 실행되어
|
|
91
|
+
// stdout 에 유령 {"continue":true} 가 1줄 추가된다. 그 결과 import 한 hook 의
|
|
92
|
+
// stdout 이 JSON 2줄이 되어 Claude Code 파싱 실패 → raw JSON 이 터미널에 노출됨.
|
|
93
|
+
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
94
|
+
main().catch((e) => {
|
|
95
|
+
const hookErr = new HookError(e instanceof Error ? e.message : String(e), {
|
|
96
|
+
hookName: 'secret-filter', eventType: 'PostToolUse', cause: e,
|
|
97
|
+
});
|
|
98
|
+
process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
|
|
99
|
+
console.log(failOpenWithTracking('secret-filter', e));
|
|
88
100
|
});
|
|
89
|
-
|
|
90
|
-
console.log(failOpenWithTracking('secret-filter', e));
|
|
91
|
-
});
|
|
101
|
+
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED