@wooojin/forgen 0.4.13 → 0.5.0

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.
Files changed (139) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +94 -0
  3. package/README.ja.md +4 -4
  4. package/README.md +89 -27
  5. package/README.zh.md +4 -4
  6. package/assets/claude/commands/forge-loop.md +7 -1
  7. package/assets/claude/commands/ship.md +18 -1
  8. package/assets/opencode/forgen.ts +52 -0
  9. package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
  10. package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
  11. package/dist/checks/_shared/model-profile.d.ts +25 -0
  12. package/dist/checks/_shared/model-profile.js +63 -0
  13. package/dist/cli.js +99 -153
  14. package/dist/core/auto-compound-runner.d.ts +0 -11
  15. package/dist/core/auto-compound-runner.js +127 -79
  16. package/dist/core/compound-consent.d.ts +15 -0
  17. package/dist/core/compound-consent.js +48 -0
  18. package/dist/core/compound-sweep-cli.d.ts +57 -0
  19. package/dist/core/compound-sweep-cli.js +354 -0
  20. package/dist/core/config-injector.d.ts +16 -1
  21. package/dist/core/config-injector.js +47 -28
  22. package/dist/core/dashboard-cli.js +40 -16
  23. package/dist/core/dashboard.d.ts +3 -4
  24. package/dist/core/dashboard.js +15 -34
  25. package/dist/core/dev-cli.d.ts +13 -0
  26. package/dist/core/dev-cli.js +70 -0
  27. package/dist/core/doctor.d.ts +16 -5
  28. package/dist/core/doctor.js +161 -176
  29. package/dist/core/drift-score.d.ts +2 -0
  30. package/dist/core/drift-score.js +9 -1
  31. package/dist/core/harness.js +115 -43
  32. package/dist/core/health-cli.d.ts +2 -0
  33. package/dist/core/health-cli.js +6 -1
  34. package/dist/core/host-detect.d.ts +3 -1
  35. package/dist/core/host-detect.js +26 -1
  36. package/dist/core/migrate-cli.js +15 -0
  37. package/dist/core/migrate-evidence-host.d.ts +2 -1
  38. package/dist/core/migrate-tenetx.d.ts +50 -0
  39. package/dist/core/migrate-tenetx.js +262 -0
  40. package/dist/core/probe-workflow-cli.d.ts +3 -3
  41. package/dist/core/probe-workflow-cli.js +13 -13
  42. package/dist/core/recall-cli.js +1 -1
  43. package/dist/core/regress-map-cli.js +1 -1
  44. package/dist/core/rendered-rules-manifest.d.ts +29 -0
  45. package/dist/core/rendered-rules-manifest.js +60 -0
  46. package/dist/core/session-store.js +14 -3
  47. package/dist/core/settings-injector.d.ts +3 -0
  48. package/dist/core/settings-injector.js +12 -16
  49. package/dist/core/spawn.d.ts +11 -1
  50. package/dist/core/spawn.js +79 -7
  51. package/dist/core/state-gc.js +1 -0
  52. package/dist/core/status-cli.d.ts +20 -0
  53. package/dist/core/status-cli.js +100 -0
  54. package/dist/core/statusline-cli.d.ts +7 -0
  55. package/dist/core/statusline-cli.js +63 -19
  56. package/dist/core/transcript-summary.d.ts +18 -0
  57. package/dist/core/transcript-summary.js +81 -0
  58. package/dist/core/trust-layer-intent.d.ts +21 -1
  59. package/dist/core/trust-layer-intent.js +7 -0
  60. package/dist/core/types.d.ts +3 -2
  61. package/dist/core/uninstall.js +12 -0
  62. package/dist/core/usage-telemetry.d.ts +7 -1
  63. package/dist/core/usage-telemetry.js +7 -9
  64. package/dist/core/v1-bootstrap.js +1 -1
  65. package/dist/core/watch-cli.js +1 -1
  66. package/dist/engine/compound-extractor.js +10 -0
  67. package/dist/engine/compound-loop.js +35 -6
  68. package/dist/engine/compound-share.d.ts +85 -0
  69. package/dist/engine/compound-share.js +606 -0
  70. package/dist/engine/correction-cluster-runner.d.ts +38 -0
  71. package/dist/engine/correction-cluster-runner.js +188 -0
  72. package/dist/engine/correction-clustering.d.ts +80 -0
  73. package/dist/engine/correction-clustering.js +167 -0
  74. package/dist/engine/enforce-classifier.d.ts +10 -1
  75. package/dist/engine/enforce-classifier.js +111 -22
  76. package/dist/engine/extraction-session.js +10 -3
  77. package/dist/engine/private-filter.d.ts +36 -0
  78. package/dist/engine/private-filter.js +100 -0
  79. package/dist/engine/ranking-pipeline.js +4 -2
  80. package/dist/engine/relevance-gate.d.ts +12 -0
  81. package/dist/engine/relevance-gate.js +12 -0
  82. package/dist/engine/roi-demotion.d.ts +79 -0
  83. package/dist/engine/roi-demotion.js +159 -0
  84. package/dist/engine/solution-format.d.ts +1 -0
  85. package/dist/engine/solution-format.js +26 -0
  86. package/dist/engine/solution-matcher.js +10 -1
  87. package/dist/fgx.js +7 -6
  88. package/dist/forge/cli.js +8 -2
  89. package/dist/hooks/compound-reflection.js +6 -1
  90. package/dist/hooks/context-guard.d.ts +16 -1
  91. package/dist/hooks/context-guard.js +92 -46
  92. package/dist/hooks/post-tool-use.js +2 -3
  93. package/dist/hooks/pre-compact.js +14 -0
  94. package/dist/hooks/pre-tool-use.js +5 -1
  95. package/dist/hooks/shared/stop-triggers.d.ts +29 -2
  96. package/dist/hooks/shared/stop-triggers.js +35 -2
  97. package/dist/hooks/solution-injector.d.ts +28 -0
  98. package/dist/hooks/solution-injector.js +126 -28
  99. package/dist/hooks/stop-guard.js +5 -2
  100. package/dist/hooks/subagent-stop-guard.js +4 -1
  101. package/dist/host/capabilities-claude.js +1 -0
  102. package/dist/host/capabilities-codex.js +1 -0
  103. package/dist/host/capabilities-opencode.d.ts +26 -0
  104. package/dist/host/capabilities-opencode.js +78 -0
  105. package/dist/host/capabilities-registry.d.ts +7 -0
  106. package/dist/host/capabilities-registry.js +14 -0
  107. package/dist/host/exec-host.d.ts +4 -3
  108. package/dist/host/exec-host.js +2 -0
  109. package/dist/host/host-binding.d.ts +27 -0
  110. package/dist/host/host-binding.js +11 -0
  111. package/dist/host/host-runtime.js +18 -0
  112. package/dist/host/install-codex.d.ts +8 -0
  113. package/dist/host/install-codex.js +2 -2
  114. package/dist/host/install-opencode.d.ts +38 -0
  115. package/dist/host/install-opencode.js +148 -0
  116. package/dist/host/install-orchestrator.d.ts +4 -1
  117. package/dist/host/install-orchestrator.js +20 -1
  118. package/dist/host/invoke-agent.d.ts +3 -2
  119. package/dist/host/opencode/context-cli.d.ts +15 -0
  120. package/dist/host/opencode/context-cli.js +25 -0
  121. package/dist/host/opencode/guard-cli.d.ts +13 -0
  122. package/dist/host/opencode/guard-cli.js +39 -0
  123. package/dist/host/opencode/plugin/forgen.d.ts +31 -0
  124. package/dist/host/opencode/plugin/forgen.js +60 -0
  125. package/dist/host/opencode/translate.d.ts +49 -0
  126. package/dist/host/opencode/translate.js +96 -0
  127. package/dist/host/parity-harness.d.ts +10 -2
  128. package/dist/host/projection.js +11 -0
  129. package/dist/mcp/tools.js +17 -4
  130. package/dist/store/evidence-store.d.ts +2 -6
  131. package/dist/store/evidence-store.js +68 -14
  132. package/dist/store/host-mismatch.d.ts +2 -1
  133. package/dist/store/host-mismatch.js +8 -8
  134. package/dist/store/profile-store.d.ts +3 -2
  135. package/dist/store/types.d.ts +9 -2
  136. package/package.json +3 -2
  137. package/plugin.json +2 -2
  138. package/skills/forge-loop/SKILL.md +7 -1
  139. package/skills/ship/SKILL.md +18 -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
