@wooojin/forgen 0.4.5 → 0.4.6

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.
@@ -21,8 +21,111 @@ import { approve, approveWithContext, approveWithWarning, failOpenWithTracking }
21
21
  import { HANDOFFS_DIR, STATE_DIR } from '../core/paths.js';
22
22
  import { recordHookTiming } from './shared/hook-timing.js';
23
23
  import { sanitizeId } from './shared/sanitize-id.js';
24
+ import { redactSecrets } from './secret-filter.js';
24
25
  const log = createLogger('context-guard');
25
26
  const CONTEXT_STATE_PATH = path.join(STATE_DIR, 'context-guard.json');
27
+ const PROMPT_HISTORY_PATH = path.join(STATE_DIR, 'prompt-history.jsonl');
28
+ const PROMPT_HISTORY_TRUNCATE = 1024; // ADR-008: 1KB cap per entry
29
+ const RATE_LIMIT_MISSES_PATH = path.join(STATE_DIR, 'rate-limit-misses.jsonl');
30
+ // ADR-008: detection regex 분리. token-limit 은 context window, rate-limit 은 API quota.
31
+ export const TOKEN_LIMIT_REGEX = /context.*limit|token.*limit|conversation.*too.*long/i;
32
+ export const RATE_LIMIT_REGEX = /rate.?limit|5.?hour.*limit|weekly.*limit|usage.*limit|quota.*exceeded/i;
33
+ /**
34
+ * Best-effort reset 시각 파서 (ADR-008 §2).
35
+ *
36
+ * 5 패턴 시도, 모두 실패 시 null. 실제 메시지 포맷은 Claude/Codex CLI 의
37
+ * 공식 contract 가 아니므로 hotfix 가능한 best-effort.
38
+ *
39
+ * @param msg stderr/error message text
40
+ * @param now epoch ms (테스트 결정성 위해 주입 가능)
41
+ * @returns ISO timestamp 또는 null
42
+ */
43
+ export function parseRateLimitResetAt(msg, now = Date.now()) {
44
+ // Pattern 4: explicit ISO timestamp — "available again at <ISO>"
45
+ const isoMatch = msg.match(/(?:available|reset|retry)\s+(?:again\s+)?(?:at|on)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)/i);
46
+ if (isoMatch) {
47
+ const ts = Date.parse(isoMatch[1]);
48
+ if (Number.isFinite(ts))
49
+ return new Date(ts).toISOString();
50
+ }
51
+ // Pattern 2: "Resets in 4h 12m" / "in 4h" / "in 12m"
52
+ const hMin = msg.match(/(?:reset|retry|try\s+again)s?\s+in\s+(?:(\d+)\s*h)?\s*(?:(\d+)\s*m(?:in)?)?/i);
53
+ if (hMin && (hMin[1] || hMin[2])) {
54
+ const hours = parseInt(hMin[1] ?? '0', 10);
55
+ const mins = parseInt(hMin[2] ?? '0', 10);
56
+ const offset = (hours * 3600 + mins * 60) * 1000;
57
+ if (offset > 0)
58
+ return new Date(now + offset).toISOString();
59
+ }
60
+ // Pattern 3: "Resets in 18000 seconds"
61
+ const secMatch = msg.match(/(?:reset|retry|try\s+again)s?\s+in\s+(\d+)\s*sec(?:ond)?s?/i);
62
+ if (secMatch) {
63
+ const sec = parseInt(secMatch[1], 10);
64
+ if (sec > 0)
65
+ return new Date(now + sec * 1000).toISOString();
66
+ }
67
+ // Pattern 1: "Resets at HH:MM(:SS)? TZ" — TZ 미지원 (UTC 가정)
68
+ const hhmm = msg.match(/(?:reset|retry|available)s?\s+at\s+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(UTC|GMT|PST|PDT|EST|EDT|KST|JST)?/i);
69
+ if (hhmm) {
70
+ const h = parseInt(hhmm[1], 10);
71
+ const m = parseInt(hhmm[2], 10);
72
+ const s = parseInt(hhmm[3] ?? '0', 10);
73
+ if (h < 24 && m < 60 && s < 60) {
74
+ const d = new Date(now);
75
+ d.setUTCHours(h, m, s, 0);
76
+ // 이미 지난 시각이면 다음 날
77
+ if (d.getTime() <= now)
78
+ d.setUTCDate(d.getUTCDate() + 1);
79
+ return d.toISOString();
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * Detector 실패 (RATE_LIMIT_REGEX 매칭 실패) raw 메시지를 누적.
86
+ * ADR-008 §5: 5건 누적 시 사용자 경고 — patch release 로 hotfix 신호.
87
+ */
88
+ function logRateLimitMiss(errorMsg) {
89
+ try {
90
+ fs.mkdirSync(STATE_DIR, { recursive: true });
91
+ fs.appendFileSync(RATE_LIMIT_MISSES_PATH, `${JSON.stringify({
92
+ timestamp: new Date().toISOString(),
93
+ message: errorMsg.slice(0, 500),
94
+ })}\n`);
95
+ }
96
+ catch (e) {
97
+ log.debug('rate-limit-miss log 실패', e);
98
+ }
99
+ }
100
+ /**
101
+ * Append-only prompt history writer (docs/codex-integration.md §"prompt-history.jsonl").
102
+ *
103
+ * compound-extractor.ts:547 의 read 코드가 0.4.5 까지 dead code 였음. 0.4.6
104
+ * 부터 UserPromptSubmit hook 에서 truncated prompt 를 append 하여 활성화.
105
+ *
106
+ * fail-open: 실패는 hook 차단하지 않음.
107
+ */
108
+ function appendPromptHistory(sessionId, prompt) {
109
+ try {
110
+ fs.mkdirSync(STATE_DIR, { recursive: true });
111
+ // ADR-008 critical: secret/PII redaction via secret-filter regex 재사용.
112
+ // password/api-key 가 평문 prompt 에 들어가면 그대로 disk 에 박제되는 위험 차단.
113
+ const safePrompt = redactSecrets(prompt).redacted;
114
+ const truncated = safePrompt.length > PROMPT_HISTORY_TRUNCATE
115
+ ? safePrompt.slice(0, PROMPT_HISTORY_TRUNCATE)
116
+ : safePrompt;
117
+ const entry = JSON.stringify({
118
+ timestamp: new Date().toISOString(),
119
+ sessionId,
120
+ promptLength: prompt.length,
121
+ prompt: truncated,
122
+ });
123
+ fs.appendFileSync(PROMPT_HISTORY_PATH, `${entry}\n`);
124
+ }
125
+ catch (e) {
126
+ log.debug('prompt-history append 실패', e);
127
+ }
128
+ }
26
129
  // 경고 임계값: 프롬프트 50회 또는 총 문자 수 200K 이상
27
130
  const PROMPT_WARNING_THRESHOLD = 50;
28
131
  const CHARS_WARNING_THRESHOLD = 200_000;
@@ -108,16 +211,20 @@ export async function main() {
108
211
  console.log(forgeLoopBlock);
109
212
  return;
110
213
  }
111
- // 에러가 포함된 경우: context limit 감지
214
+ // 에러가 포함된 경우: context-limit / rate-limit 감지 (ADR-008)
112
215
  if (input.error) {
113
216
  const errorMsg = input.error;
114
- if (/context.*limit|token.*limit|conversation.*too.*long/i.test(errorMsg)) {
217
+ // FORGEN_RUNTIME 은 buildEnv (config-injector.ts:479) 에서 spawn 시 주입.
218
+ // hook 은 claude/codex CLI 를 통해 invoke 되어 부모 env 상속.
219
+ const runtime = process.env.FORGEN_RUNTIME ?? 'claude';
220
+ if (TOKEN_LIMIT_REGEX.test(errorMsg)) {
115
221
  saveHandoff(sessionId, 'context-limit', errorMsg);
116
222
  try {
117
223
  const resumePath = path.join(STATE_DIR, 'pending-resume.json');
118
224
  fs.writeFileSync(resumePath, JSON.stringify({
119
225
  reason: 'token-limit',
120
226
  sessionId,
227
+ runtime,
121
228
  savedAt: new Date().toISOString(),
122
229
  cwd: process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd(),
123
230
  }, null, 2));
@@ -126,6 +233,30 @@ export async function main() {
126
233
  console.log(approveWithWarning(`[Forgen] Context limit reached. Current state has been saved to ~/.forgen/handoffs/.\nThe previous work will be automatically recovered in the next session.`));
127
234
  return;
128
235
  }
236
+ if (RATE_LIMIT_REGEX.test(errorMsg)) {
237
+ const resetAt = parseRateLimitResetAt(errorMsg);
238
+ saveHandoff(sessionId, 'rate-limit', errorMsg);
239
+ try {
240
+ const resumePath = path.join(STATE_DIR, 'pending-resume.json');
241
+ fs.writeFileSync(resumePath, JSON.stringify({
242
+ reason: 'rate-limit',
243
+ sessionId,
244
+ runtime,
245
+ resetAt,
246
+ savedAt: new Date().toISOString(),
247
+ cwd: process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd(),
248
+ }, null, 2));
249
+ }
250
+ catch { /* fail-open */ }
251
+ const eta = resetAt ? `at ${resetAt}` : 'with backoff schedule';
252
+ console.log(approveWithWarning(`[Forgen] Rate limit reached. Auto-resume scheduled ${eta}. State saved to ~/.forgen/handoffs/.`));
253
+ return;
254
+ }
255
+ // ADR-008 §5: detection 실패 raw 누적 → patch hotfix 신호
256
+ // (어느 limit regex 도 매칭 안 됨 = unknown error 또는 detector miss)
257
+ if (/limit|quota|throttle/i.test(errorMsg)) {
258
+ logRateLimitMiss(errorMsg);
259
+ }
129
260
  }
130
261
  // 정상 종료 시: 의미 있는 세션이었으면 compound 안내/자동 트리거
131
262
  if (input.stop_hook_type === 'user' || input.stop_hook_type === 'end_turn') {
@@ -175,6 +306,8 @@ export async function main() {
175
306
  const state = loadContextState(sessionId);
176
307
  state.promptCount++;
177
308
  state.totalChars += input.prompt.length;
309
+ // ADR-008 / docs/codex-integration.md — prompt-history writer 활성화
310
+ appendPromptHistory(sessionId, input.prompt);
178
311
  // auto-compact: 추적 문자 120K 이상이면 compact 지시 주입
179
312
  const autoCompactThreshold = typeof config?.autoCompactChars === 'number' ? config.autoCompactChars : undefined;
180
313
  if (shouldAutoCompact(state, autoCompactThreshold !== undefined ? { charsThreshold: autoCompactThreshold } : {})) {
@@ -246,7 +379,24 @@ const FORGE_LOOP_STATE_PATH = path.join(STATE_DIR, 'forge-loop.json');
246
379
  * dedup 파일은 session-recovery hook 과 공유되어 double-run 방지.
247
380
  * fire-and-forget (detached) — hook timeout 과 무관.
248
381
  */
249
- const AUTO_COMPOUND_COOLDOWN_MS = 5 * 60 * 1000; // 5 min
382
+ const AUTO_COMPOUND_COOLDOWN_MS = 5 * 60 * 1000; // 5 min (default)
383
+ const AUTO_COMPOUND_BARREN_COOLDOWN_MS = 30 * 60 * 1000; // 30 min if last run extracted nothing
384
+ /**
385
+ * 0.4.6 perf #12 — adaptive cooldown.
386
+ *
387
+ * 마지막 auto-compound run 이 0건 추출했으면 (barren), 다음 cooldown 을 5min →
388
+ * 30min 으로 확장. 같은 session 의 짧은 prompt 추가에서 동일 transcript 로 매번
389
+ * 3 LLM 호출하는 wasted run 차단. completed marker 의 extractedSolutions /
390
+ * promotedRules / userPatternFound 합산이 0 이면 barren 판정.
391
+ *
392
+ * 일반 case (추출 있음) 은 5분 cooldown 유지 — adaptive 가 sparsity 시그널을
393
+ * 강화할 뿐 정상 동작 차단 안 함.
394
+ */
395
+ function effectiveCooldownMs(parsed) {
396
+ const total = (parsed.extractedSolutions ?? 0) + (parsed.promotedRules ?? 0)
397
+ + (parsed.userPatternFound ? 1 : 0);
398
+ return total === 0 ? AUTO_COMPOUND_BARREN_COOLDOWN_MS : AUTO_COMPOUND_COOLDOWN_MS;
399
+ }
250
400
  async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
251
401
  if (!transcriptPath || promptCount < 10)
252
402
  return;
@@ -256,7 +406,8 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
256
406
  const parsed = JSON.parse(raw);
257
407
  if (parsed.sessionId === sessionId) {
258
408
  const last = parsed.completedAt ? Date.parse(parsed.completedAt) : 0;
259
- if (Number.isFinite(last) && Date.now() - last < AUTO_COMPOUND_COOLDOWN_MS)
409
+ const cooldown = effectiveCooldownMs(parsed);
410
+ if (Number.isFinite(last) && Date.now() - last < cooldown)
260
411
  return;
261
412
  }
262
413
  }
@@ -21,6 +21,7 @@ import { STATE_DIR } from '../core/paths.js';
21
21
  import { recordHookTiming } from './shared/hook-timing.js';
22
22
  import { createDriftState, evaluateDrift } from '../core/drift-score.js';
23
23
  import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
24
+ import { recordToolCall } from '../core/usage-telemetry.js';
24
25
  const RECENT_TOOL_NAMES_WINDOW = 20;
25
26
  /** Lightweight hash for content comparison (not cryptographic) */
26
27
  function simpleHash(content) {
@@ -118,6 +119,8 @@ async function main() {
118
119
  console.log(approve());
119
120
  return;
120
121
  }
122
+ // ADR-008 / 0.4.6 — usage telemetry. fail-open, hook 차단 안 함.
123
+ recordToolCall(process.env.FORGEN_RUNTIME ?? 'claude');
121
124
  const toolName = data.tool_name ?? data.toolName ?? '';
122
125
  const toolInput = data.tool_input ?? data.toolInput ?? {};
123
126
  // tool_response 는 string / object / array 모두 가능 (sub-agent 결과는 object 가 흔함).
@@ -165,6 +165,32 @@ function getActiveReminders() {
165
165
  }
166
166
  return reminders;
167
167
  }
168
+ /**
169
+ * Tool-call supplement log (ADR-008 / docs/codex-integration.md).
170
+ *
171
+ * Codex `approval_policy=auto`/`workspace-write` 환경에서 PermissionRequest
172
+ * hook 이 dispatch 되지 않아 `permissions-<id>.jsonl` 가 생성되지 않는 갭을
173
+ * 보완. 모든 PreToolUse 호출에서 동일 파일에 append (source='pre-tool-use'
174
+ * 로 구분). permission-handler 의 entry 와 source 필드로 reader 측 dedup.
175
+ *
176
+ * fail-open: write 실패는 hook 차단하지 않음.
177
+ */
178
+ function logToolCallSupplement(sessionId, toolName) {
179
+ try {
180
+ const logPath = path.join(STATE_DIR, `permissions-${sanitizeId(sessionId)}.jsonl`);
181
+ fs.mkdirSync(STATE_DIR, { recursive: true });
182
+ const entry = JSON.stringify({
183
+ timestamp: new Date().toISOString(),
184
+ tool: toolName,
185
+ decision: 'pre-approved',
186
+ source: 'pre-tool-use',
187
+ });
188
+ fs.appendFileSync(logPath, `${entry}\n`);
189
+ }
190
+ catch (e) {
191
+ log.debug('tool-call supplement log 실패', e);
192
+ }
193
+ }
168
194
  /** 연속 파싱 실패 카운터 관리 */
169
195
  function getAndIncrementFailCount() {
170
196
  try {
@@ -322,6 +348,11 @@ async function main() {
322
348
  const toolName = data.tool_name ?? data.toolName ?? '';
323
349
  const toolInput = data.tool_input ?? data.toolInput ?? {};
324
350
  const sessionId = data.session_id ?? 'default';
351
+ // ADR-008 / codex-integration.md — Codex `approval_policy=auto` 에서
352
+ // PermissionRequest 가 dispatch 안 되는 갭 보완. 모든 tool call 을
353
+ // permissions-<id>.jsonl 에 supplement 로 append (source='pre-tool-use').
354
+ if (toolName)
355
+ logToolCallSupplement(sessionId, toolName);
325
356
  // ADR-001 Mech-A PreToolUse dispatcher — 사용자가 정의한 rule 이 빌트인 위험-명령 감지보다 먼저.
326
357
  // 이렇게 해야 rule.block_message (맥락 있는 안내) 가 제네릭 "Dangerous command blocked" 대신 노출됨.
327
358
  // fail-open: 예외는 hook 차단 안 함.
@@ -149,6 +149,17 @@ async function main() {
149
149
  console.log(approve());
150
150
  return;
151
151
  }
152
+ // 0.4.6 — codex/claude 진입점 양쪽에서 v1-bootstrap 보장.
153
+ // 이전엔 prepareHarness (fgx/forgen wrapper) 만 호출 → 직접 claude/codex 호출 시
154
+ // ~/.forgen/state/sessions/<id>.json 미생성. SessionStart hook 에서도 호출하여
155
+ // 양쪽 진입 경로 모두에서 session state 박제.
156
+ try {
157
+ const { bootstrapV1Session } = await import('../core/v1-bootstrap.js');
158
+ bootstrapV1Session();
159
+ }
160
+ catch (e) {
161
+ log.debug('v1-bootstrap SessionStart 호출 실패 (fail-open)', e);
162
+ }
152
163
  if (!fs.existsSync(STATE_DIR)) {
153
164
  console.log(approve());
154
165
  return;
@@ -5,54 +5,69 @@
5
5
  * (for await of process.stdin은 일부 환경에서 hang 발생)
6
6
  */
7
7
  const MAX_STDIN_BYTES = 10 * 1024 * 1024; // 10MB — 메모리 고갈 방지
8
+ /**
9
+ * 0.4.6 perf #11 — idle-based early resolve + initial-wait fallback.
10
+ *
11
+ * Claude/Codex CLI 가 hook 호출 후 stdin 을 EOF 닫지 않고 hang on 하는 케이스가 있어
12
+ * 이전엔 timeoutMs (2000ms) 까지 무조건 대기 → p95 hook latency 가 2003ms 였음
13
+ * (hook-timing.jsonl 측정 결과).
14
+ *
15
+ * 두 단계 fix:
16
+ * 1. IDLE_RESOLVE_MS — 'data' 받은 후 추가 chunk 없으면 100ms 후 resolve
17
+ * 2. INITIAL_WAIT_MS — 'data' 자체가 안 오는 케이스 (codex 일부 hook event)
18
+ * 를 대비해 INITIAL_WAIT_MS 후 chunks 가 비어 있으면 빈 데이터로 resolve
19
+ *
20
+ * 합법적 데이터 손실 위험 없음 — Claude/Codex 의 stdin 페이로드는 호출 즉시
21
+ * (≤ INITIAL_WAIT_MS) 첫 chunk 도착. 안 오면 그 이벤트는 stdin 자체가 없는 것.
22
+ */
23
+ const IDLE_RESOLVE_MS = 100;
24
+ const INITIAL_WAIT_MS = 300;
8
25
  /** stdin에서 JSON 데이터를 읽어 파싱. 실패 시 null 반환. */
9
26
  export async function readStdinJSON(timeoutMs = 2000) {
10
27
  const chunks = [];
11
28
  let totalSize = 0;
12
29
  let settled = false;
30
+ let idleTimer = null;
13
31
  const raw = await new Promise((resolve) => {
14
- const timeout = setTimeout(() => {
15
- if (!settled) {
16
- settled = true;
17
- process.stdin.removeAllListeners();
32
+ const settle = (clearIdle = true) => {
33
+ if (settled)
34
+ return;
35
+ settled = true;
36
+ if (clearIdle && idleTimer)
37
+ clearTimeout(idleTimer);
38
+ clearTimeout(timeout);
39
+ process.stdin.removeAllListeners();
40
+ if (typeof process.stdin.pause === 'function')
18
41
  process.stdin.pause();
19
- resolve(Buffer.concat(chunks).toString('utf-8'));
20
- }
21
- }, timeoutMs);
42
+ resolve(Buffer.concat(chunks).toString('utf-8'));
43
+ };
44
+ const timeout = setTimeout(() => settle(), timeoutMs);
45
+ // perf: INITIAL_WAIT_MS 안에 'data' 가 한 번도 안 오면 stdin 부재로 간주, 조기 resolve.
46
+ // codex 일부 hook event (e.g. SessionStart, Stop without payload) 가 stdin 안 보내는 케이스 대응.
47
+ const initialWait = setTimeout(() => {
48
+ if (!settled && chunks.length === 0)
49
+ settle();
50
+ }, INITIAL_WAIT_MS);
22
51
  // 일부 Node.js 환경에서 stdin이 paused 상태로 시작 — 명시적 resume 필요
23
52
  if (typeof process.stdin.resume === 'function') {
24
53
  process.stdin.resume();
25
54
  }
26
55
  process.stdin.on('data', (chunk) => {
56
+ clearTimeout(initialWait);
27
57
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28
58
  totalSize += buf.length;
29
59
  if (totalSize > MAX_STDIN_BYTES) {
30
- if (!settled) {
31
- settled = true;
32
- clearTimeout(timeout);
33
- process.stdin.removeAllListeners();
34
- if (typeof process.stdin.pause === 'function')
35
- process.stdin.pause();
36
- resolve('');
37
- }
60
+ settle();
38
61
  return;
39
62
  }
40
63
  chunks.push(buf);
64
+ // perf: 데이터 수신 후 IDLE_RESOLVE_MS 동안 추가 chunk 없으면 early resolve
65
+ if (idleTimer)
66
+ clearTimeout(idleTimer);
67
+ idleTimer = setTimeout(() => settle(false), IDLE_RESOLVE_MS);
41
68
  });
42
- process.stdin.on('end', () => {
43
- if (!settled) {
44
- settled = true;
45
- clearTimeout(timeout);
46
- resolve(Buffer.concat(chunks).toString('utf-8'));
47
- }
48
- });
49
- process.stdin.on('error', () => {
50
- if (!settled) {
51
- settled = true;
52
- clearTimeout(timeout);
53
- resolve('');
54
- }
55
- });
69
+ process.stdin.on('end', () => settle());
70
+ process.stdin.on('error', () => settle());
56
71
  });
57
72
  try {
58
73
  return JSON.parse(raw);
@@ -55,11 +55,16 @@ async function main() {
55
55
  }
56
56
  })();
57
57
  try {
58
+ // 0.4.6 fix — delegate hook 이 자기가 codex runtime context 인지 알 수 있게
59
+ // FORGEN_RUNTIME=codex 명시 주입. 이전엔 buildEnv (forgen wrapper) 경로로만
60
+ // set 되어 codex 직접 호출 시 default 'claude' 로 fall-through → usage-telemetry
61
+ // 의 rt 필드가 잘못 박힘 (clean-container e2e 라운드 20 에서 발견).
58
62
  const result = spawnSync(process.execPath, [delegatePath, ...restArgs], {
59
63
  encoding: 'utf-8',
60
64
  input: JSON.stringify(input),
61
65
  cwd: process.cwd(),
62
66
  stdio: ['pipe', 'pipe', 'pipe'],
67
+ env: { ...process.env, FORGEN_RUNTIME: 'codex' },
63
68
  });
64
69
  if (result.error) {
65
70
  console.log(JSON.stringify({ continue: true }));
@@ -23,13 +23,19 @@ const AGENTS_MD_END = '<!-- <<< forgen-managed-rules -->';
23
23
  function resolveCodexHome(opts) {
24
24
  return opts.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
25
25
  }
26
+ // 0.4.6 fix — pkgRoot match 외에 script-marker fallback 추가.
27
+ // Stale install path (예: 다른 머신에서 install 한 hooks.json 마운트, 또는
28
+ // node_modules path 변경) 의 forgen entry 를 "user entry" 로 오분류 → 중복 누적
29
+ // 하던 버그. forgen hook 의 시그니처는 dist/host/codex-adapter.js 또는
30
+ // dist/hooks/<name>.js — 사용자 custom hook 과 충돌 가능성 거의 없음.
31
+ const FORGEN_HOOK_SCRIPT_MARKER = /\bdist\/(host\/codex-adapter|hooks\/[a-z][a-z0-9-]+)\.js\b/;
26
32
  function isForgenManagedHook(entry, pkgRoot) {
27
33
  if (!entry || typeof entry !== 'object')
28
34
  return false;
29
35
  const e = entry;
30
36
  if (!Array.isArray(e.hooks))
31
37
  return false;
32
- return e.hooks.some((h) => typeof h.command === 'string' && h.command.includes(pkgRoot));
38
+ return e.hooks.some((h) => typeof h.command === 'string' && (h.command.includes(pkgRoot) || FORGEN_HOOK_SCRIPT_MARKER.test(h.command)));
33
39
  }
34
40
  function readJsonFile(p) {
35
41
  try {
@@ -85,6 +85,19 @@ export function resolveLaunchContext(args) {
85
85
  }
86
86
  continue;
87
87
  }
88
+ // 0.4.6 — shortcut flags `--codex` / `--claude` (= `--runtime codex|claude`).
89
+ // fgx --codex 같은 짧은 호출을 위해. claude/codex CLI 가 동일 이름 플래그를
90
+ // 자체적으로 정의하지 않는 한 forwarding 충돌 없음.
91
+ if (arg === '--codex') {
92
+ result.runtime = 'codex';
93
+ result.runtimeSource = 'flag';
94
+ continue;
95
+ }
96
+ if (arg === '--claude') {
97
+ result.runtime = 'claude';
98
+ result.runtimeSource = 'flag';
99
+ continue;
100
+ }
88
101
  result.args.push(arg);
89
102
  }
90
103
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooojin/forgen",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
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.5",
4
+ "version": "0.4.6",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",