@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.11",
4
+ "version": "0.4.12",
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,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 서브에이전트,
@@ -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
  *
@@ -223,19 +223,47 @@ async function scanCommitDiffForActedOn(sessionId, cwd, sessionStartTime) {
223
223
  * 세션 종료 후 자동 compound 추출 + USER.md 업데이트.
224
224
  * auto-compound-runner.ts를 동기 실행하여 솔루션 추출 + 사용자 패턴 관찰.
225
225
  */
226
- async function runAutoCompound(cwd, transcriptPath, sessionId) {
227
- console.log('\n[forgen] 세션 분석 중... (자동 compound)');
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
- execFileSync('node', [runnerPath, cwd, transcriptPath, sessionId], {
257
+ const child = spawn('node', [runnerPath, cwd, transcriptPath, sessionId], {
231
258
  cwd,
232
- timeout: 120_000,
233
- stdio: ['pipe', 'inherit', 'inherit'],
259
+ detached: true,
260
+ stdio: 'ignore',
234
261
  });
235
- console.log('[forgen] 자동 compound 완료\n');
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
- await runAutoCompound(context.cwd, transcript, sessionId);
446
+ runAutoCompound(context.cwd, transcript, sessionId);
419
447
  }
420
448
  else {
421
449
  console.log(`[forgen] 세션이 짧아 auto-compound 생략 (${userMsgCount} messages)`);
@@ -22,3 +22,4 @@ export declare function redactSecrets(text: string): {
22
22
  };
23
23
  /** 텍스트에서 민감 정보 패턴 감지 (순수 함수) */
24
24
  export declare function detectSecrets(text: string): SecretPattern[];
25
+ export declare function main(): Promise<void>;
@@ -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().catch((e) => {
86
- const hookErr = new HookError(e instanceof Error ? e.message : String(e), {
87
- hookName: 'secret-filter', eventType: 'PostToolUse', cause: e,
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
- process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
90
- console.log(failOpenWithTracking('secret-filter', e));
91
- });
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooojin/forgen",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "preferGlobal": true,
5
5
  "main": "dist/lib.js",
6
6
  "types": "./dist/lib.d.ts",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.11",
4
+ "version": "0.4.12",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",