@wooojin/forgen 0.4.13 → 0.5.5
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 +261 -0
- package/README.ja.md +16 -12
- package/README.ko.md +12 -8
- package/README.md +90 -27
- package/README.zh.md +16 -12
- package/assets/claude/commands/forge-loop.md +7 -1
- package/assets/claude/commands/ship.md +18 -1
- package/assets/opencode/forgen.ts +52 -0
- package/assets/shared/hook-registry.json +210 -22
- package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
- package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
- package/dist/checks/_shared/model-profile.d.ts +25 -0
- package/dist/checks/_shared/model-profile.js +63 -0
- package/dist/cli.js +99 -153
- package/dist/core/auto-compound-runner.d.ts +0 -11
- package/dist/core/auto-compound-runner.js +199 -92
- package/dist/core/changelog-cli.d.ts +4 -1
- package/dist/core/changelog-cli.js +8 -7
- package/dist/core/compound-consent.d.ts +15 -0
- package/dist/core/compound-consent.js +48 -0
- package/dist/core/compound-sweep-cli.d.ts +60 -0
- package/dist/core/compound-sweep-cli.js +374 -0
- package/dist/core/config-injector.d.ts +16 -1
- package/dist/core/config-injector.js +47 -28
- package/dist/core/dashboard-cli.js +40 -16
- package/dist/core/dashboard.d.ts +3 -4
- package/dist/core/dashboard.js +15 -34
- package/dist/core/dev-cli.d.ts +13 -0
- package/dist/core/dev-cli.js +70 -0
- package/dist/core/doctor.d.ts +16 -5
- package/dist/core/doctor.js +190 -176
- package/dist/core/drift-score.d.ts +2 -0
- package/dist/core/drift-score.js +9 -1
- package/dist/core/harness.js +115 -43
- package/dist/core/health-cli.d.ts +2 -0
- package/dist/core/health-cli.js +6 -1
- package/dist/core/host-detect.d.ts +3 -1
- package/dist/core/host-detect.js +26 -1
- package/dist/core/migrate-cli.js +15 -0
- package/dist/core/migrate-evidence-host.d.ts +2 -1
- package/dist/core/migrate-tenetx.d.ts +50 -0
- package/dist/core/migrate-tenetx.js +262 -0
- package/dist/core/probe-workflow-cli.d.ts +3 -3
- package/dist/core/probe-workflow-cli.js +13 -13
- package/dist/core/recall-cli.js +1 -1
- package/dist/core/regress-map-cli.js +1 -1
- package/dist/core/rendered-rules-manifest.d.ts +29 -0
- package/dist/core/rendered-rules-manifest.js +60 -0
- package/dist/core/session-store.js +14 -3
- package/dist/core/settings-injector.d.ts +3 -0
- package/dist/core/settings-injector.js +12 -16
- package/dist/core/spawn.d.ts +37 -1
- package/dist/core/spawn.js +116 -9
- package/dist/core/state-gc.js +1 -0
- package/dist/core/status-cli.d.ts +20 -0
- package/dist/core/status-cli.js +100 -0
- package/dist/core/statusline-cli.d.ts +7 -0
- package/dist/core/statusline-cli.js +63 -19
- package/dist/core/transcript-summary.d.ts +18 -0
- package/dist/core/transcript-summary.js +81 -0
- package/dist/core/trust-layer-intent.d.ts +21 -1
- package/dist/core/trust-layer-intent.js +7 -0
- package/dist/core/types.d.ts +3 -2
- package/dist/core/uninstall.js +12 -0
- package/dist/core/usage-telemetry.d.ts +7 -1
- package/dist/core/usage-telemetry.js +7 -9
- package/dist/core/v1-bootstrap.js +1 -1
- package/dist/core/watch-cli.js +1 -1
- package/dist/engine/compound-extractor.js +10 -0
- package/dist/engine/compound-loop.js +55 -7
- package/dist/engine/compound-share.d.ts +85 -0
- package/dist/engine/compound-share.js +606 -0
- package/dist/engine/correction-cluster-runner.d.ts +38 -0
- package/dist/engine/correction-cluster-runner.js +188 -0
- package/dist/engine/correction-clustering.d.ts +80 -0
- package/dist/engine/correction-clustering.js +167 -0
- package/dist/engine/enforce-classifier.d.ts +10 -1
- package/dist/engine/enforce-classifier.js +111 -22
- package/dist/engine/extraction-session.js +10 -3
- package/dist/engine/private-filter.d.ts +36 -0
- package/dist/engine/private-filter.js +100 -0
- package/dist/engine/ranking-pipeline.js +4 -2
- package/dist/engine/relevance-gate.d.ts +12 -0
- package/dist/engine/relevance-gate.js +12 -0
- package/dist/engine/roi-demotion.d.ts +79 -0
- package/dist/engine/roi-demotion.js +159 -0
- package/dist/engine/solution-format.d.ts +1 -0
- package/dist/engine/solution-format.js +26 -0
- package/dist/engine/solution-matcher.js +10 -1
- package/dist/fgx.js +7 -6
- package/dist/forge/cli.js +8 -2
- package/dist/hooks/compound-reflection.js +6 -1
- package/dist/hooks/context-guard.d.ts +16 -1
- package/dist/hooks/context-guard.js +99 -49
- package/dist/hooks/hook-config.js +5 -0
- package/dist/hooks/hook-registry.d.ts +6 -1
- package/dist/hooks/hooks-generator.js +3 -0
- package/dist/hooks/post-tool-use.js +2 -3
- package/dist/hooks/pre-compact.js +14 -0
- package/dist/hooks/pre-tool-use.js +5 -1
- package/dist/hooks/session-end.d.ts +34 -0
- package/dist/hooks/session-end.js +100 -0
- package/dist/hooks/session-recovery.js +18 -1
- package/dist/hooks/shared/hook-timing.js +6 -1
- package/dist/hooks/shared/stop-triggers.d.ts +29 -2
- package/dist/hooks/shared/stop-triggers.js +35 -2
- package/dist/hooks/solution-injector.d.ts +28 -0
- package/dist/hooks/solution-injector.js +126 -28
- package/dist/hooks/stop-guard.js +5 -2
- package/dist/hooks/subagent-stop-guard.js +4 -1
- package/dist/host/capabilities-claude.js +1 -0
- package/dist/host/capabilities-codex.js +1 -0
- package/dist/host/capabilities-opencode.d.ts +26 -0
- package/dist/host/capabilities-opencode.js +78 -0
- package/dist/host/capabilities-registry.d.ts +7 -0
- package/dist/host/capabilities-registry.js +14 -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 +14 -3
- package/dist/host/exec-host.js +15 -2
- package/dist/host/host-binding.d.ts +27 -0
- package/dist/host/host-binding.js +11 -0
- package/dist/host/host-runtime.js +18 -0
- package/dist/host/install-codex.d.ts +66 -0
- package/dist/host/install-codex.js +293 -25
- package/dist/host/install-opencode.d.ts +38 -0
- package/dist/host/install-opencode.js +148 -0
- package/dist/host/install-orchestrator.d.ts +4 -1
- package/dist/host/install-orchestrator.js +30 -1
- package/dist/host/invoke-agent.d.ts +3 -2
- package/dist/host/invoke-agent.js +1 -0
- package/dist/host/opencode/context-cli.d.ts +15 -0
- package/dist/host/opencode/context-cli.js +25 -0
- package/dist/host/opencode/guard-cli.d.ts +13 -0
- package/dist/host/opencode/guard-cli.js +39 -0
- package/dist/host/opencode/plugin/forgen.d.ts +31 -0
- package/dist/host/opencode/plugin/forgen.js +60 -0
- package/dist/host/opencode/translate.d.ts +49 -0
- package/dist/host/opencode/translate.js +96 -0
- package/dist/host/parity-harness.d.ts +10 -2
- package/dist/host/parity-harness.js +7 -3
- package/dist/host/projection.d.ts +37 -3
- package/dist/host/projection.js +104 -42
- package/dist/mcp/tools.js +17 -4
- package/dist/store/evidence-store.d.ts +2 -6
- package/dist/store/evidence-store.js +164 -46
- package/dist/store/host-mismatch.d.ts +2 -1
- package/dist/store/host-mismatch.js +8 -8
- package/dist/store/profile-store.d.ts +3 -2
- package/dist/store/rule-store.d.ts +41 -0
- package/dist/store/rule-store.js +77 -2
- package/dist/store/types.d.ts +9 -2
- package/hooks/hooks.json +13 -1
- package/package.json +3 -2
- package/plugin.json +2 -2
- 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 +30 -1
- package/skills/learn/SKILL.md +15 -0
- package/skills/retro/SKILL.md +16 -0
- package/skills/ship/SKILL.md +36 -1
|
@@ -22,6 +22,7 @@ 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
24
|
import { redactSecrets } from './secret-filter.js';
|
|
25
|
+
import { stripPrivate } from '../engine/private-filter.js';
|
|
25
26
|
const log = createLogger('context-guard');
|
|
26
27
|
const CONTEXT_STATE_PATH = path.join(STATE_DIR, 'context-guard.json');
|
|
27
28
|
const PROMPT_HISTORY_PATH = path.join(STATE_DIR, 'prompt-history.jsonl');
|
|
@@ -135,9 +136,14 @@ function appendPromptHistory(sessionId, prompt) {
|
|
|
135
136
|
// ADR-008 critical: secret/PII redaction via secret-filter regex 재사용.
|
|
136
137
|
// password/api-key 가 평문 prompt 에 들어가면 그대로 disk 에 박제되는 위험 차단.
|
|
137
138
|
const safePrompt = redactSecrets(prompt).redacted;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
// W2-5 (private 태그, flow-reviewer SEV-2): prompt-history 는 disk 에 영속되고
|
|
140
|
+
// extraction-session.ts 가 이를 읽어 세션 컨텍스트 추출에 쓴다. 사용자가 프롬프트에
|
|
141
|
+
// 넣은 <private> 범위가 박제·추출입력이 되지 않도록 저장 전에 제거한다(보안 축인
|
|
142
|
+
// redactSecrets 와 별개인 프라이버시 축).
|
|
143
|
+
const privatelessPrompt = stripPrivate(safePrompt).cleaned;
|
|
144
|
+
const truncated = privatelessPrompt.length > PROMPT_HISTORY_TRUNCATE
|
|
145
|
+
? privatelessPrompt.slice(0, PROMPT_HISTORY_TRUNCATE)
|
|
146
|
+
: privatelessPrompt;
|
|
141
147
|
const entry = JSON.stringify({
|
|
142
148
|
timestamp: new Date().toISOString(),
|
|
143
149
|
sessionId,
|
|
@@ -215,8 +221,12 @@ export async function main() {
|
|
|
215
221
|
return;
|
|
216
222
|
}
|
|
217
223
|
const sessionId = input.session_id ?? 'default';
|
|
218
|
-
// Stop
|
|
219
|
-
|
|
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) {
|
|
220
230
|
_hookEvent = 'Stop';
|
|
221
231
|
// 세션 종료 시 pending outcome을 unknown으로 finalize.
|
|
222
232
|
// 과거에는 프로덕션에서 호출되지 않아 pending이 다음 세션의 flushAccept에
|
|
@@ -230,7 +240,7 @@ export async function main() {
|
|
|
230
240
|
log.debug('finalizeSession 실패 (fail-open)', e);
|
|
231
241
|
}
|
|
232
242
|
// forge-loop 활성 시 미완료 스토리 감지 → 지속 메시지 주입 (polite-stop 방지)
|
|
233
|
-
const forgeLoopBlock = checkForgeLoopActive();
|
|
243
|
+
const forgeLoopBlock = checkForgeLoopActive(sessionId);
|
|
234
244
|
if (forgeLoopBlock) {
|
|
235
245
|
console.log(forgeLoopBlock);
|
|
236
246
|
return;
|
|
@@ -283,7 +293,7 @@ export async function main() {
|
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
295
|
// 정상 종료 시: 의미 있는 세션이었으면 compound 안내/자동 트리거
|
|
286
|
-
if (
|
|
296
|
+
if (stopType === 'user' || stopType === 'end_turn') {
|
|
287
297
|
const state = loadContextState(sessionId);
|
|
288
298
|
// ADR-002 T1 — 세션 중간에 교정이 들어와도 session-scoped rule 이 me-scope 으로
|
|
289
299
|
// 승급되도록 Stop 에서 직접 auto-compound-runner 를 debounced 로 트리거.
|
|
@@ -355,11 +365,11 @@ export async function main() {
|
|
|
355
365
|
}
|
|
356
366
|
}
|
|
357
367
|
/**
|
|
358
|
-
* 세션 종료 시
|
|
359
|
-
* solution-cache에서 이번
|
|
360
|
-
*
|
|
368
|
+
* 세션 종료 시 forgen *활동*을 요약 (관찰 — 인과 효과/절약 미주장).
|
|
369
|
+
* solution-cache 에서 이번 세션 주입된 compound 수·상위 솔루션·주입 대화 비율을 보여준다.
|
|
370
|
+
* honest-null (positioning #74): "forgen 없었으면 ~N분 절약" 카운터팩추얼은 은퇴.
|
|
361
371
|
*/
|
|
362
|
-
function buildSessionSummary(sessionId, promptCount) {
|
|
372
|
+
export function buildSessionSummary(sessionId, promptCount) {
|
|
363
373
|
try {
|
|
364
374
|
// P1-S3 fix (2026-04-20): sanitizeId로 path traversal 차단.
|
|
365
375
|
// 다른 세션 캐시 경로는 모두 sanitizeId 사용. 여기만 누락되어 있었다.
|
|
@@ -370,20 +380,20 @@ function buildSessionSummary(sessionId, promptCount) {
|
|
|
370
380
|
const injected = Array.isArray(cache.injected) ? cache.injected : [];
|
|
371
381
|
if (injected.length === 0)
|
|
372
382
|
return '';
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
? `${Math.floor(savedMins / 60)}시간 ${savedMins % 60}분`
|
|
377
|
-
: `${savedMins}분`;
|
|
378
|
-
// 상위 3개 솔루션
|
|
383
|
+
// honest-null (2026-07-21, positioning #74 정합): "절약 시간(forgen 없었으면)"
|
|
384
|
+
// 카운터팩추얼은 은퇴 — 측정된 δ 없이 시간절약을 추정하는 건 날조다. *관찰 가능한*
|
|
385
|
+
// 활동만 보여준다 (무엇이 주입됐나), 인과 효과 크기는 주장하지 않는다.
|
|
379
386
|
const topNames = injected.slice(0, 3).map(i => `"${i.name}"`).join(', ');
|
|
380
387
|
const moreCount = injected.length - 3;
|
|
381
388
|
const topStr = moreCount > 0 ? `${topNames} 외 ${moreCount}개` : topNames;
|
|
389
|
+
// 리뷰 SEV-3: 이전 "주입이 있던 대화 비율 = injected/promptCount × 100" 은 단위
|
|
390
|
+
// 불일치(솔루션수/프롬프트수)라 한 프롬프트 다중 주입 시 >100% 라는 불가능한 %가
|
|
391
|
+
// 났다. injected 캐시엔 프롬프트별 그룹핑이 없으므로 커버리지%를 낼 수 없다 →
|
|
392
|
+
// 오해 없는 *원시 밀도*(N건 / M프롬프트)로 관찰만 보여준다.
|
|
382
393
|
return [
|
|
383
|
-
`\n📊 이번 세션 forgen
|
|
394
|
+
`\n📊 이번 세션 forgen 활동:`,
|
|
384
395
|
` 주입된 compound: ${injected.length}건 (${topStr})`,
|
|
385
|
-
`
|
|
386
|
-
` 프롬프트 대비 효율: ${(injected.length / promptCount * 100).toFixed(0)}% 의 대화가 축적된 지식의 도움을 받음\n`,
|
|
396
|
+
` 세션 규모: ${promptCount} 프롬프트 ${'\x1b[2m'}(주입 밀도 = 주입 ${injected.length}건 / ${promptCount} 프롬프트, 관찰 — 도움 여부는 미측정)${'\x1b[0m'}`,
|
|
387
397
|
].join('\n');
|
|
388
398
|
}
|
|
389
399
|
catch {
|
|
@@ -416,10 +426,18 @@ const AUTO_COMPOUND_BARREN_COOLDOWN_MS = 30 * 60 * 1000; // 30 min if last run e
|
|
|
416
426
|
* 일반 case (추출 있음) 은 5분 cooldown 유지 — adaptive 가 sparsity 시그널을
|
|
417
427
|
* 강화할 뿐 정상 동작 차단 안 함.
|
|
418
428
|
*/
|
|
419
|
-
|
|
429
|
+
// ADR-011 B수정: barren backoff 는 "지난번 0건 추출" 세션의 재추출을 30분 미룬다.
|
|
430
|
+
// 하지만 세션이 그동안 자라났으면(새 프롬프트 ≥ 이 값) 새 학습거리가 생긴 것이므로
|
|
431
|
+
// barren 을 무시하고 정상 쿨다운(5분)으로 재추출한다 — 긴 활성 세션이 굶지 않도록.
|
|
432
|
+
const AUTO_COMPOUND_GROWTH_THRESHOLD = 5;
|
|
433
|
+
export function effectiveCooldownMs(parsed, currentPromptCount = 0) {
|
|
420
434
|
const total = (parsed.extractedSolutions ?? 0) + (parsed.promotedRules ?? 0)
|
|
421
435
|
+ (parsed.userPatternFound ? 1 : 0);
|
|
422
|
-
|
|
436
|
+
if (total !== 0)
|
|
437
|
+
return AUTO_COMPOUND_COOLDOWN_MS;
|
|
438
|
+
// barren: 세션 성장 시 backoff 무시(재추출), 아니면 barren 유지.
|
|
439
|
+
const grew = currentPromptCount - (parsed.promptCount ?? 0) >= AUTO_COMPOUND_GROWTH_THRESHOLD;
|
|
440
|
+
return grew ? AUTO_COMPOUND_COOLDOWN_MS : AUTO_COMPOUND_BARREN_COOLDOWN_MS;
|
|
423
441
|
}
|
|
424
442
|
async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
|
|
425
443
|
if (!transcriptPath || promptCount < 10)
|
|
@@ -430,7 +448,7 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
|
|
|
430
448
|
const parsed = JSON.parse(raw);
|
|
431
449
|
if (parsed.sessionId === sessionId) {
|
|
432
450
|
const last = parsed.completedAt ? Date.parse(parsed.completedAt) : 0;
|
|
433
|
-
const cooldown = effectiveCooldownMs(parsed);
|
|
451
|
+
const cooldown = effectiveCooldownMs(parsed, promptCount);
|
|
434
452
|
if (Number.isFinite(last) && Date.now() - last < cooldown)
|
|
435
453
|
return;
|
|
436
454
|
}
|
|
@@ -463,43 +481,80 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
|
|
|
463
481
|
else if (override) {
|
|
464
482
|
log.debug('FORGEN_AUTO_COMPOUND_RUNNER_PATH 무시 — FORGEN_TEST=1 가 필요');
|
|
465
483
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
484
|
+
// 리뷰 SEV-2 #3: runAutoCompound 와 공유하는 in-flight start-gate 로 이중 spawn 방지.
|
|
485
|
+
// (이 경로는 test-override 러너를 쓰므로 runAutoCompound 를 직접 호출하지 않고 게이트만 공유.)
|
|
486
|
+
const { claimAutoCompoundInflight, releaseAutoCompoundInflight } = await import('../core/spawn.js');
|
|
487
|
+
if (!claimAutoCompoundInflight(sessionId)) {
|
|
488
|
+
log.debug('Stop-triggered auto-compound skip: 세션 in-flight');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const child = spawnProcess('node', [runnerPath, cwd, transcriptPath, sessionId, String(promptCount)], {
|
|
493
|
+
detached: true,
|
|
494
|
+
stdio: 'ignore',
|
|
495
|
+
});
|
|
496
|
+
child.unref();
|
|
497
|
+
log.debug(`Stop-triggered auto-compound 시작: ${sessionId} (${promptCount} prompts)`);
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
releaseAutoCompoundInflight(sessionId); // spawn 실패 → 재시도 가능
|
|
501
|
+
log.debug('Stop-triggered auto-compound spawn 실패', e);
|
|
502
|
+
}
|
|
472
503
|
}
|
|
473
504
|
// forge-loop 차단 안전 상한 (무한 루프 방지)
|
|
474
505
|
const FORGE_LOOP_MAX_BLOCKS = 30;
|
|
475
|
-
|
|
506
|
+
// TTL: 24시간. 과거 2시간은 정상적으로 오래 걸리는 forge-loop 세션까지 중도
|
|
507
|
+
// 해제시켰다. "크래시된 세션의 잔여 active:true 가 무관한 후속 세션을 영구
|
|
508
|
+
// 차단"하는 문제는 이 TTL 이 아니라 아래 sessionId 바인딩으로 별도 해결한다 —
|
|
509
|
+
// TTL 은 진짜로 죽은(재개 안 되는) 세션을 회수하기 위한 상한일 뿐이다.
|
|
510
|
+
const FORGE_LOOP_STALE_MS = 24 * 60 * 60 * 1000;
|
|
476
511
|
/**
|
|
477
512
|
* forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
|
|
478
513
|
* OMC의 persistent-mode.cjs 패턴 참고.
|
|
514
|
+
*
|
|
515
|
+
* @param sessionId 호출한 Stop hook 의 세션 ID. 미지정 시(레거시 호출) 세션
|
|
516
|
+
* 바인딩을 건너뛰고 기존처럼 전역으로 평가한다.
|
|
479
517
|
*/
|
|
480
|
-
export function checkForgeLoopActive() {
|
|
518
|
+
export function checkForgeLoopActive(sessionId) {
|
|
481
519
|
try {
|
|
482
520
|
if (!fs.existsSync(FORGE_LOOP_STATE_PATH))
|
|
483
521
|
return null;
|
|
484
522
|
const state = JSON.parse(fs.readFileSync(FORGE_LOOP_STATE_PATH, 'utf-8'));
|
|
485
523
|
if (!state.active)
|
|
486
524
|
return null;
|
|
487
|
-
//
|
|
525
|
+
// 세션 바인딩: 이미 다른 세션에 귀속된 루프라면 이 세션과 무관 — 손대지 않고 통과.
|
|
526
|
+
// 크래시된 세션이 남긴 active:true 가 이후 전혀 다른 세션을 영구 차단하는 것을 방지.
|
|
527
|
+
if (state.sessionId && sessionId && state.sessionId !== sessionId) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
// 최초 차단 후보 세션에 자동 귀속 (스킬은 세션 ID를 알 필요 없음).
|
|
531
|
+
if (!state.sessionId && sessionId) {
|
|
532
|
+
state.sessionId = sessionId;
|
|
533
|
+
}
|
|
534
|
+
// 사용자 명시 우회 — 이번 턴만 통과시키고 루프 상태(active/blockCount)는 보존.
|
|
535
|
+
// pre-tool-use/stop-guard 와 동일한 FORGEN_USER_CONFIRMED=1 관용구.
|
|
536
|
+
if (process.env.FORGEN_USER_CONFIRMED === '1')
|
|
537
|
+
return null;
|
|
538
|
+
// Stale 감지: TTL(24h) 초과 → 자동 비활성화 + 1회성 안내 (침묵 해제 금지).
|
|
488
539
|
const startedAt = new Date(state.startedAt).getTime();
|
|
489
540
|
if (Number.isFinite(startedAt) && Date.now() - startedAt > FORGE_LOOP_STALE_MS) {
|
|
490
541
|
state.active = false;
|
|
491
542
|
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
492
|
-
return
|
|
543
|
+
return approveWithWarning(`[Forgen] forge-loop 상태가 ${Math.round(FORGE_LOOP_STALE_MS / 3_600_000)}시간 이상 갱신되지 않아 자동 해제되었습니다. ` +
|
|
544
|
+
`재개하려면 "/forge-loop resume" 를 입력하세요.`);
|
|
493
545
|
}
|
|
494
|
-
// 확인 대기 중이면 차단하지 않음 (사용자 개입 허용)
|
|
495
|
-
if (state.awaitingConfirmation)
|
|
546
|
+
// 확인 대기 중이면 차단하지 않음 (사용자 개입 허용) — 귀속은 반영 후 통과.
|
|
547
|
+
if (state.awaitingConfirmation) {
|
|
548
|
+
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
496
549
|
return null;
|
|
497
|
-
|
|
550
|
+
}
|
|
551
|
+
// 안전 상한: 30회 이상 연속 차단 시 무한 루프로 간주하여 해제 + 1회성 안내.
|
|
498
552
|
const blockCount = state.blockCount ?? 0;
|
|
499
553
|
if (blockCount >= FORGE_LOOP_MAX_BLOCKS) {
|
|
500
554
|
state.active = false;
|
|
501
555
|
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
502
|
-
return
|
|
556
|
+
return approveWithWarning(`[Forgen] forge-loop 이 연속 ${FORGE_LOOP_MAX_BLOCKS}회 차단되어 안전 상한으로 자동 해제되었습니다. ` +
|
|
557
|
+
`무한 루프 가능성을 점검한 뒤 필요 시 "/forge-loop resume" 로 재개하세요.`);
|
|
503
558
|
}
|
|
504
559
|
// 미완료 스토리 확인
|
|
505
560
|
const stories = Array.isArray(state.stories) ? state.stories : [];
|
|
@@ -515,25 +570,20 @@ export function checkForgeLoopActive() {
|
|
|
515
570
|
state.lastBlockAt = new Date().toISOString();
|
|
516
571
|
atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
|
|
517
572
|
const nextStory = pending[0];
|
|
518
|
-
const
|
|
573
|
+
const firstAC = Array.isArray(nextStory.acceptanceCriteria) ? nextStory.acceptanceCriteria[0] : undefined;
|
|
574
|
+
const lines = [
|
|
519
575
|
`<forgen-forge-loop iteration="${state.blockCount}/${FORGE_LOOP_MAX_BLOCKS}">`,
|
|
520
576
|
`[FORGE-LOOP] ${pending.length}개 스토리가 미완료입니다.`,
|
|
521
577
|
`현재 스토리: ${nextStory.id} — ${nextStory.title}`,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
` 3. Context limit 접근 (handoff)`,
|
|
527
|
-
``,
|
|
528
|
-
`중간 "완료했습니다" 보고는 polite-stop anti-pattern입니다.`,
|
|
529
|
-
`취소하려면: "/forge-loop cancel" 또는 "cancelforgen" 입력`,
|
|
530
|
-
`</forgen-forge-loop>`,
|
|
531
|
-
].join('\n');
|
|
578
|
+
];
|
|
579
|
+
if (firstAC)
|
|
580
|
+
lines.push(`AC1: ${firstAC}`);
|
|
581
|
+
lines.push(``, `계속 진행하세요. 보고는 다음 시점에만 합니다:`, ` 1. 모든 스토리 완료 (최종 리포트)`, ` 2. 3회 실패 (에스컬레이션)`, ` 3. Context limit 접근 (handoff)`, ``, `중간 "완료했습니다" 보고는 polite-stop anti-pattern입니다.`, `취소하려면: "/forge-loop cancel" 또는 "cancelforgen" 입력`, `</forgen-forge-loop>`);
|
|
532
582
|
// block 결정으로 Claude가 계속 작업하도록 강제
|
|
533
583
|
return JSON.stringify({
|
|
534
584
|
continue: true,
|
|
535
585
|
decision: 'block',
|
|
536
|
-
reason:
|
|
586
|
+
reason: lines.join('\n'),
|
|
537
587
|
});
|
|
538
588
|
}
|
|
539
589
|
catch (e) {
|
|
@@ -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,11 @@ 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'>;
|
|
30
35
|
}
|
|
31
36
|
export declare const HOOK_REGISTRY: HookEntry[];
|
|
32
37
|
/** 티어별 훅 목록 조회 */
|
|
@@ -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;
|
|
@@ -21,7 +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
|
-
|
|
24
|
+
// ADR-010 W2-2: usage-telemetry 기록 제거 — native /usage 이관. (hot path I/O 절감)
|
|
25
25
|
import { emitSolutionEvent, querySurfacedWithin } from '../core/observability-store.js';
|
|
26
26
|
import { parseSolutionV3 } from '../engine/solution-format.js';
|
|
27
27
|
import { ME_SOLUTIONS } from '../core/paths.js';
|
|
@@ -122,8 +122,7 @@ async function main() {
|
|
|
122
122
|
console.log(approve());
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
|
-
// ADR-
|
|
126
|
-
recordToolCall(process.env.FORGEN_RUNTIME ?? 'claude');
|
|
125
|
+
// (ADR-010 W2-2: usage-telemetry 기록 제거됨 — native /usage 이관)
|
|
127
126
|
const toolName = data.tool_name ?? data.toolName ?? '';
|
|
128
127
|
const toolInput = data.tool_input ?? data.toolInput ?? {};
|
|
129
128
|
// tool_response 는 string / object / array 모두 가능 (sub-agent 결과는 object 가 흔함).
|
|
@@ -179,6 +179,20 @@ async function main() {
|
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
const sessionId = data.session_id ?? 'default';
|
|
182
|
+
// ADR-011 B수정: 프롬프트 주입(모델이 추출)에 더해, **러너도 spawn** 해서 컴팩션으로
|
|
183
|
+
// 컨텍스트가 사라지기 직전에 확실히 추출한다. runAutoCompound 의 in-flight/cooldown
|
|
184
|
+
// dedup 이 Stop 훅과의 이중실행을 막는다(최근에 이미 돌았으면 skip).
|
|
185
|
+
const transcriptPath = data.transcript_path;
|
|
186
|
+
if (transcriptPath) {
|
|
187
|
+
try {
|
|
188
|
+
const { runAutoCompound } = await import('../core/spawn.js');
|
|
189
|
+
const cwd = data.cwd ?? process.cwd();
|
|
190
|
+
runAutoCompound(cwd, transcriptPath, sessionId);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
log.debug('PreCompact auto-compound spawn 실패 (프롬프트 주입은 계속)', e);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
182
196
|
// 오래된 handoff 정리
|
|
183
197
|
cleanOldHandoffs();
|
|
184
198
|
// 기존 behavioral 패턴 목록 로드 (중복 방지)
|
|
@@ -18,6 +18,7 @@ import { withFileLockSync, FileLockError } from './shared/file-lock.js';
|
|
|
18
18
|
import { sanitizeId } from './shared/sanitize-id.js';
|
|
19
19
|
import { incrementEvidence } from '../engine/solution-writer.js';
|
|
20
20
|
import { isReflectionCandidate } from './compound-reflection.js';
|
|
21
|
+
import { stripPrivate } from '../engine/private-filter.js';
|
|
21
22
|
import { isHookEnabled } from './hook-config.js';
|
|
22
23
|
import { approve, approveWithWarning, denyOrObserve, failOpenWithTracking } from './shared/hook-response.js';
|
|
23
24
|
import { FORGEN_HOME, STATE_DIR } from '../core/paths.js';
|
|
@@ -229,7 +230,10 @@ function resetFailCount() {
|
|
|
229
230
|
function checkCompoundReflection(toolName, toolInput, sessionId) {
|
|
230
231
|
if (toolName !== 'Edit' && toolName !== 'Write')
|
|
231
232
|
return;
|
|
232
|
-
|
|
233
|
+
// W2-5 (private 태그): 사용자가 <private> 로 표시한 코드 범위는 reflection 관측
|
|
234
|
+
// 대상에서 제외한다. isReflectionCandidate 내부에서도 strip 하지만, 아래 태그
|
|
235
|
+
// fallback 매칭은 이 code 를 직접 쓰므로 여기서 한 번 스트립해 두 경로를 일치시킨다.
|
|
236
|
+
const code = stripPrivate(String(toolInput.new_string ?? toolInput.content ?? '')).cleaned;
|
|
233
237
|
if (!code || code.length < 10)
|
|
234
238
|
return;
|
|
235
239
|
const cachePath = path.join(STATE_DIR, `injection-cache-${sanitizeId(sessionId)}.json`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — SessionEnd Hook (ADR-015 C-G6, Claude Code 전용)
|
|
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 에는 등록하지 않는다 (hooks.json 바이트 동일성 = 훅 신뢰 유지). Codex 의 SessionEnd 는
|
|
14
|
+
* hooks.json 변경이 필요한 다음 메이저에서 함께 추가.
|
|
15
|
+
* - fail-open: 어떤 실패도 종료를 막지 않는다.
|
|
16
|
+
*/
|
|
17
|
+
/** session-recovery 와 동일 임계값 — 짧은 세션은 compound 가치가 없다. */
|
|
18
|
+
export declare const MIN_USER_MESSAGES = 10;
|
|
19
|
+
export interface SessionEndInput {
|
|
20
|
+
session_id?: string;
|
|
21
|
+
transcript_path?: string;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
/** 앞 N바이트만 읽는 상한. critic 2026-10-01: 239MB transcript 전체 스트리밍은 3.2s → 예산 초과로 SIGKILL. */
|
|
26
|
+
export declare const COUNT_SCAN_BYTES: number;
|
|
27
|
+
/**
|
|
28
|
+
* user 메시지 수를 transcript 앞 COUNT_SCAN_BYTES 만 읽어 센다 (session-recovery 와 동일 방식).
|
|
29
|
+
* 임계값(MIN_USER_MESSAGES) 판정에만 쓰이므로 하한 추정으로 충분하다. 큰 transcript 에서도 O(200KB).
|
|
30
|
+
*/
|
|
31
|
+
export declare function countUserMessagesBounded(transcriptPath: string, maxBytes?: number): number;
|
|
32
|
+
/** 순수 판정: 이 입력으로 auto-compound 를 띄울지. (테스트 대상) */
|
|
33
|
+
export declare function shouldRunSessionEndCompound(input: SessionEndInput | null, userMessageCount: number): boolean;
|
|
34
|
+
export declare function main(): Promise<void>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — SessionEnd Hook (ADR-015 C-G6, Claude Code 전용)
|
|
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 에는 등록하지 않는다 (hooks.json 바이트 동일성 = 훅 신뢰 유지). Codex 의 SessionEnd 는
|
|
14
|
+
* hooks.json 변경이 필요한 다음 메이저에서 함께 추가.
|
|
15
|
+
* - fail-open: 어떤 실패도 종료를 막지 않는다.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { createLogger } from '../core/logger.js';
|
|
21
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
22
|
+
import { isHookEnabled } from './hook-config.js';
|
|
23
|
+
import { approve } from './shared/hook-response.js';
|
|
24
|
+
import { recordHookTiming } from './shared/hook-timing.js';
|
|
25
|
+
const log = createLogger('session-end');
|
|
26
|
+
/** session-recovery 와 동일 임계값 — 짧은 세션은 compound 가치가 없다. */
|
|
27
|
+
export const MIN_USER_MESSAGES = 10;
|
|
28
|
+
/** 앞 N바이트만 읽는 상한. critic 2026-10-01: 239MB transcript 전체 스트리밍은 3.2s → 예산 초과로 SIGKILL. */
|
|
29
|
+
export const COUNT_SCAN_BYTES = 200 * 1024;
|
|
30
|
+
/**
|
|
31
|
+
* user 메시지 수를 transcript 앞 COUNT_SCAN_BYTES 만 읽어 센다 (session-recovery 와 동일 방식).
|
|
32
|
+
* 임계값(MIN_USER_MESSAGES) 판정에만 쓰이므로 하한 추정으로 충분하다. 큰 transcript 에서도 O(200KB).
|
|
33
|
+
*/
|
|
34
|
+
export function countUserMessagesBounded(transcriptPath, maxBytes = COUNT_SCAN_BYTES) {
|
|
35
|
+
const fd = fs.openSync(transcriptPath, 'r');
|
|
36
|
+
try {
|
|
37
|
+
const buf = Buffer.alloc(maxBytes);
|
|
38
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
39
|
+
const content = buf.toString('utf-8', 0, bytesRead);
|
|
40
|
+
let n = 0;
|
|
41
|
+
for (const line of content.split('\n')) {
|
|
42
|
+
try {
|
|
43
|
+
const t = JSON.parse(line).type;
|
|
44
|
+
if (t === 'user' || t === 'queue-operation')
|
|
45
|
+
n += 1;
|
|
46
|
+
}
|
|
47
|
+
catch { /* partial last line / non-JSON */ }
|
|
48
|
+
}
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
fs.closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** 순수 판정: 이 입력으로 auto-compound 를 띄울지. (테스트 대상) */
|
|
56
|
+
export function shouldRunSessionEndCompound(input, userMessageCount) {
|
|
57
|
+
if (!input || typeof input.transcript_path !== 'string' || input.transcript_path.length === 0)
|
|
58
|
+
return false;
|
|
59
|
+
return userMessageCount >= MIN_USER_MESSAGES;
|
|
60
|
+
}
|
|
61
|
+
export async function main() {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
try {
|
|
64
|
+
const input = await readStdinJSON();
|
|
65
|
+
if (!isHookEnabled('session-end') || !input) {
|
|
66
|
+
console.log(approve());
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { runAutoCompound } = await import('../core/spawn.js');
|
|
70
|
+
const transcript = input.transcript_path ?? '';
|
|
71
|
+
let count = 0;
|
|
72
|
+
if (transcript && fs.existsSync(transcript)) {
|
|
73
|
+
try {
|
|
74
|
+
count = countUserMessagesBounded(transcript);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
log.debug('user message count 실패', e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (shouldRunSessionEndCompound(input, count)) {
|
|
81
|
+
const cwd = input.cwd ?? process.cwd();
|
|
82
|
+
const sessionId = input.session_id ?? path.basename(transcript, '.jsonl');
|
|
83
|
+
const status = runAutoCompound(cwd, transcript, sessionId, count);
|
|
84
|
+
log.debug(`SessionEnd auto-compound: ${status} (reason=${input.reason ?? '?'}, msgs=${count})`);
|
|
85
|
+
}
|
|
86
|
+
console.log(approve());
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
log.debug('session-end 실패 (fail-open)', e);
|
|
90
|
+
console.log(approve());
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
recordHookTiming('session-end', Date.now() - start, 'SessionEnd');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
|
97
|
+
main().catch(() => {
|
|
98
|
+
console.log(approve());
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -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 {
|
|
@@ -5,11 +5,21 @@
|
|
|
5
5
|
* 단일 소스로 통합. 한쪽만 고치면 다른 쪽이 drift 하는 sibling-bug 패턴 차단.
|
|
6
6
|
*
|
|
7
7
|
* 설계 결정:
|
|
8
|
-
* - trigger 는 명시적 완료 선언 동사/어미만 — "완료" 단독 매칭 금지 (retraction 오매칭 방지).
|
|
8
|
+
* - DEFAULT trigger 는 명시적 완료 선언 동사/어미만 — "완료" 단독 매칭 금지 (retraction 오매칭 방지).
|
|
9
9
|
* - exclude 는 retraction/negation/meta 언급 광범위 차단.
|
|
10
10
|
* - A1 spike 결과로 검증됨 (10/10 scenarios pass, FP 0%).
|
|
11
|
+
*
|
|
12
|
+
* 2026-07-22 (강제층 실측 갭 — critic-review 룰 한정 수정, 리뷰 SEV-2 반영):
|
|
13
|
+
* 완료 키워드 없이 "리뷰 생략하고 다음으로 넘어감" 하는 응답이 완료-키워드 트리거를
|
|
14
|
+
* 우회해 critic-룰(청크마다 리뷰) 강제가 새던 갭. 이를 닫되:
|
|
15
|
+
* - skip-signal 은 DEFAULT 에 섞지 않고 **critic-review 룰 전용** CRITIC_STOP_TRIGGER 로 분리
|
|
16
|
+
* (e2e·mock-as-proof 등 다른 완료룰 오염 방지, 리뷰 SEV-2 #3).
|
|
17
|
+
* - "리뷰 생략" AND "다음으로 넘어감" **결합(conjunction)** 으로만 발화 → 경고/질문/신중
|
|
18
|
+
* 응답 FP 제거 (리뷰 SEV-2 #1).
|
|
19
|
+
* - exclude 에 부정/금지/질문형(말고·안 했·should not·될까요…) 보강 (리뷰 SEV-2 #2).
|
|
20
|
+
* - 기존 baked 룰(b0aabac3)은 `forgen rule migrate-triggers` 로 재-bake 해야 적용 (리뷰 SEV-2 #4).
|
|
11
21
|
*/
|
|
12
|
-
/** Stop hook
|
|
22
|
+
/** Stop hook 기본 완료 선언 매칭 (완료 동사/어미만 — skip-signal 미포함). */
|
|
13
23
|
export declare const DEFAULT_STOP_TRIGGER_RE = "(\uC644\uB8CC\uD588|\uC644\uC131\uB410|\uC644\uC131\uB418|\uC644\uC131\uD588|done\\.|ready\\.|shipped\\.|LGTM|finished\\.)";
|
|
14
24
|
/** Stop hook 기본 exclude — retraction/negation/meta 맥락 제외. */
|
|
15
25
|
export declare const DEFAULT_STOP_EXCLUDE_RE = "(\uCDE8\uC18C|\uCCA0\uD68C|\uC5C6\uC74C|\uC5C6\uC2B5\uB2C8\uB2E4|\uC54A\uC558|\uD558\uC9C0\\s*\uC54A|\uC544\uB2D9\uB2C8\uB2E4|not\\s*yet|no\\s*longer|retract|withdraw|\uC544\uC9C1\\s*(\uC548|\uC544))";
|
|
@@ -17,3 +27,20 @@ export declare const DEFAULT_STOP_EXCLUDE_RE = "(\uCDE8\uC18C|\uCCA0\uD68C|\uC5C
|
|
|
17
27
|
export declare const MOCK_TRIGGER_RE = "(mock|stub|fake)";
|
|
18
28
|
/** mock trigger 의 exclude — 테스트 맥락은 정상. */
|
|
19
29
|
export declare const MOCK_EXCLUDE_RE = "(\uD14C\uC2A4\uD2B8|test|vi\\.mock|jest\\.mock|spec\\.)";
|
|
30
|
+
/**
|
|
31
|
+
* "리뷰 생략" AND "다음으로 넘어감" 이 함께 있을 때만 발화하는 결합 트리거.
|
|
32
|
+
* 두 lookahead 로 conjunction — 한쪽만 있는 경고/질문/신중 응답은 미발화(FP 0).
|
|
33
|
+
*/
|
|
34
|
+
export declare const SKIP_REVIEW_TRIGGER_RE = "(?=[\\s\\S]*?(?:(\uB9AC\uBDF0|\uAC80\uD1A0)[^.!?\\n]{0,8}(\uC0DD\uB7B5|\uAC74\uB108\uB6F0|\uC5C6\uC774|\uC2A4\uD0B5|\uD328\uC2A4|\uC548\\s*\uD558\uACE0)|skip(?:ping|s)?\\s+(?:the\\s+)?review))(?=[\\s\\S]*?(?:\uB118\uC5B4\uAC00|\uB118\uC5B4\uAC11|\uB118\uC5B4\uAC14|\uB118\uC5B4\uAC10|\uB118\uC5B4\uAC08|\uB2E4\uC74C\\s*(\uC791\uC5C5|\uAE30\uB2A5|\uB2E8\uACC4|\uC2A4\uD15D|\uAC83|\uC774\uD130\uB808\uC774\uC158)|move\\s+on|next\\s+(step|task|feature)|proceed))";
|
|
35
|
+
/**
|
|
36
|
+
* critic-review 룰 트리거 = 완료 선언 OR 리뷰생략-넘어감.
|
|
37
|
+
* (완료 시점 + skip 시점 양쪽에서 critic 강제. e2e/mock 룰엔 부여 안 함 — DEFAULT 만 사용.)
|
|
38
|
+
*/
|
|
39
|
+
export declare const CRITIC_STOP_TRIGGER_RE = "((\uC644\uB8CC\uD588|\uC644\uC131\uB410|\uC644\uC131\uB418|\uC644\uC131\uD588|done\\.|ready\\.|shipped\\.|LGTM|finished\\.)|(?=[\\s\\S]*?(?:(\uB9AC\uBDF0|\uAC80\uD1A0)[^.!?\\n]{0,8}(\uC0DD\uB7B5|\uAC74\uB108\uB6F0|\uC5C6\uC774|\uC2A4\uD0B5|\uD328\uC2A4|\uC548\\s*\uD558\uACE0)|skip(?:ping|s)?\\s+(?:the\\s+)?review))(?=[\\s\\S]*?(?:\uB118\uC5B4\uAC00|\uB118\uC5B4\uAC11|\uB118\uC5B4\uAC14|\uB118\uC5B4\uAC10|\uB118\uC5B4\uAC08|\uB2E4\uC74C\\s*(\uC791\uC5C5|\uAE30\uB2A5|\uB2E8\uACC4|\uC2A4\uD15D|\uAC83|\uC774\uD130\uB808\uC774\uC158)|move\\s+on|next\\s+(step|task|feature)|proceed)))";
|
|
40
|
+
/**
|
|
41
|
+
* critic 트리거 exclude — 기본 retraction + 부정/금지/질문형 + 숙고형 보강.
|
|
42
|
+
* 주의: bare `안\s*하` 는 넣지 않는다 — "리뷰 안 하고 넘어감"(실제 skip TP)을 죽이므로.
|
|
43
|
+
* retraction 은 과거형 `안\s*했`(안 했다)만 배제. 숙고형(할지|여부|고민…)은 skip 단언이
|
|
44
|
+
* 아니라 결정 전 단계라 배제 (리뷰 SEV-3 (a)).
|
|
45
|
+
*/
|
|
46
|
+
export declare const CRITIC_STOP_EXCLUDE_RE: string;
|