- const truncated = safePrompt.length > PROMPT_HISTORY_TRUNCATE
139
- ? safePrompt.slice(0, PROMPT_HISTORY_TRUNCATE)
140
- : safePrompt;
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,
@@ -230,7 +236,7 @@ export async function main() {
230
236
  log.debug('finalizeSession 실패 (fail-open)', e);
231
237
  }
232
238
  // forge-loop 활성 시 미완료 스토리 감지 → 지속 메시지 주입 (polite-stop 방지)
233
- const forgeLoopBlock = checkForgeLoopActive();
239
+ const forgeLoopBlock = checkForgeLoopActive(sessionId);
234
240
  if (forgeLoopBlock) {
235
241
  console.log(forgeLoopBlock);
236
242
  return;
@@ -355,11 +361,11 @@ export async function main() {
355
361
  }
356
362
  }
357
363
  /**
358
- * 세션 종료 시 "forgen이 도움이 된 정도"를 요약.
359
- * solution-cache에서 이번 세션에 주입된 compound 솔루션 수를 집계하여
360
- * 카운터팩추얼 "forgen 없었으면 ~N분 더 걸렸을 것" 메시지 생성.
364
+ * 세션 종료 시 forgen *활동*을 요약 (관찰 — 인과 효과/절약 미주장).
365
+ * solution-cache 에서 이번 세션 주입된 compound 수·상위 솔루션·주입 대화 비율을 보여준다.
366
+ * honest-null (positioning #74): "forgen 없었으면 ~N분 절약" 카운터팩추얼은 은퇴.
361
367
  */
