@wooojin/forgen 0.5.0 → 0.5.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +236 -0
- package/README.ja.md +16 -8
- package/README.ko.md +17 -9
- package/README.md +6 -1
- package/README.zh.md +16 -8
- package/assets/claude/skills/verify/SKILL.md +66 -0
- package/assets/shared/hook-registry.json +217 -22
- package/dist/cli.js +7 -3
- package/dist/core/auto-compound-runner.js +73 -14
- package/dist/core/changelog-cli.d.ts +4 -1
- package/dist/core/changelog-cli.js +8 -7
- package/dist/core/compound-sweep-cli.d.ts +4 -1
- package/dist/core/compound-sweep-cli.js +25 -5
- package/dist/core/doctor.js +37 -0
- package/dist/core/spawn.d.ts +26 -0
- package/dist/core/spawn.js +38 -3
- package/dist/core/uninstall.d.ts +7 -0
- package/dist/core/uninstall.js +46 -1
- package/dist/engine/compound-loop.js +20 -1
- package/dist/hooks/context-guard.d.ts +3 -0
- package/dist/hooks/context-guard.js +16 -8
- package/dist/hooks/hook-config.js +5 -0
- package/dist/hooks/hook-registry.d.ts +17 -1
- package/dist/hooks/hooks-generator.d.ts +2 -0
- package/dist/hooks/hooks-generator.js +8 -1
- package/dist/hooks/session-end.d.ts +41 -0
- package/dist/hooks/session-end.js +113 -0
- package/dist/hooks/session-recovery.js +18 -1
- package/dist/hooks/shared/hook-timing.js +6 -1
- package/dist/host/codex-adapter.js +5 -0
- package/dist/host/codex-hook-alive.d.ts +23 -0
- package/dist/host/codex-hook-alive.js +51 -0
- package/dist/host/codex-notify.d.ts +55 -0
- package/dist/host/codex-notify.js +153 -0
- package/dist/host/codex-rollout.d.ts +21 -0
- package/dist/host/codex-rollout.js +82 -0
- package/dist/host/codex-rules-context.d.ts +25 -0
- package/dist/host/codex-rules-context.js +62 -0
- package/dist/host/exec-host.d.ts +10 -0
- package/dist/host/exec-host.js +18 -3
- package/dist/host/install-claude.d.ts +14 -0
- package/dist/host/install-claude.js +53 -0
- package/dist/host/install-codex.d.ts +120 -0
- package/dist/host/install-codex.js +560 -52
- package/dist/host/install-orchestrator.d.ts +4 -0
- package/dist/host/install-orchestrator.js +32 -2
- package/dist/host/invoke-agent.js +1 -0
- package/dist/host/parity-harness.js +7 -3
- package/dist/host/projection.d.ts +37 -3
- package/dist/host/projection.js +93 -42
- package/dist/store/evidence-store.js +121 -57
- package/dist/store/rule-store.d.ts +41 -0
- package/dist/store/rule-store.js +77 -2
- package/hooks/hooks.json +13 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/architecture-decision/SKILL.md +18 -0
- package/skills/calibrate/SKILL.md +18 -0
- package/skills/code-review/SKILL.md +17 -0
- package/skills/compound/SKILL.md +17 -0
- package/skills/deep-interview/SKILL.md +7 -0
- package/skills/docker/SKILL.md +18 -0
- package/skills/forge-loop/SKILL.md +23 -0
- package/skills/learn/SKILL.md +15 -0
- package/skills/retro/SKILL.md +16 -0
- package/skills/ship/SKILL.md +18 -0
package/dist/core/spawn.js
CHANGED
|
@@ -112,7 +112,7 @@ function findSessionTranscript(cwd, sessionStartMs, preSnapshot, runtime = 'clau
|
|
|
112
112
|
*
|
|
113
113
|
* 단일 함수에서 둘 다 처리 — schema 자동 감지.
|
|
114
114
|
*/
|
|
115
|
-
async function countUserMessages(transcriptPath) {
|
|
115
|
+
export async function countUserMessages(transcriptPath) {
|
|
116
116
|
const { createInterface } = await import('node:readline');
|
|
117
117
|
const stream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
|
|
118
118
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
@@ -300,6 +300,18 @@ export function releaseAutoCompoundInflight(sessionId) {
|
|
|
300
300
|
/* noop */
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* ~/.forgen/state/auto-compound/<sessionId>.log — 세션별 러너 로그 경로.
|
|
305
|
+
*
|
|
306
|
+
* 결함2 관측성 fix (2026-08-18): 이전엔 detached spawn 을 `stdio: 'ignore'` 로
|
|
307
|
+
* 실행해 러너의 stdout/stderr(진단 메시지 포함)를 통째로 버렸다. 그 결과 solution
|
|
308
|
+
* extraction 이 98/98 sweep 에서 `extractedSolutions=0` 을 기록해도 원인을 알 방법이
|
|
309
|
+
* 없었다(완전 무음 실패). 이제 fd 를 파일로 열어 넘겨 러너의 모든 출력을 세션별
|
|
310
|
+
* 로그로 남긴다 — 다음 실패 모드를 디버그 가능하게 만드는 전제 조건.
|
|
311
|
+
*/
|
|
312
|
+
export function autoCompoundLogPath(sessionId) {
|
|
313
|
+
return path.join(STATE_DIR, 'auto-compound', `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.log`);
|
|
314
|
+
}
|
|
303
315
|
export function runAutoCompound(cwd, transcriptPath, sessionId, promptCount = 0) {
|
|
304
316
|
const now = Date.now();
|
|
305
317
|
// 1. 완료 마커 cooldown (secondary, 단일슬롯 — 값싼 read 먼저).
|
|
@@ -322,17 +334,40 @@ export function runAutoCompound(cwd, transcriptPath, sessionId, promptCount = 0)
|
|
|
322
334
|
return 'skipped-inflight';
|
|
323
335
|
}
|
|
324
336
|
const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'auto-compound-runner.js');
|
|
337
|
+
// 로그 파일 fd 열기 — 실패(디스크 문제 등)해도 fail-open 으로 'ignore' 폴백.
|
|
338
|
+
const logPath = autoCompoundLogPath(sessionId);
|
|
339
|
+
let logFd;
|
|
325
340
|
try {
|
|
326
|
-
|
|
341
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
342
|
+
logFd = fs.openSync(logPath, 'a');
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
log.debug('auto-compound 로그 파일 열기 실패 — stdio ignore로 폴백', e);
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
// 결함2 corrected fix (2026-08-19): 리터럴 'node' 대신 process.execPath 로 spawn.
|
|
349
|
+
// 실 cron 은 절대 nvm node 경로로 sweep 를 돌리는데, 러너를 'node' 로 spawn 하면
|
|
350
|
+
// cron sparse PATH 의 /bin/node 로 해석돼 러너의 process.execPath 가 달라지고,
|
|
351
|
+
// 러너가 forgen(nvm 형제 바이너리)을 절대경로로 못 찾아 solution write 가 ENOENT 로
|
|
352
|
+
// 실패한다. execPath 를 물려주면 러너가 sweep 과 같은 node(=forgen 형제)를 상속한다.
|
|
353
|
+
const child = spawn(process.execPath, [runnerPath, cwd, transcriptPath, sessionId, String(promptCount)], {
|
|
327
354
|
cwd,
|
|
328
355
|
detached: true,
|
|
329
|
-
stdio: 'ignore',
|
|
356
|
+
stdio: logFd !== undefined ? ['ignore', logFd, logFd] : 'ignore',
|
|
330
357
|
});
|
|
331
358
|
child.unref();
|
|
359
|
+
if (logFd !== undefined)
|
|
360
|
+
fs.closeSync(logFd); // child가 dup — 부모 측 fd는 닫아도 안전
|
|
332
361
|
console.log('\n[forgen] 세션 분석을 백그라운드로 시작했습니다 (자동 compound) — 결과는 다음 세션 시작 시 반영됩니다.\n');
|
|
333
362
|
return 'spawned';
|
|
334
363
|
}
|
|
335
364
|
catch (e) {
|
|
365
|
+
if (logFd !== undefined) {
|
|
366
|
+
try {
|
|
367
|
+
fs.closeSync(logFd);
|
|
368
|
+
}
|
|
369
|
+
catch { /* noop */ }
|
|
370
|
+
}
|
|
336
371
|
log.debug('auto-compound 시작 실패', e);
|
|
337
372
|
releaseAutoCompoundInflight(sessionId); // spawn 실패 시 표식 제거 → 재시도 가능
|
|
338
373
|
return 'failed';
|
package/dist/core/uninstall.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/** ADR-016 D3 — ~/.claude/skills/verify 제거 (forgen-managed 마커가 있는 것만; 사용자 스킬은 보존) */
|
|
2
|
+
export declare function cleanVerifySkill(homeDir?: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* ADR-016 D1 — Codex config.toml 의 forgen notify 블록 제거. 남겨 두면 Codex 가 매 턴 사라진 스크립트를
|
|
5
|
+
* spawn 한다. (Codex hooks.json / MCP 블록 정리는 아직 uninstall 범위 밖 — 알려진 갭.)
|
|
6
|
+
*/
|
|
7
|
+
export declare function cleanCodexNotify(codexHome?: string): Promise<boolean>;
|
|
1
8
|
/** forgen uninstall 메인 */
|
|
2
9
|
export declare function handleUninstall(cwd: string, options: {
|
|
3
10
|
force?: boolean;
|
package/dist/core/uninstall.js
CHANGED
|
@@ -96,6 +96,47 @@ function cleanSlashCommands() {
|
|
|
96
96
|
console.log(' - No forgen-managed slash commands found');
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
/** ADR-016 D3 — ~/.claude/skills/verify 제거 (forgen-managed 마커가 있는 것만; 사용자 스킬은 보존) */
|
|
100
|
+
export function cleanVerifySkill(homeDir = os.homedir()) {
|
|
101
|
+
const dir = path.join(homeDir, '.claude', 'skills', 'verify');
|
|
102
|
+
const file = path.join(dir, 'SKILL.md');
|
|
103
|
+
try {
|
|
104
|
+
if (fs.lstatSync(dir).isSymbolicLink() || fs.lstatSync(file).isSymbolicLink())
|
|
105
|
+
return false;
|
|
106
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
107
|
+
if (!/^---\n[\s\S]*?\n---\n\s*<!-- forgen-managed -->/.test(content))
|
|
108
|
+
return false;
|
|
109
|
+
fs.unlinkSync(file);
|
|
110
|
+
try {
|
|
111
|
+
if (fs.readdirSync(dir).length === 0)
|
|
112
|
+
fs.rmdirSync(dir);
|
|
113
|
+
}
|
|
114
|
+
catch { /* ignore */ }
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false; // 없음
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* ADR-016 D1 — Codex config.toml 의 forgen notify 블록 제거. 남겨 두면 Codex 가 매 턴 사라진 스크립트를
|
|
123
|
+
* spawn 한다. (Codex hooks.json / MCP 블록 정리는 아직 uninstall 범위 밖 — 알려진 갭.)
|
|
124
|
+
*/
|
|
125
|
+
export async function cleanCodexNotify(codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex')) {
|
|
126
|
+
const configPath = path.join(codexHome, 'config.toml');
|
|
127
|
+
try {
|
|
128
|
+
const current = fs.readFileSync(configPath, 'utf-8');
|
|
129
|
+
const { removeNotifyBlock } = await import('../host/install-codex.js');
|
|
130
|
+
const r = removeNotifyBlock(current);
|
|
131
|
+
if (!r.removed)
|
|
132
|
+
return false;
|
|
133
|
+
fs.writeFileSync(configPath, r.content, 'utf-8');
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false; // Codex 미사용
|
|
138
|
+
}
|
|
139
|
+
}
|
|
99
140
|
/** 사용자에게 y/n 확인 */
|
|
100
141
|
function confirm(message) {
|
|
101
142
|
return new Promise((resolve) => {
|
|
@@ -301,7 +342,7 @@ export async function handleUninstall(cwd, options) {
|
|
|
301
342
|
console.log(' 2. Delete .claude/agents/ch-*.md agent files');
|
|
302
343
|
console.log(' 3. Delete .claude/rules/ rule files (project-context, routing, forge-*)');
|
|
303
344
|
console.log(' 4. Remove forgen block from CLAUDE.md');
|
|
304
|
-
console.log(' 5. Remove slash commands (~/.claude/commands/forgen/)');
|
|
345
|
+
console.log(' 5. Remove slash commands (~/.claude/commands/forgen/) and the forgen-managed verify skill (~/.claude/skills/verify/)');
|
|
305
346
|
console.log(' 6. Remove plugin artifacts (cache, installed_plugins.json, plugin directory)');
|
|
306
347
|
if (options.purge) {
|
|
307
348
|
console.log(' 7. --purge: Delete ~/.forgen/ entirely (rules, me/, state/, solutions/, behavior/)');
|
|
@@ -330,6 +371,10 @@ export async function handleUninstall(cwd, options) {
|
|
|
330
371
|
cleanCompoundRules(cwd);
|
|
331
372
|
cleanClaudeMd(cwd);
|
|
332
373
|
cleanSlashCommands();
|
|
374
|
+
if (cleanVerifySkill())
|
|
375
|
+
console.log(' ✓ Removed forgen verify skill (~/.claude/skills/verify/)');
|
|
376
|
+
if (await cleanCodexNotify())
|
|
377
|
+
console.log(' ✓ Removed forgen notify block from Codex config.toml');
|
|
333
378
|
cleanPluginArtifacts();
|
|
334
379
|
if (options.purge) {
|
|
335
380
|
try {
|
|
@@ -210,6 +210,17 @@ export async function handleCompound(args) {
|
|
|
210
210
|
`);
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
|
+
// --- manual add (defense-in-depth, 2026-08-20) ---
|
|
214
|
+
// manual add(--solution/--rule/--convention/--pattern)를 아래 서브커맨드 dispatch 보다
|
|
215
|
+
// 먼저 처리한다. dispatch 는 args.includes('remove'|'clean-stale'|...) 로 전체 인자를
|
|
216
|
+
// 스캔하므로, add 의 위치인자(title/content)에 그런 토큰이 섞이면 삭제·정리 분기를
|
|
217
|
+
// 탈취할 수 있다(argument confusion — auto-compound 러너가 untrusted 모델 출력을 넘기던
|
|
218
|
+
// 경로에서 실증). type flag 는 add 전용이라 우선 처리해도 legit 동작 불변이며, 이렇게
|
|
219
|
+
// 하면 title/content 가 dispatch 스캔에 아예 닿지 않는다. (러너측 대시-선두 가드에 더한 2중 방어)
|
|
220
|
+
if (['--solution', '--rule', '--convention', '--pattern'].some(f => args.includes(f))) {
|
|
221
|
+
await handleManualAdd(args, cwd, scope);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
213
224
|
// --- export command ---
|
|
214
225
|
// 통짜 tar.gz export(플래그만, positional 없음) vs 이름 지정 패턴 export를
|
|
215
226
|
// positional 인자 유무로 구분한다. `compound export foo` = 패턴 export,
|
|
@@ -435,10 +446,18 @@ export async function handleCompound(args) {
|
|
|
435
446
|
console.log(' Unknown compound arguments. Run `forgen compound --help` for usage.\n');
|
|
436
447
|
return;
|
|
437
448
|
}
|
|
449
|
+
await handleManualAdd(args, cwd, scope);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 수동 인사이트 추가 (--solution/--rule/--convention/--pattern).
|
|
453
|
+
* handleCompound 에서 두 경로로 호출된다:
|
|
454
|
+
* (1) hoisted early-dispatch — type flag 가 있으면 서브커맨드 스캔 전에 즉시(arg-confusion 방어).
|
|
455
|
+
* (2) fallthrough — type flag 없이 다른 known flag(예: --to)만 있는 legacy 경로(동작 보존).
|
|
456
|
+
*/
|
|
457
|
+
async function handleManualAdd(args, cwd, scope) {
|
|
438
458
|
console.log('\n Compound Loop — Accumulating insights\n');
|
|
439
459
|
console.log(` Scope: ${scope.summary}`);
|
|
440
460
|
console.log();
|
|
441
|
-
// 수동 인사이트 추가
|
|
442
461
|
const type = args.includes('--solution') ? 'solution'
|
|
443
462
|
: args.includes('--rule') ? 'rule'
|
|
444
463
|
: args.includes('--convention') ? 'convention'
|
|
@@ -57,6 +57,9 @@ export declare function effectiveCooldownMs(parsed: {
|
|
|
57
57
|
userPatternFound?: boolean;
|
|
58
58
|
promptCount?: number;
|
|
59
59
|
}, currentPromptCount?: number): number;
|
|
60
|
+
export declare function maybeSpawnAutoCompound(sessionId: string, transcriptPath: string | undefined, promptCount: number,
|
|
61
|
+
/** ADR-016 D1: notify 폴백은 훅 env(FORGEN_CWD) 없이 돌므로 페이로드의 cwd 를 넘긴다. */
|
|
62
|
+
cwdOverride?: string): Promise<boolean>;
|
|
60
63
|
/**
|
|
61
64
|
* forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
|
|
62
65
|
* OMC의 persistent-mode.cjs 패턴 참고.
|
|
@@ -221,8 +221,12 @@ export async function main() {
|
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
223
|
const sessionId = input.session_id ?? 'default';
|
|
224
|
-
// Stop
|
|
225
|
-
|
|
224
|
+
// Stop 훅 판별. Claude 는 `stop_hook_type`(user|end_turn|…) 을 주지만 Codex 의 Stop 입력에는 그 필드가
|
|
225
|
+
// 없고 `hook_event_name:"Stop"` 만 온다 (0.5.5 — 이전엔 Codex 세션에서 Stop 분기 전체가 건너뛰어져
|
|
226
|
+
// finalizeSession / Stop 트리거 auto-compound / rate-limit 감지가 돌지 않았다). Codex 의 정상 턴 종료는
|
|
227
|
+
// end_turn 과 동치로 취급한다.
|
|
228
|
+
const stopType = input.stop_hook_type ?? (input.hook_event_name === 'Stop' ? 'end_turn' : undefined);
|
|
229
|
+
if (stopType) {
|
|
226
230
|
_hookEvent = 'Stop';
|
|
227
231
|
// 세션 종료 시 pending outcome을 unknown으로 finalize.
|
|
228
232
|
// 과거에는 프로덕션에서 호출되지 않아 pending이 다음 세션의 flushAccept에
|
|
@@ -289,7 +293,7 @@ export async function main() {
|
|
|
289
293
|
}
|
|
290
294
|
}
|
|
291
295
|
// 정상 종료 시: 의미 있는 세션이었으면 compound 안내/자동 트리거
|
|
292
|
-
if (
|
|
296
|
+
if (stopType === 'user' || stopType === 'end_turn') {
|
|
293
297
|
const state = loadContextState(sessionId);
|
|
294
298
|
// ADR-002 T1 — 세션 중간에 교정이 들어와도 session-scoped rule 이 me-scope 으로
|
|
295
299
|
// 승급되도록 Stop 에서 직접 auto-compound-runner 를 debounced 로 트리거.
|
|
@@ -435,9 +439,11 @@ export function effectiveCooldownMs(parsed, currentPromptCount = 0) {
|
|
|
435
439
|
const grew = currentPromptCount - (parsed.promptCount ?? 0) >= AUTO_COMPOUND_GROWTH_THRESHOLD;
|
|
436
440
|
return grew ? AUTO_COMPOUND_COOLDOWN_MS : AUTO_COMPOUND_BARREN_COOLDOWN_MS;
|
|
437
441
|
}
|
|
438
|
-
async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount
|
|
442
|
+
export async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount,
|
|
443
|
+
/** ADR-016 D1: notify 폴백은 훅 env(FORGEN_CWD) 없이 돌므로 페이로드의 cwd 를 넘긴다. */
|
|
444
|
+
cwdOverride) {
|
|
439
445
|
if (!transcriptPath || promptCount < 10)
|
|
440
|
-
return;
|
|
446
|
+
return false;
|
|
441
447
|
const markerPath = path.join(STATE_DIR, 'last-auto-compound.json');
|
|
442
448
|
try {
|
|
443
449
|
const raw = fs.readFileSync(markerPath, 'utf-8');
|
|
@@ -446,12 +452,12 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
|
|
|
446
452
|
const last = parsed.completedAt ? Date.parse(parsed.completedAt) : 0;
|
|
447
453
|
const cooldown = effectiveCooldownMs(parsed, promptCount);
|
|
448
454
|
if (Number.isFinite(last) && Date.now() - last < cooldown)
|
|
449
|
-
return;
|
|
455
|
+
return false;
|
|
450
456
|
}
|
|
451
457
|
}
|
|
452
458
|
catch { /* first time or corrupt — proceed */ }
|
|
453
459
|
const { spawn: spawnProcess } = await import('node:child_process');
|
|
454
|
-
const cwd = process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
460
|
+
const cwd = cwdOverride ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
455
461
|
// 기본: 번들된 auto-compound-runner. 프로덕션 빌드는 이 경로만 실행.
|
|
456
462
|
const defaultRunner = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'core', 'auto-compound-runner.js');
|
|
457
463
|
// 테스트 주입 경로 — FORGEN_TEST=1 게이트 + 경로 containment (~/.forgen 또는 /tmp 하위만 허용).
|
|
@@ -482,7 +488,7 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
|
|
|
482
488
|
const { claimAutoCompoundInflight, releaseAutoCompoundInflight } = await import('../core/spawn.js');
|
|
483
489
|
if (!claimAutoCompoundInflight(sessionId)) {
|
|
484
490
|
log.debug('Stop-triggered auto-compound skip: 세션 in-flight');
|
|
485
|
-
return;
|
|
491
|
+
return false;
|
|
486
492
|
}
|
|
487
493
|
try {
|
|
488
494
|
const child = spawnProcess('node', [runnerPath, cwd, transcriptPath, sessionId, String(promptCount)], {
|
|
@@ -491,10 +497,12 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
|
|
|
491
497
|
});
|
|
492
498
|
child.unref();
|
|
493
499
|
log.debug(`Stop-triggered auto-compound 시작: ${sessionId} (${promptCount} prompts)`);
|
|
500
|
+
return true;
|
|
494
501
|
}
|
|
495
502
|
catch (e) {
|
|
496
503
|
releaseAutoCompoundInflight(sessionId); // spawn 실패 → 재시도 가능
|
|
497
504
|
log.debug('Stop-triggered auto-compound spawn 실패', e);
|
|
505
|
+
return false;
|
|
498
506
|
}
|
|
499
507
|
}
|
|
500
508
|
// forge-loop 차단 안전 상한 (무한 루프 방지)
|
|
@@ -146,6 +146,11 @@ export function loadHookConfig(hookName) {
|
|
|
146
146
|
* 4. 기본값 true (하위호환)
|
|
147
147
|
*/
|
|
148
148
|
export function isHookEnabled(hookName) {
|
|
149
|
+
// -1) ADR-015 C-G1: forgen 자신이 띄운 중첩 `claude -p` 추출 실행 안에서는 모든 훅을 끈다.
|
|
150
|
+
// (--bare 는 OAuth 를 끊어 쓸 수 없음.) 이전엔 추출 run 마다 22개 훅이 재귀 발화해
|
|
151
|
+
// hook-timing / sessions / usage-telemetry 를 오염시키고 auto-compound 연쇄를 유발했다.
|
|
152
|
+
if (process.env.FORGEN_NESTED_RUN === '1')
|
|
153
|
+
return false;
|
|
149
154
|
// 0) compound-core 가드레일 — config 어떤 경로로도 끌 수 없음
|
|
150
155
|
if (PROTECTED_HOOKS.has(hookName))
|
|
151
156
|
return true;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - workflow: 워크플로우 스킬 훅 (다른 플러그인 감지 시 자동 비활성)
|
|
12
12
|
*/
|
|
13
13
|
export type HookTier = 'compound-core' | 'safety' | 'workflow';
|
|
14
|
-
export type HookEventType = 'UserPromptSubmit' | 'SessionStart' | 'Stop' | 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure' | 'SubagentStart' | 'SubagentStop' | 'PreCompact' | 'PermissionRequest';
|
|
14
|
+
export type HookEventType = 'UserPromptSubmit' | 'SessionStart' | 'Stop' | 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure' | 'SubagentStart' | 'SubagentStop' | 'PreCompact' | 'PermissionRequest' | 'SessionEnd';
|
|
15
15
|
export interface HookEntry {
|
|
16
16
|
/** 고유 이름 (hook-config.json에서 사용) */
|
|
17
17
|
name: string;
|
|
@@ -27,6 +27,22 @@ export interface HookEntry {
|
|
|
27
27
|
timeout: number;
|
|
28
28
|
/** compound 피드백 루프에 필수인 훅인지 */
|
|
29
29
|
compoundCritical: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* ADR-015: 이 훅을 등록할 host 목록. 생략 시 모든 host. Codex 의 hooks.json 은 바이트 동일성이
|
|
32
|
+
* 훅 신뢰(trust) 와 묶여 있으므로, Claude 에만 추가하는 이벤트는 `["claude"]` 로 한정한다.
|
|
33
|
+
*/
|
|
34
|
+
hosts?: Array<'claude' | 'codex' | 'opencode'>;
|
|
35
|
+
/**
|
|
36
|
+
* ADR-016 D2: Codex hooks.json 핸들러에만 붙는 필드. Codex 의 trust 해시는 핸들러 단위라
|
|
37
|
+
* 여기 값을 바꾸면 *그 핸들러만* `/hooks` 재승인이 필요하다 (다른 핸들러는 영향 없음).
|
|
38
|
+
*/
|
|
39
|
+
codex?: {
|
|
40
|
+
/**
|
|
41
|
+
* `additionalContext` 를 임시 파일로 스필하는 임계(근사 토큰, 기본 2500 ≈ 10KB). 0 = 스필 안 함.
|
|
42
|
+
* Codex 는 PreToolUse/PostToolUse/SessionStart/UserPromptSubmit/SubagentStart 에서만 인정한다.
|
|
43
|
+
*/
|
|
44
|
+
additionalContextLimit?: number;
|
|
45
|
+
};
|
|
30
46
|
}
|
|
31
47
|
export declare const HOOK_REGISTRY: HookEntry[];
|
|
32
48
|
/** 티어별 훅 목록 조회 */
|
|
@@ -57,6 +57,9 @@ export function generateHooksJson(options) {
|
|
|
57
57
|
const hasOtherPlugins = !releaseMode && detectInstalledPlugins(cwd).length > 0;
|
|
58
58
|
// 활성 훅 필터링
|
|
59
59
|
const activeHooks = HOOK_REGISTRY.filter(hook => {
|
|
60
|
+
// 0) ADR-015: host 한정 훅 (예: SessionEnd 는 Claude 전용 — Codex hooks.json 바이트 동일성 유지)
|
|
61
|
+
if (hook.hosts && !hook.hosts.includes(runtime))
|
|
62
|
+
return false;
|
|
60
63
|
// 1) hook-config.json에서 명시적 비활성화 (releaseMode 시 무시)
|
|
61
64
|
if (!releaseMode && !isHookEnabled(hook.name))
|
|
62
65
|
return false;
|
|
@@ -89,7 +92,11 @@ export function generateHooksJson(options) {
|
|
|
89
92
|
matcher,
|
|
90
93
|
hooks: matcherEntries.map(h => {
|
|
91
94
|
const command = buildHookCommand(pluginRoot, h.script, runtime);
|
|
92
|
-
|
|
95
|
+
const handler = { type: 'command', command, timeout: h.timeout };
|
|
96
|
+
if (runtime === 'codex' && typeof h.codex?.additionalContextLimit === 'number') {
|
|
97
|
+
handler.additionalContextLimit = h.codex.additionalContextLimit;
|
|
98
|
+
}
|
|
99
|
+
return handler;
|
|
93
100
|
}),
|
|
94
101
|
}));
|
|
95
102
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — SessionEnd Hook (ADR-015 C-G6 Claude, ADR-016 D2 Codex)
|
|
4
|
+
*
|
|
5
|
+
* Claude Code `SessionEnd` (reason: clear|resume|logout|prompt_input_exit|other, 기본 예산 1.5s)
|
|
6
|
+
* 에서 이전 세션 transcript 를 auto-compound 러너에 넘긴다. 기존 트리거(Stop / PreCompact /
|
|
7
|
+
* 다음 SessionStart) 는 Stop 이 안 오는 종료(Ctrl+C, /exit 직후 종료) 와 "마지막 컴팩션 이후
|
|
8
|
+
* 학습" 을 놓쳤다 (memory: forgen-autocompound-gap). runAutoCompound 의 in-flight/cooldown dedup
|
|
9
|
+
* 이 이중 실행을 막는다.
|
|
10
|
+
*
|
|
11
|
+
* - 예산(기본 1.5s, registry timeout 3s 로 상향) 안에 끝나야 하므로: stdin 파싱 → user 메시지 수
|
|
12
|
+
* (앞 200KB 만 읽음, 대용량 transcript 보호) → detached spawn.
|
|
13
|
+
* - Codex 0.153+ 에도 등록한다 (ADR-016 D2). Codex 의 SessionEnd 는 stdin 이
|
|
14
|
+
* `session_id`/`transcript_path`/`cwd`/`reason`(항상 "other") 이고 stdout 을 무시하며 타임아웃을 1~3s 로
|
|
15
|
+
* clamp 한다 — 같은 "bounded count → detached spawn" 경로가 그대로 맞는다. trust 해시가 핸들러 단위라
|
|
16
|
+
* 새 이벤트 추가는 기존 훅의 신뢰를 깨지 않는다 (이 훅 1개만 `/hooks` 승인 필요).
|
|
17
|
+
* - fail-open: 어떤 실패도 종료를 막지 않는다.
|
|
18
|
+
*/
|
|
19
|
+
/** session-recovery 와 동일 임계값 — 짧은 세션은 compound 가치가 없다. */
|
|
20
|
+
export declare const MIN_USER_MESSAGES = 10;
|
|
21
|
+
export interface SessionEndInput {
|
|
22
|
+
session_id?: string;
|
|
23
|
+
transcript_path?: string;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
/** 앞 N바이트만 읽는 상한. critic 2026-10-01: 239MB transcript 전체 스트리밍은 3.2s → 예산 초과로 SIGKILL. */
|
|
28
|
+
export declare const COUNT_SCAN_BYTES: number;
|
|
29
|
+
/**
|
|
30
|
+
* user 메시지 수를 transcript 앞 COUNT_SCAN_BYTES 만 읽어 센다 (session-recovery 와 동일 방식).
|
|
31
|
+
* 임계값(MIN_USER_MESSAGES) 판정에만 쓰이므로 하한 추정으로 충분하다. 큰 transcript 에서도 O(200KB).
|
|
32
|
+
*/
|
|
33
|
+
export declare function countUserMessagesBounded(transcriptPath: string, maxBytes?: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* host 별 user 메시지 수. Codex rollout 은 스키마가 달라 전용 카운터를 쓴다 (ADR-016 D2 — 실제 사용자
|
|
36
|
+
* 프롬프트만, raw 바이트 스캔). codex-adapter 가 delegate 훅에 FORGEN_RUNTIME=codex 를 주입한다.
|
|
37
|
+
*/
|
|
38
|
+
export declare function countSessionUserMessages(transcriptPath: string, runtime?: string | undefined): Promise<number>;
|
|
39
|
+
/** 순수 판정: 이 입력으로 auto-compound 를 띄울지. (테스트 대상) */
|
|
40
|
+
export declare function shouldRunSessionEndCompound(input: SessionEndInput | null, userMessageCount: number): boolean;
|
|
41
|
+
export declare function main(): Promise<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — SessionEnd Hook (ADR-015 C-G6 Claude, ADR-016 D2 Codex)
|
|
4
|
+
*
|
|
5
|
+
* Claude Code `SessionEnd` (reason: clear|resume|logout|prompt_input_exit|other, 기본 예산 1.5s)
|
|
6
|
+
* 에서 이전 세션 transcript 를 auto-compound 러너에 넘긴다. 기존 트리거(Stop / PreCompact /
|
|
7
|
+
* 다음 SessionStart) 는 Stop 이 안 오는 종료(Ctrl+C, /exit 직후 종료) 와 "마지막 컴팩션 이후
|
|
8
|
+
* 학습" 을 놓쳤다 (memory: forgen-autocompound-gap). runAutoCompound 의 in-flight/cooldown dedup
|
|
9
|
+
* 이 이중 실행을 막는다.
|
|
10
|
+
*
|
|
11
|
+
* - 예산(기본 1.5s, registry timeout 3s 로 상향) 안에 끝나야 하므로: stdin 파싱 → user 메시지 수
|
|
12
|
+
* (앞 200KB 만 읽음, 대용량 transcript 보호) → detached spawn.
|
|
13
|
+
* - Codex 0.153+ 에도 등록한다 (ADR-016 D2). Codex 의 SessionEnd 는 stdin 이
|
|
14
|
+
* `session_id`/`transcript_path`/`cwd`/`reason`(항상 "other") 이고 stdout 을 무시하며 타임아웃을 1~3s 로
|
|
15
|
+
* clamp 한다 — 같은 "bounded count → detached spawn" 경로가 그대로 맞는다. trust 해시가 핸들러 단위라
|
|
16
|
+
* 새 이벤트 추가는 기존 훅의 신뢰를 깨지 않는다 (이 훅 1개만 `/hooks` 승인 필요).
|
|
17
|
+
* - fail-open: 어떤 실패도 종료를 막지 않는다.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { createLogger } from '../core/logger.js';
|
|
23
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
24
|
+
import { isHookEnabled } from './hook-config.js';
|
|
25
|
+
import { approve } from './shared/hook-response.js';
|
|
26
|
+
import { recordHookTiming } from './shared/hook-timing.js';
|
|
27
|
+
const log = createLogger('session-end');
|
|
28
|
+
/** session-recovery 와 동일 임계값 — 짧은 세션은 compound 가치가 없다. */
|
|
29
|
+
export const MIN_USER_MESSAGES = 10;
|
|
30
|
+
/** 앞 N바이트만 읽는 상한. critic 2026-10-01: 239MB transcript 전체 스트리밍은 3.2s → 예산 초과로 SIGKILL. */
|
|
31
|
+
export const COUNT_SCAN_BYTES = 200 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* user 메시지 수를 transcript 앞 COUNT_SCAN_BYTES 만 읽어 센다 (session-recovery 와 동일 방식).
|
|
34
|
+
* 임계값(MIN_USER_MESSAGES) 판정에만 쓰이므로 하한 추정으로 충분하다. 큰 transcript 에서도 O(200KB).
|
|
35
|
+
*/
|
|
36
|
+
export function countUserMessagesBounded(transcriptPath, maxBytes = COUNT_SCAN_BYTES) {
|
|
37
|
+
const fd = fs.openSync(transcriptPath, 'r');
|
|
38
|
+
try {
|
|
39
|
+
const buf = Buffer.alloc(maxBytes);
|
|
40
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
41
|
+
const content = buf.toString('utf-8', 0, bytesRead);
|
|
42
|
+
let n = 0;
|
|
43
|
+
for (const line of content.split('\n')) {
|
|
44
|
+
try {
|
|
45
|
+
const t = JSON.parse(line).type;
|
|
46
|
+
if (t === 'user' || t === 'queue-operation')
|
|
47
|
+
n += 1;
|
|
48
|
+
}
|
|
49
|
+
catch { /* partial last line / non-JSON */ }
|
|
50
|
+
}
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
fs.closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* host 별 user 메시지 수. Codex rollout 은 스키마가 달라 전용 카운터를 쓴다 (ADR-016 D2 — 실제 사용자
|
|
59
|
+
* 프롬프트만, raw 바이트 스캔). codex-adapter 가 delegate 훅에 FORGEN_RUNTIME=codex 를 주입한다.
|
|
60
|
+
*/
|
|
61
|
+
export async function countSessionUserMessages(transcriptPath, runtime = process.env.FORGEN_RUNTIME) {
|
|
62
|
+
if (runtime === 'codex') {
|
|
63
|
+
const { countCodexUserPrompts } = await import('../host/codex-rollout.js');
|
|
64
|
+
return countCodexUserPrompts(transcriptPath);
|
|
65
|
+
}
|
|
66
|
+
return countUserMessagesBounded(transcriptPath);
|
|
67
|
+
}
|
|
68
|
+
/** 순수 판정: 이 입력으로 auto-compound 를 띄울지. (테스트 대상) */
|
|
69
|
+
export function shouldRunSessionEndCompound(input, userMessageCount) {
|
|
70
|
+
if (!input || typeof input.transcript_path !== 'string' || input.transcript_path.length === 0)
|
|
71
|
+
return false;
|
|
72
|
+
return userMessageCount >= MIN_USER_MESSAGES;
|
|
73
|
+
}
|
|
74
|
+
export async function main() {
|
|
75
|
+
const start = Date.now();
|
|
76
|
+
try {
|
|
77
|
+
const input = await readStdinJSON();
|
|
78
|
+
if (!isHookEnabled('session-end') || !input) {
|
|
79
|
+
console.log(approve());
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const { runAutoCompound } = await import('../core/spawn.js');
|
|
83
|
+
const transcript = input.transcript_path ?? '';
|
|
84
|
+
let count = 0;
|
|
85
|
+
if (transcript && fs.existsSync(transcript)) {
|
|
86
|
+
try {
|
|
87
|
+
count = await countSessionUserMessages(transcript);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
log.debug('user message count 실패', e);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (shouldRunSessionEndCompound(input, count)) {
|
|
94
|
+
const cwd = input.cwd ?? process.cwd();
|
|
95
|
+
const sessionId = input.session_id ?? path.basename(transcript, '.jsonl');
|
|
96
|
+
const status = runAutoCompound(cwd, transcript, sessionId, count);
|
|
97
|
+
log.debug(`SessionEnd auto-compound: ${status} (reason=${input.reason ?? '?'}, msgs=${count})`);
|
|
98
|
+
}
|
|
99
|
+
console.log(approve());
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
log.debug('session-end 실패 (fail-open)', e);
|
|
103
|
+
console.log(approve());
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
recordHookTiming('session-end', Date.now() - start, 'SessionEnd');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
110
|
+
main().catch(() => {
|
|
111
|
+
console.log(approve());
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -153,13 +153,28 @@ async function main() {
|
|
|
153
153
|
// 이전엔 prepareHarness (fgx/forgen wrapper) 만 호출 → 직접 claude/codex 호출 시
|
|
154
154
|
// ~/.forgen/state/sessions/<id>.json 미생성. SessionStart hook 에서도 호출하여
|
|
155
155
|
// 양쪽 진입 경로 모두에서 session state 박제.
|
|
156
|
+
let v1RenderedRules = null;
|
|
156
157
|
try {
|
|
157
158
|
const { bootstrapV1Session } = await import('../core/v1-bootstrap.js');
|
|
158
|
-
bootstrapV1Session();
|
|
159
|
+
v1RenderedRules = bootstrapV1Session().renderedRules;
|
|
159
160
|
}
|
|
160
161
|
catch (e) {
|
|
161
162
|
log.debug('v1-bootstrap SessionStart 호출 실패 (fail-open)', e);
|
|
162
163
|
}
|
|
164
|
+
// ADR-014 D1 — Codex 에는 .claude/rules/ 로드 표면이 없으므로 같은 룰을 SessionStart
|
|
165
|
+
// additionalContext 로 주입한다 (Claude 세션은 파일 경로로 이미 로드되므로 skip).
|
|
166
|
+
const codexRulesBlock = await (async () => {
|
|
167
|
+
try {
|
|
168
|
+
const { isCodexRuntime, buildCodexRulesContext } = await import('../host/codex-rules-context.js');
|
|
169
|
+
if (!isCodexRuntime())
|
|
170
|
+
return null;
|
|
171
|
+
return await buildCodexRulesContext(sessionContext.cwd, v1RenderedRules);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
log.debug('codex rules inject 실패 (fail-open)', e);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
163
178
|
if (!fs.existsSync(STATE_DIR)) {
|
|
164
179
|
console.log(approve());
|
|
165
180
|
return;
|
|
@@ -435,6 +450,8 @@ async function main() {
|
|
|
435
450
|
catch (e) {
|
|
436
451
|
log.debug('lifecycle check 실패', e);
|
|
437
452
|
}
|
|
453
|
+
if (codexRulesBlock)
|
|
454
|
+
recoveryMessages.unshift(codexRulesBlock);
|
|
438
455
|
if (recoveryMessages.length > 0) {
|
|
439
456
|
console.log(approveWithContext(recoveryMessages.join('\n\n'), 'SessionStart'));
|
|
440
457
|
}
|
|
@@ -16,9 +16,14 @@ const MAX_LINES = 500;
|
|
|
16
16
|
// MAX_LINES × 1.5 여유를 둠.
|
|
17
17
|
const ROTATE_SIZE_BYTES = MAX_LINES * 80 * 2; // ~80KB
|
|
18
18
|
export function recordHookTiming(hookName, durationMs, event) {
|
|
19
|
+
// ADR-015 C-G1 (critic #2): 중첩 추출 run 의 빠른 early-approve 타이밍이 p95 를 왜곡하지 않도록 기록 생략.
|
|
20
|
+
if (process.env.FORGEN_NESTED_RUN === '1')
|
|
21
|
+
return;
|
|
19
22
|
try {
|
|
20
23
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
21
|
-
|
|
24
|
+
// ADR-014 D5 — host 별 발화 구분 (codex-adapter 가 FORGEN_RUNTIME=codex 주입).
|
|
25
|
+
const rt = process.env.FORGEN_RUNTIME === 'codex' ? 'codex' : 'claude';
|
|
26
|
+
const entry = JSON.stringify({ hook: hookName, ms: durationMs, event, at: Date.now(), rt });
|
|
22
27
|
fs.appendFileSync(TIMING_LOG, `${entry}\n`);
|
|
23
28
|
// Rotate if too large — size-gated (statSync only, skip read/write 대부분의 호출)
|
|
24
29
|
try {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import { projectCodexToClaude } from './projection.js';
|
|
15
|
+
import { markCodexHookAlive } from './codex-hook-alive.js';
|
|
15
16
|
function lastJSONObjectFromText(raw) {
|
|
16
17
|
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
17
18
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
@@ -54,6 +55,10 @@ async function main() {
|
|
|
54
55
|
return {};
|
|
55
56
|
}
|
|
56
57
|
})();
|
|
58
|
+
// ADR-016 D1 — "Codex 가 forgen 훅을 실제로 실행 중" 마커 (notify 폴백이 신선도를 본다).
|
|
59
|
+
// forgen 자신의 추출용 중첩 실행은 사용자 세션이 아니므로 제외.
|
|
60
|
+
if (process.env.FORGEN_NESTED_RUN !== '1')
|
|
61
|
+
markCodexHookAlive(input);
|
|
57
62
|
try {
|
|
58
63
|
// 0.4.6 fix — delegate hook 이 자기가 codex runtime context 인지 알 수 있게
|
|
59
64
|
// FORGEN_RUNTIME=codex 명시 주입. 이전엔 buildEnv (forgen wrapper) 경로로만
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex 훅 생존 마커 — ADR-016 D1
|
|
3
|
+
*
|
|
4
|
+
* Codex 는 미승인(untrusted)/변경된(modified) 훅을 조용히 skip 한다. "forgen 훅이 실제로 실행되고 있는가"
|
|
5
|
+
* 를 관측하는 유일한 choke point 는 codex-adapter 다 (Codex 가 forgen 훅을 실행하면 반드시 거친다).
|
|
6
|
+
* 어댑터가 턴 경계 이벤트에서 전역 마커를 갱신하고, notify 폴백(codex-notify)이 그 신선도를 본다.
|
|
7
|
+
*
|
|
8
|
+
* 훅 신뢰 상태는 hooks.json 단위로 모든 세션이 공유하므로 마커는 세션별이 아니라 전역 1개다.
|
|
9
|
+
* 어댑터가 매 훅마다 import 하므로 의존성은 node 내장 + paths 만 둔다.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CODEX_HOOK_ALIVE_PATH: string;
|
|
12
|
+
/** notify 는 Stop 훅 직후 발화한다. Stop 훅 타임아웃(10s) 대비 넉넉한 창. */
|
|
13
|
+
export declare const CODEX_HOOK_ALIVE_WINDOW_MS = 120000;
|
|
14
|
+
export interface CodexHookAliveMarker {
|
|
15
|
+
at: number;
|
|
16
|
+
event: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
}
|
|
19
|
+
/** 어댑터 입력(stdin JSON)으로 마커 갱신. 대상 이벤트가 아니면 no-op. 실패는 삼킨다 (fail-open). */
|
|
20
|
+
export declare function markCodexHookAlive(input: unknown, now?: number): boolean;
|
|
21
|
+
export declare function readCodexHookAlive(): CodexHookAliveMarker | null;
|
|
22
|
+
/** 최근 window 안에 forgen 훅이 Codex 에서 실행됐는가. */
|
|
23
|
+
export declare function isCodexHookAlive(now?: number, windowMs?: number): boolean;
|