@wooojin/forgen 0.4.11 → 0.4.12
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 +16 -0
- package/dist/core/spawn.d.ts +13 -0
- package/dist/core/spawn.js +36 -8
- 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,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.12] — 2026-06-01 — Hotfix: 훅 stdout 누출 + 세션 종료 블로킹
|
|
11
|
+
|
|
12
|
+
핫픽스 두 건.
|
|
13
|
+
|
|
14
|
+
- **fix(hooks)**: `secret-filter` 에 ESM main-guard 누락 → `context-guard` 가 `redactSecrets`
|
|
15
|
+
를 import 할 때 `secret-filter.main()` 이 import 부작용으로 실행되어 유령 `{"continue":true}`
|
|
16
|
+
를 1줄 추가 emit. 그 결과 context-guard 의 stdout 이 JSON 2줄이 되어 Claude Code 파싱
|
|
17
|
+
실패 → raw `{"continue":true}` 가 매 프롬프트/세션 종료마다 사용자 터미널에 노출되었다.
|
|
18
|
+
표준 main-guard 추가로 수정. (등록 훅 11개 중 context-guard 만 영향)
|
|
19
|
+
- **perf(compound)**: 세션 종료 시 `runAutoCompound` 가 execFileSync 로 동기 실행되어 최대
|
|
20
|
+
~210초(haiku LLM 3회 순차) 블록되었다. 같은 작업을 Stop 훅·session-recovery 가 이미
|
|
21
|
+
detached 로 spawn 하므로(dedup 마커 공유), 동기 경로를 detached + unref 로 전환해 세션
|
|
22
|
+
종료를 막지 않게 했다. 결과는 다음 세션 시작 시 surface.
|
|
23
|
+
- **test**: `hook-single-line-output` (훅당 stdout 1줄 + secret-filter import 무부작용),
|
|
24
|
+
`auto-compound-detached` (detached/unref/dedup 분기) 회귀 테스트 추가.
|
|
25
|
+
|
|
10
26
|
## [0.4.11] — 2026-05-29 — Opus 4.8 + Dynamic Workflows 대응
|
|
11
27
|
|
|
12
28
|
테마: 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)`);
|
|
@@ -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