362
- function buildSessionSummary(sessionId, promptCount) {
368
+ export function buildSessionSummary(sessionId, promptCount) {
363
369
  try {
364
370
  // P1-S3 fix (2026-04-20): sanitizeId로 path traversal 차단.
365
371
  // 다른 세션 캐시 경로는 모두 sanitizeId 사용. 여기만 누락되어 있었다.
@@ -370,20 +376,20 @@ function buildSessionSummary(sessionId, promptCount) {
370
376
  const injected = Array.isArray(cache.injected) ? cache.injected : [];
371
377
  if (injected.length === 0)
372
378
  return '';
373
- // 카운터팩추얼: 주입된 compound 1건당 평균 8분 절약 가정 (하한 추정)
374
- const savedMins = injected.length * 8;
375
- const savedStr = savedMins >= 60
376
- ? `${Math.floor(savedMins / 60)}시간 ${savedMins % 60}분`
377
- : `${savedMins}분`;
378
- // 상위 3개 솔루션
379
+ // honest-null (2026-07-21, positioning #74 정합): "절약 시간(forgen 없었으면)"
380
+ // 카운터팩추얼은 은퇴 — 측정된 δ 없이 시간절약을 추정하는 건 날조다. *관찰 가능한*
381
+ // 활동만 보여준다 (무엇이 주입됐나), 인과 효과 크기는 주장하지 않는다.
379
382
  const topNames = injected.slice(0, 3).map(i => `"${i.name}"`).join(', ');
380
383
  const moreCount = injected.length - 3;
381
384
  const topStr = moreCount > 0 ? `${topNames} 외 ${moreCount}개` : topNames;
385
+ // 리뷰 SEV-3: 이전 "주입이 있던 대화 비율 = injected/promptCount × 100" 은 단위
386
+ // 불일치(솔루션수/프롬프트수)라 한 프롬프트 다중 주입 시 >100% 라는 불가능한 %가
387
+ // 났다. injected 캐시엔 프롬프트별 그룹핑이 없으므로 커버리지%를 낼 수 없다 →
388
+ // 오해 없는 *원시 밀도*(N건 / M프롬프트)로 관찰만 보여준다.
382
389
  return [
383
- `\n📊 이번 세션 forgen 효과:`,
390
+ `\n📊 이번 세션 forgen 활동:`,
384
391
  ` 주입된 compound: ${injected.length}건 (${topStr})`,
385
- ` 추정 절약 시간: ${savedStr} (forgen 없었으면 시행착오 필요)`,
386
- ` 프롬프트 대비 효율: ${(injected.length / promptCount * 100).toFixed(0)}% 의 대화가 축적된 지식의 도움을 받음\n`,
392
+ ` 세션 규모: ${promptCount} 프롬프트 ${'\x1b[2m'}(주입 밀도 = 주입 ${injected.length}건 / ${promptCount} 프롬프트, 관찰 — 도움 여부는 미측정)${'\x1b[0m'}`,
387
393
  ].join('\n');
388
394
  }
389
395
  catch {
@@ -416,10 +422,18 @@ const AUTO_COMPOUND_BARREN_COOLDOWN_MS = 30 * 60 * 1000; // 30 min if last run e
416
422
  * 일반 case (추출 있음) 은 5분 cooldown 유지 — adaptive 가 sparsity 시그널을
417
423
  * 강화할 뿐 정상 동작 차단 안 함.
418
424
  */
419
- function effectiveCooldownMs(parsed) {
425
+ // ADR-011 B수정: barren backoff 는 "지난번 0건 추출" 세션의 재추출을 30분 미룬다.
426
+ // 하지만 세션이 그동안 자라났으면(새 프롬프트 ≥ 이 값) 새 학습거리가 생긴 것이므로
427
+ // barren 을 무시하고 정상 쿨다운(5분)으로 재추출한다 — 긴 활성 세션이 굶지 않도록.
428
+ const AUTO_COMPOUND_GROWTH_THRESHOLD = 5;
429
+ export function effectiveCooldownMs(parsed, currentPromptCount = 0) {
420
430
  const total = (parsed.extractedSolutions ?? 0) + (parsed.promotedRules ?? 0)
421
431
  + (parsed.userPatternFound ? 1 : 0);
422
- return total === 0 ? AUTO_COMPOUND_BARREN_COOLDOWN_MS : AUTO_COMPOUND_COOLDOWN_MS;
432
+ if (total !== 0)
433
+ return AUTO_COMPOUND_COOLDOWN_MS;
434
+ // barren: 세션 성장 시 backoff 무시(재추출), 아니면 barren 유지.
435
+ const grew = currentPromptCount - (parsed.promptCount ?? 0) >= AUTO_COMPOUND_GROWTH_THRESHOLD;
436
+ return grew ? AUTO_COMPOUND_COOLDOWN_MS : AUTO_COMPOUND_BARREN_COOLDOWN_MS;
423
437
  }
424
438
  async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
425
439
  if (!transcriptPath || promptCount < 10)
@@ -430,7 +444,7 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
430
444
  const parsed = JSON.parse(raw);
431
445
  if (parsed.sessionId === sessionId) {
432
446
  const last = parsed.completedAt ? Date.parse(parsed.completedAt) : 0;
433
- const cooldown = effectiveCooldownMs(parsed);
447
+ const cooldown = effectiveCooldownMs(parsed, promptCount);
434
448
  if (Number.isFinite(last) && Date.now() - last < cooldown)
435
449
  return;
436
450
  }
@@ -463,43 +477,80 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
463
477
  else if (override) {
464
478
  log.debug('FORGEN_AUTO_COMPOUND_RUNNER_PATH 무시 — FORGEN_TEST=1 가 필요');
465
479
  }
466
- const child = spawnProcess('node', [runnerPath, cwd, transcriptPath, sessionId], {
467
- detached: true,
468
- stdio: 'ignore',
469
- });
470
- child.unref();
471
- log.debug(`Stop-triggered auto-compound 시작: ${sessionId} (${promptCount} prompts)`);
480
+ // 리뷰 SEV-2 #3: runAutoCompound 와 공유하는 in-flight start-gate 로 이중 spawn 방지.
481
+ // (이 경로는 test-override 러너를 쓰므로 runAutoCompound 를 직접 호출하지 않고 게이트만 공유.)
482
+ const { claimAutoCompoundInflight, releaseAutoCompoundInflight } = await import('../core/spawn.js');
483
+ if (!claimAutoCompoundInflight(sessionId)) {
484
+ log.debug('Stop-triggered auto-compound skip: 세션 in-flight');
485
+ return;
486
+ }
487
+ try {
488
+ const child = spawnProcess('node', [runnerPath, cwd, transcriptPath, sessionId, String(promptCount)], {
489
+ detached: true,
490
+ stdio: 'ignore',
491
+ });
492
+ child.unref();
493
+ log.debug(`Stop-triggered auto-compound 시작: ${sessionId} (${promptCount} prompts)`);
494
+ }
495
+ catch (e) {
496
+ releaseAutoCompoundInflight(sessionId); // spawn 실패 → 재시도 가능
497
+ log.debug('Stop-triggered auto-compound spawn 실패', e);
498
+ }
472
499
  }
473
500
  // forge-loop 차단 안전 상한 (무한 루프 방지)
474
501
  const FORGE_LOOP_MAX_BLOCKS = 30;
475
- const FORGE_LOOP_STALE_MS = 2 * 60 * 60 * 1000; // 2시간
502
+ // TTL: 24시간. 과거 2시간은 정상적으로 오래 걸리는 forge-loop 세션까지 중도
503
+ // 해제시켰다. "크래시된 세션의 잔여 active:true 가 무관한 후속 세션을 영구
504
+ // 차단"하는 문제는 이 TTL 이 아니라 아래 sessionId 바인딩으로 별도 해결한다 —
505
+ // TTL 은 진짜로 죽은(재개 안 되는) 세션을 회수하기 위한 상한일 뿐이다.
506
+ const FORGE_LOOP_STALE_MS = 24 * 60 * 60 * 1000;
476
507
  /**
477
508
  * forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
478
509
  * OMC의 persistent-mode.cjs 패턴 참고.
510
+ *
511
+ * @param sessionId 호출한 Stop hook 의 세션 ID. 미지정 시(레거시 호출) 세션
512
+ * 바인딩을 건너뛰고 기존처럼 전역으로 평가한다.
479
513
  */
480
- export function checkForgeLoopActive() {
514
+ export function checkForgeLoopActive(sessionId) {
481
515
  try {
482
516
  if (!fs.existsSync(FORGE_LOOP_STATE_PATH))
483
517
  return null;
484
518
  const state = JSON.parse(fs.readFileSync(FORGE_LOOP_STATE_PATH, 'utf-8'));
485
519
  if (!state.active)
486
520
  return null;
487
- // Stale 감지: 2시간+ 미활동 → 자동 비활성화
521
+ // 세션 바인딩: 이미 다른 세션에 귀속된 루프라면 이 세션과 무관 — 손대지 않고 통과.
522
+ // 크래시된 세션이 남긴 active:true 가 이후 전혀 다른 세션을 영구 차단하는 것을 방지.
523
+ if (state.sessionId && sessionId && state.sessionId !== sessionId) {
524
+ return null;
525
+ }
526
+ // 최초 차단 후보 세션에 자동 귀속 (스킬은 세션 ID를 알 필요 없음).
527
+ if (!state.sessionId && sessionId) {
528
+ state.sessionId = sessionId;
529
+ }
530
+ // 사용자 명시 우회 — 이번 턴만 통과시키고 루프 상태(active/blockCount)는 보존.
531
+ // pre-tool-use/stop-guard 와 동일한 FORGEN_USER_CONFIRMED=1 관용구.
532
+ if (process.env.FORGEN_USER_CONFIRMED === '1')
533
+ return null;
534
+ // Stale 감지: TTL(24h) 초과 → 자동 비활성화 + 1회성 안내 (침묵 해제 금지).
488
535
  const startedAt = new Date(state.startedAt).getTime();
489
536
  if (Number.isFinite(startedAt) && Date.now() - startedAt > FORGE_LOOP_STALE_MS) {
490
537
  state.active = false;
491
538
  atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
492
- return null;
539
+ return approveWithWarning(`[Forgen] forge-loop 상태가 ${Math.round(FORGE_LOOP_STALE_MS / 3_600_000)}시간 이상 갱신되지 않아 자동 해제되었습니다. ` +
540
+ `재개하려면 "/forge-loop resume" 를 입력하세요.`);
493
541
  }
494
- // 확인 대기 중이면 차단하지 않음 (사용자 개입 허용)
495
- if (state.awaitingConfirmation)
542
+ // 확인 대기 중이면 차단하지 않음 (사용자 개입 허용) — 귀속은 반영 후 통과.
543
+ if (state.awaitingConfirmation) {
544
+ atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
496
545
  return null;
497
- // 안전 상한: 30회 이상 차단 시 무한 루프로 간주하여 해제
546
+ }
547
+ // 안전 상한: 30회 이상 연속 차단 시 무한 루프로 간주하여 해제 + 1회성 안내.
498
548
  const blockCount = state.blockCount ?? 0;
499
549
  if (blockCount >= FORGE_LOOP_MAX_BLOCKS) {
500
550
  state.active = false;
501
551
  atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
502
- return null;
552
+ return approveWithWarning(`[Forgen] forge-loop 이 연속 ${FORGE_LOOP_MAX_BLOCKS}회 차단되어 안전 상한으로 자동 해제되었습니다. ` +
553
+ `무한 루프 가능성을 점검한 뒤 필요 시 "/forge-loop resume" 로 재개하세요.`);
503
554
  }
504
555
  // 미완료 스토리 확인
505
556
  const stories = Array.isArray(state.stories) ? state.stories : [];
@@ -515,25 +566,20 @@ export function checkForgeLoopActive() {
515
566
  state.lastBlockAt = new Date().toISOString();
516
567
  atomicWriteJSON(FORGE_LOOP_STATE_PATH, state);
517
568
  const nextStory = pending[0];
518
- const message = [
569
+ const firstAC = Array.isArray(nextStory.acceptanceCriteria) ? nextStory.acceptanceCriteria[0] : undefined;
570
+ const lines = [
519
571
  `<forgen-forge-loop iteration="${state.blockCount}/${FORGE_LOOP_MAX_BLOCKS}">`,
520
572
  `[FORGE-LOOP] ${pending.length}개 스토리가 미완료입니다.`,
521
573
  `현재 스토리: ${nextStory.id} — ${nextStory.title}`,
522
- ``,
523
- `계속 진행하세요. 보고는 다음 시점에만 합니다:`,
524
- ` 1. 모든 스토리 완료 (최종 리포트)`,
525
- ` 2. 3회 실패 (에스컬레이션)`,
526
- ` 3. Context limit 접근 (handoff)`,
527
- ``,
528
- `중간 "완료했습니다" 보고는 polite-stop anti-pattern입니다.`,
529
- `취소하려면: "/forge-loop cancel" 또는 "cancelforgen" 입력`,
530
- `</forgen-forge-loop>`,
531
- ].join('\n');
574
+ ];
575
+ if (firstAC)
576
+ lines.push(`AC1: ${firstAC}`);
577
+ lines.push(``, `계속 진행하세요. 보고는 다음 시점에만 합니다:`, ` 1. 모든 스토리 완료 (최종 리포트)`, ` 2. 3회 실패 (에스컬레이션)`, ` 3. Context limit 접근 (handoff)`, ``, `중간 "완료했습니다" 보고는 polite-stop anti-pattern입니다.`, `취소하려면: "/forge-loop cancel" 또는 "cancelforgen" 입력`, `</forgen-forge-loop>`);
532
578
  // block 결정으로 Claude가 계속 작업하도록 강제
533
579
  return JSON.stringify({
534
580
  continue: true,
535
581
  decision: 'block',
536
- reason: message,
582
+ reason: lines.join('\n'),
537
583
  });
538
584
  }
539
585
  catch (e) {
@@ -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
- import { recordToolCall } from '../core/usage-telemetry.js';
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-008 / 0.4.6 — usage telemetry. fail-open, hook 차단 안 함.
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
- const code = String(toolInput.new_string ?? toolInput.content ?? '');
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`);
@@ -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 에서 rule trigger 가 명시되지 않을 때의 기본 완료 선언 매칭. */
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;
@@ -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 에서 rule trigger 가 명시되지 않을 때의 기본 완료 선언 매칭. */
22
+ /** Stop hook 기본 완료 선언 매칭 (완료 동사/어미만 — skip-signal 미포함). */
13
23
  export const DEFAULT_STOP_TRIGGER_RE = '(완료했|완성됐|완성되|완성했|done\\.|ready\\.|shipped\\.|LGTM|finished\\.)';
14
24
  /** Stop hook 기본 exclude — retraction/negation/meta 맥락 제외. */
15
25
  export const DEFAULT_STOP_EXCLUDE_RE = '(취소|철회|없음|없습니다|않았|하지\\s*않|아닙니다|not\\s*yet|no\\s*longer|retract|withdraw|아직\\s*(안|아))';
@@ -17,3 +27,26 @@ export const DEFAULT_STOP_EXCLUDE_RE = '(취소|철회|없음|없습니다|않
17
27
  export const MOCK_TRIGGER_RE = '(mock|stub|fake)';
18
28
  /** mock trigger 의 exclude — 테스트 맥락은 정상. */
19
29
  export const MOCK_EXCLUDE_RE = '(테스트|test|vi\\.mock|jest\\.mock|spec\\.)';
30
+ // ── critic-review 룰 전용 트리거 (2026-07-22) ─────────────────────────────────
31
+ /** "리뷰/검토 생략" 시그널 (한/영). 외래어(스킵/패스)·구어(안 하고) 포함 — 리뷰 SEV-3 (a). */
32
+ const REVIEW_SKIP = '(리뷰|검토)[^.!?\\n]{0,8}(생략|건너뛰|없이|스킵|패스|안\\s*하고)|skip(?:ping|s)?\\s+(?:the\\s+)?review';
33
+ /** "다음으로 넘어감" 진행 시그널 (활용형 커버). */
34
+ const MOVE_ON = '넘어가|넘어갑|넘어갔|넘어감|넘어갈|다음\\s*(작업|기능|단계|스텝|것|이터레이션)|move\\s+on|next\\s+(step|task|feature)|proceed';
35
+ /**
36
+ * "리뷰 생략" AND "다음으로 넘어감" 이 함께 있을 때만 발화하는 결합 트리거.
37
+ * 두 lookahead 로 conjunction — 한쪽만 있는 경고/질문/신중 응답은 미발화(FP 0).
38
+ */
39
+ export const SKIP_REVIEW_TRIGGER_RE = `(?=[\\s\\S]*?(?:${REVIEW_SKIP}))(?=[\\s\\S]*?(?:${MOVE_ON}))`;
40
+ /**
41
+ * critic-review 룰 트리거 = 완료 선언 OR 리뷰생략-넘어감.
42
+ * (완료 시점 + skip 시점 양쪽에서 critic 강제. e2e/mock 룰엔 부여 안 함 — DEFAULT 만 사용.)
43
+ */
44
+ export const CRITIC_STOP_TRIGGER_RE = `(${DEFAULT_STOP_TRIGGER_RE}|${SKIP_REVIEW_TRIGGER_RE})`;
45
+ /**
46
+ * critic 트리거 exclude — 기본 retraction + 부정/금지/질문형 + 숙고형 보강.
47
+ * 주의: bare `안\s*하` 는 넣지 않는다 — "리뷰 안 하고 넘어감"(실제 skip TP)을 죽이므로.
48
+ * retraction 은 과거형 `안\s*했`(안 했다)만 배제. 숙고형(할지|여부|고민…)은 skip 단언이
49
+ * 아니라 결정 전 단계라 배제 (리뷰 SEV-3 (a)).
50
+ */
51
+ export const CRITIC_STOP_EXCLUDE_RE = '(?:' + DEFAULT_STOP_EXCLUDE_RE +
52
+ '|말고|마세요|말라|안\\s*했|안\\s*해|안\\s*할|안\\s*돼|안\\s*됩|하세요|반드시|위험|should\\s*not|shouldn|don.?t|될까요|까요\\?|할지|갈지|여부|고민|결정하)';
@@ -25,6 +25,9 @@
25
25
  * If fitness data is unavailable (fresh install, empty outcomes/),
26
26
  * every solution falls into the default 0.3 bucket — identical to the
27
27
  * pre-0.3.2 gate. No cold-start regression.
28
+ *
29
+ * W3-2 리뷰(SEV-3 #1): 0.3 리터럴을 relevance-gate.RELEVANCE_MATCH_GATE 로 단일화 —
30
+ * 교정 클러스터링 τ 와 같은 소스를 공유해 근거-정직성 드리프트를 코드로 방지.
28
31
  */
29
32
  export declare const MIN_INJECT_RELEVANCE = 0.3;
30
33
  export declare const MIN_INJECT_RELEVANCE_TRUSTED = 0.25;
@@ -85,4 +88,29 @@ export declare function commitSessionCacheEntries(sessionId: string, newEntries:
85
88
  name: string;
86
89
  chars: number;
87
90
  }>): SessionCacheCommitResult;
91
+ interface TierSolution {
92
+ name: string;
93
+ type: string;
94
+ confidence: number;
95
+ matchedTags: string[];
96
+ }
97
+ /** Tier 2 — 헤더 + 태그 + 핵심 최대 3줄 (최대 SUMMARY_MAX_CHARS) */
98
+ export declare function buildSummaryTier(sol: TierSolution, raw: string): string;
99
+ /**
100
+ * Tier 1 — 이름 + one-liner + compound-read MCP 힌트 (전문은 pull-on-demand).
101
+ * type/confidence 헤더는 의도적으로 생략 — 인덱스 라인은 포인터일 뿐, 상세는
102
+ * compound-read로 당겨 읽는다.
103
+ */
104
+ export declare function buildIndexTier(sol: TierSolution, raw: string): string;
105
+ /** Tier 0 — 전문(Context+Content) 그대로. 파싱 실패/캡 초과면 null (호출부가 Tier 2로 폴백). */
106
+ export declare function buildFullTier(sol: TierSolution, raw: string): string | null;
107
+ /**
108
+ * 매치 목록(relevance desc 정렬 가정)을 Progressive Disclosure 텍스트로 렌더한다.
109
+ * 순수 함수 — fs 읽기는 caller(main)가 `rawByName`으로 미리 채워 넣는다.
110
+ *
111
+ * - 매치가 정확히 1건 & 전문이 FULL_TEXT_MAX_CHARS 이하 → Tier 0(전문)
112
+ * - 그 외 상위 TOP_TIER_COUNT건 → Tier 2(요약)
113
+ * - 나머지 → Tier 1(인덱스 라인)
114
+ */
115
+ export declare function renderSolutionTiers(sols: readonly TierSolution[], rawByName: ReadonlyMap<string, string>): Map<string, string>;
88
116
  export {};