@wooojin/forgen 0.3.2 → 0.4.1

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 (124) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +94 -0
  3. package/README.ja.md +119 -8
  4. package/README.ko.md +73 -2
  5. package/README.md +163 -9
  6. package/README.zh.md +87 -7
  7. package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
  8. package/dist/checks/conclusion-verification-ratio.js +86 -0
  9. package/dist/checks/fact-vs-agreement.d.ts +47 -0
  10. package/dist/checks/fact-vs-agreement.js +92 -0
  11. package/dist/checks/self-score-deflation.d.ts +38 -0
  12. package/dist/checks/self-score-deflation.js +108 -0
  13. package/dist/cli.js +158 -6
  14. package/dist/core/auto-compound-runner.js +85 -13
  15. package/dist/core/dashboard.js +9 -2
  16. package/dist/core/doctor.js +90 -15
  17. package/dist/core/extraction-notice.d.ts +18 -0
  18. package/dist/core/extraction-notice.js +64 -0
  19. package/dist/core/init-cli.d.ts +26 -0
  20. package/dist/core/init-cli.js +104 -0
  21. package/dist/core/init.js +17 -0
  22. package/dist/core/inspect-cli.js +64 -5
  23. package/dist/core/migrate-cli.d.ts +10 -0
  24. package/dist/core/migrate-cli.js +34 -0
  25. package/dist/core/paths.d.ts +8 -1
  26. package/dist/core/paths.js +11 -2
  27. package/dist/core/recall-cli.d.ts +26 -0
  28. package/dist/core/recall-cli.js +125 -0
  29. package/dist/core/recall-reference-detector.d.ts +43 -0
  30. package/dist/core/recall-reference-detector.js +65 -0
  31. package/dist/core/state-gc.d.ts +19 -0
  32. package/dist/core/state-gc.js +48 -4
  33. package/dist/core/stats-cli.d.ts +36 -0
  34. package/dist/core/stats-cli.js +254 -0
  35. package/dist/core/uninstall.d.ts +1 -0
  36. package/dist/core/uninstall.js +25 -1
  37. package/dist/core/v1-bootstrap.js +9 -1
  38. package/dist/engine/classify-enforce-cli.d.ts +8 -0
  39. package/dist/engine/classify-enforce-cli.js +61 -0
  40. package/dist/engine/compound-cli.js +1 -0
  41. package/dist/engine/compound-export.js +8 -3
  42. package/dist/engine/enforce-classifier.d.ts +31 -0
  43. package/dist/engine/enforce-classifier.js +123 -0
  44. package/dist/engine/learn-cli.js +1 -4
  45. package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
  46. package/dist/engine/lifecycle/bypass-detector.js +82 -0
  47. package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
  48. package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
  49. package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
  50. package/dist/engine/lifecycle/meta-cli.js +7 -0
  51. package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
  52. package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
  53. package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
  54. package/dist/engine/lifecycle/orchestrator.js +131 -0
  55. package/dist/engine/lifecycle/signals.d.ts +30 -0
  56. package/dist/engine/lifecycle/signals.js +142 -0
  57. package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
  58. package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
  59. package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
  60. package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
  61. package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
  62. package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
  63. package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
  64. package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
  65. package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
  66. package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
  67. package/dist/engine/lifecycle/types.d.ts +52 -0
  68. package/dist/engine/lifecycle/types.js +7 -0
  69. package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
  70. package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
  71. package/dist/engine/rule-toggle-cli.d.ts +13 -0
  72. package/dist/engine/rule-toggle-cli.js +76 -0
  73. package/dist/engine/skill-promoter.js +3 -6
  74. package/dist/forge/evidence-processor.js +10 -2
  75. package/dist/hooks/context-guard.js +72 -1
  76. package/dist/hooks/dangerous-patterns.json +3 -3
  77. package/dist/hooks/db-guard.js +18 -2
  78. package/dist/hooks/intent-classifier.js +1 -1
  79. package/dist/hooks/keyword-detector.js +1 -1
  80. package/dist/hooks/notepad-injector.js +1 -1
  81. package/dist/hooks/permission-handler.js +1 -1
  82. package/dist/hooks/post-tool-failure.js +1 -1
  83. package/dist/hooks/post-tool-use.d.ts +6 -0
  84. package/dist/hooks/post-tool-use.js +94 -14
  85. package/dist/hooks/pre-compact.js +1 -1
  86. package/dist/hooks/pre-tool-use.d.ts +7 -0
  87. package/dist/hooks/pre-tool-use.js +79 -5
  88. package/dist/hooks/rate-limiter.js +1 -1
  89. package/dist/hooks/secret-filter.d.ts +10 -0
  90. package/dist/hooks/secret-filter.js +21 -1
  91. package/dist/hooks/session-recovery.js +1 -1
  92. package/dist/hooks/shared/atomic-write.d.ts +8 -1
  93. package/dist/hooks/shared/atomic-write.js +17 -3
  94. package/dist/hooks/shared/command-parser.d.ts +44 -0
  95. package/dist/hooks/shared/command-parser.js +50 -0
  96. package/dist/hooks/shared/hook-response.d.ts +23 -2
  97. package/dist/hooks/shared/hook-response.js +48 -3
  98. package/dist/hooks/shared/safe-regex.d.ts +25 -0
  99. package/dist/hooks/shared/safe-regex.js +50 -0
  100. package/dist/hooks/shared/stop-triggers.d.ts +19 -0
  101. package/dist/hooks/shared/stop-triggers.js +19 -0
  102. package/dist/hooks/skill-injector.js +1 -1
  103. package/dist/hooks/slop-detector.js +2 -2
  104. package/dist/hooks/solution-injector.d.ts +9 -0
  105. package/dist/hooks/solution-injector.js +48 -5
  106. package/dist/hooks/stop-guard.d.ts +84 -0
  107. package/dist/hooks/stop-guard.js +606 -0
  108. package/dist/hooks/subagent-tracker.js +1 -1
  109. package/dist/i18n/index.js +3 -5
  110. package/dist/mcp/tools.js +19 -2
  111. package/dist/store/evidence-store.d.ts +15 -0
  112. package/dist/store/evidence-store.js +61 -1
  113. package/dist/store/implicit-feedback-store.d.ts +59 -0
  114. package/dist/store/implicit-feedback-store.js +153 -0
  115. package/dist/store/rule-lifecycle.d.ts +23 -0
  116. package/dist/store/rule-lifecycle.js +63 -0
  117. package/dist/store/rule-store.d.ts +21 -0
  118. package/dist/store/rule-store.js +136 -8
  119. package/dist/store/types.d.ts +83 -0
  120. package/dist/store/types.js +7 -1
  121. package/hooks/hook-registry.json +1 -0
  122. package/hooks/hooks.json +6 -1
  123. package/package.json +11 -3
  124. package/plugin.json +1 -1
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Safe regex compiler — ReDoS 방지용 경량 가드.
3
+ *
4
+ * rule JSON 의 verifier.params.pattern 등 user-controlled regex 를 hook 런타임에
5
+ * 그대로 new RegExp() 하면 catastrophic backtracking 으로 hook hang 위험이 있다.
6
+ * re2 같은 linear-time 엔진 의존은 native binding 을 추가시키므로, 여기서는
7
+ * **패턴 복잡도 제한** + **입력 크기 제한** 으로 1차 방어.
8
+ *
9
+ * 정책:
10
+ * - 패턴 길이 ≤ 500자.
11
+ * - 중첩 quantifier (`(...)+)+` / `(...)*)*` / `(.+)+`) 같은 catastrophic 신호 거부.
12
+ * - backreference `\1..\9` 금지.
13
+ * - compile 실패 또는 거부 시 null 반환 → 호출자가 skip.
14
+ */
15
+ export interface SafeRegexResult {
16
+ regex: RegExp | null;
17
+ reason: string | null;
18
+ }
19
+ /**
20
+ * 패턴을 안전하게 컴파일. 거부되거나 실패 시 { regex: null, reason } 반환.
21
+ * 호출자는 reason 을 log.debug 로 기록하고 skip 하는 것이 권장 사용법.
22
+ */
23
+ export declare function compileSafeRegex(pattern: string, flags?: string): SafeRegexResult;
24
+ /** 입력을 MAX_INPUT_LEN 으로 자른 뒤 regex.test() 수행. 입력 DoS 방어. */
25
+ export declare function safeRegexTest(regex: RegExp, input: string): boolean;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Safe regex compiler — ReDoS 방지용 경량 가드.
3
+ *
4
+ * rule JSON 의 verifier.params.pattern 등 user-controlled regex 를 hook 런타임에
5
+ * 그대로 new RegExp() 하면 catastrophic backtracking 으로 hook hang 위험이 있다.
6
+ * re2 같은 linear-time 엔진 의존은 native binding 을 추가시키므로, 여기서는
7
+ * **패턴 복잡도 제한** + **입력 크기 제한** 으로 1차 방어.
8
+ *
9
+ * 정책:
10
+ * - 패턴 길이 ≤ 500자.
11
+ * - 중첩 quantifier (`(...)+)+` / `(...)*)*` / `(.+)+`) 같은 catastrophic 신호 거부.
12
+ * - backreference `\1..\9` 금지.
13
+ * - compile 실패 또는 거부 시 null 반환 → 호출자가 skip.
14
+ */
15
+ const MAX_PATTERN_LEN = 500;
16
+ const MAX_INPUT_LEN = 65536;
17
+ // Catastrophic backtracking 의 흔한 형태 — 중첩된 quantifier 체인.
18
+ const NESTED_QUANTIFIER = /\([^)]*[+*][^)]*\)[+*]/;
19
+ // Alternation with shared prefix can also be catastrophic — heuristic only.
20
+ const OVERLAPPING_ALT = /\(([^|)]+)\|\1[^)]*\)[+*]/;
21
+ const BACKREFERENCE = /\\[1-9]/;
22
+ /**
23
+ * 패턴을 안전하게 컴파일. 거부되거나 실패 시 { regex: null, reason } 반환.
24
+ * 호출자는 reason 을 log.debug 로 기록하고 skip 하는 것이 권장 사용법.
25
+ */
26
+ export function compileSafeRegex(pattern, flags = '') {
27
+ if (typeof pattern !== 'string')
28
+ return { regex: null, reason: 'non-string pattern' };
29
+ if (pattern.length === 0)
30
+ return { regex: null, reason: 'empty pattern' };
31
+ if (pattern.length > MAX_PATTERN_LEN)
32
+ return { regex: null, reason: `pattern length ${pattern.length} > ${MAX_PATTERN_LEN}` };
33
+ if (NESTED_QUANTIFIER.test(pattern))
34
+ return { regex: null, reason: 'nested quantifier (catastrophic backtracking risk)' };
35
+ if (OVERLAPPING_ALT.test(pattern))
36
+ return { regex: null, reason: 'overlapping alternation with quantifier' };
37
+ if (BACKREFERENCE.test(pattern))
38
+ return { regex: null, reason: 'backreference in user regex (perf risk)' };
39
+ try {
40
+ return { regex: new RegExp(pattern, flags), reason: null };
41
+ }
42
+ catch (e) {
43
+ return { regex: null, reason: `compile error: ${String(e).slice(0, 80)}` };
44
+ }
45
+ }
46
+ /** 입력을 MAX_INPUT_LEN 으로 자른 뒤 regex.test() 수행. 입력 DoS 방어. */
47
+ export function safeRegexTest(regex, input) {
48
+ const truncated = input.length > MAX_INPUT_LEN ? input.slice(0, MAX_INPUT_LEN) : input;
49
+ return regex.test(truncated);
50
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared Stop hook default trigger regexes.
3
+ *
4
+ * R6-F2 (2026-04-22): stop-guard 와 enforce-classifier 에 리터럴 중복되던 정규식을
5
+ * 단일 소스로 통합. 한쪽만 고치면 다른 쪽이 drift 하는 sibling-bug 패턴 차단.
6
+ *
7
+ * 설계 결정:
8
+ * - trigger 는 명시적 완료 선언 동사/어미만 — "완료" 단독 매칭 금지 (retraction 오매칭 방지).
9
+ * - exclude 는 retraction/negation/meta 언급 광범위 차단.
10
+ * - A1 spike 결과로 검증됨 (10/10 scenarios pass, FP 0%).
11
+ */
12
+ /** Stop hook 에서 rule trigger 가 명시되지 않을 때의 기본 완료 선언 매칭. */
13
+ 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
+ /** Stop hook 기본 exclude — retraction/negation/meta 맥락 제외. */
15
+ 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))";
16
+ /** mock/stub/fake 감지 — R-B2 전용 pattern (자가검증 주장 차단). */
17
+ export declare const MOCK_TRIGGER_RE = "(mock|stub|fake)";
18
+ /** mock trigger 의 exclude — 테스트 맥락은 정상. */
19
+ export declare const MOCK_EXCLUDE_RE = "(\uD14C\uC2A4\uD2B8|test|vi\\.mock|jest\\.mock|spec\\.)";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared Stop hook default trigger regexes.
3
+ *
4
+ * R6-F2 (2026-04-22): stop-guard 와 enforce-classifier 에 리터럴 중복되던 정규식을
5
+ * 단일 소스로 통합. 한쪽만 고치면 다른 쪽이 drift 하는 sibling-bug 패턴 차단.
6
+ *
7
+ * 설계 결정:
8
+ * - trigger 는 명시적 완료 선언 동사/어미만 — "완료" 단독 매칭 금지 (retraction 오매칭 방지).
9
+ * - exclude 는 retraction/negation/meta 언급 광범위 차단.
10
+ * - A1 spike 결과로 검증됨 (10/10 scenarios pass, FP 0%).
11
+ */
12
+ /** Stop hook 에서 rule trigger 가 명시되지 않을 때의 기본 완료 선언 매칭. */
13
+ export const DEFAULT_STOP_TRIGGER_RE = '(완료했|완성됐|완성되|완성했|done\\.|ready\\.|shipped\\.|LGTM|finished\\.)';
14
+ /** Stop hook 기본 exclude — retraction/negation/meta 맥락 제외. */
15
+ export const DEFAULT_STOP_EXCLUDE_RE = '(취소|철회|없음|없습니다|않았|하지\\s*않|아닙니다|not\\s*yet|no\\s*longer|retract|withdraw|아직\\s*(안|아))';
16
+ /** mock/stub/fake 감지 — R-B2 전용 pattern (자가검증 주장 차단). */
17
+ export const MOCK_TRIGGER_RE = '(mock|stub|fake)';
18
+ /** mock trigger 의 exclude — 테스트 맥락은 정상. */
19
+ export const MOCK_EXCLUDE_RE = '(테스트|test|vi\\.mock|jest\\.mock|spec\\.)';
@@ -312,5 +312,5 @@ async function main() {
312
312
  }
313
313
  main().catch((e) => {
314
314
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
315
- console.log(failOpenWithTracking('skill-injector'));
315
+ console.log(failOpenWithTracking('skill-injector', e));
316
316
  });
@@ -84,10 +84,10 @@ async function main() {
84
84
  }
85
85
  catch (e) {
86
86
  log.debug('슬롭 감지 실패', e);
87
- console.log(failOpenWithTracking('slop-detector'));
87
+ console.log(failOpenWithTracking('slop-detector', e));
88
88
  }
89
89
  }
90
90
  main().catch((e) => {
91
91
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
92
- console.log(failOpenWithTracking('slop-detector'));
92
+ console.log(failOpenWithTracking('slop-detector', e));
93
93
  });
@@ -28,6 +28,15 @@
28
28
  */
29
29
  export declare const MIN_INJECT_RELEVANCE = 0.3;
30
30
  export declare const MIN_INJECT_RELEVANCE_TRUSTED = 0.25;
31
+ /**
32
+ * v0.4.1 — cold-start 사용자 threshold. outcomes 이벤트가 거의 없는 신규 사용자
33
+ * 는 starter-pack 이 champion/active 로 승격될 기회가 없어 0.3 gate 에 막혀 주입 0.
34
+ * 실측 (buyer-day1 v2): starter 15개 중 recall 5건 전부 relevance 0.08~0.25, 주입 0.
35
+ * 이 값으로 첫날부터 매칭 가능성 제공 + 누적 후엔 표준 threshold 로 자연 전환.
36
+ */
37
+ export declare const MIN_INJECT_RELEVANCE_COLD_START = 0.2;
38
+ /** cold-start 판정 임계 — fitness state 있는 솔루션이 이 수 미만이면 신규 사용자 간주. */
39
+ export declare const COLD_START_FITNESS_THRESHOLD = 5;
31
40
  interface SessionCacheCommitResult {
32
41
  /**
33
42
  * commit 상태:
@@ -29,6 +29,7 @@ import { approve, approveWithContext, failOpenWithTracking } from './shared/hook
29
29
  import { STATE_DIR } from '../core/paths.js';
30
30
  import { recordHookTiming } from './shared/hook-timing.js';
31
31
  import { appendPending, flushAccept } from '../engine/solution-outcomes.js';
32
+ import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
32
33
  const MAX_SOLUTIONS_PER_SESSION = 10;
33
34
  /**
34
35
  * Minimum relevance thresholds by fitness state (2026-04-21 gate sweep).
@@ -51,6 +52,15 @@ const MAX_SOLUTIONS_PER_SESSION = 10;
51
52
  */
52
53
  export const MIN_INJECT_RELEVANCE = 0.3;
53
54
  export const MIN_INJECT_RELEVANCE_TRUSTED = 0.25;
55
+ /**
56
+ * v0.4.1 — cold-start 사용자 threshold. outcomes 이벤트가 거의 없는 신규 사용자
57
+ * 는 starter-pack 이 champion/active 로 승격될 기회가 없어 0.3 gate 에 막혀 주입 0.
58
+ * 실측 (buyer-day1 v2): starter 15개 중 recall 5건 전부 relevance 0.08~0.25, 주입 0.
59
+ * 이 값으로 첫날부터 매칭 가능성 제공 + 누적 후엔 표준 threshold 로 자연 전환.
60
+ */
61
+ export const MIN_INJECT_RELEVANCE_COLD_START = 0.2;
62
+ /** cold-start 판정 임계 — fitness state 있는 솔루션이 이 수 미만이면 신규 사용자 간주. */
63
+ export const COLD_START_FITNESS_THRESHOLD = 5;
54
64
  /** 세션별 이미 주입된 솔루션 추적 (중복 방지) */
55
65
  function getSessionCachePath(sessionId) {
56
66
  return path.join(STATE_DIR, `solution-cache-${sanitizeId(sessionId)}.json`);
@@ -347,11 +357,17 @@ async function main() {
347
357
  catch (e) {
348
358
  log.debug('fitness state load 실패 — default 0.3 적용', e);
349
359
  }
360
+ // v0.4.1 cold-start boost: outcome 이벤트가 누적되지 않은 신규 사용자 (fitness
361
+ // state 있는 솔루션 < THRESHOLD) 는 champion/active 로 승격될 기회가 없으므로
362
+ // 보정된 낮은 threshold 적용. 누적되면 자동으로 표준 경로로 전환.
363
+ const isColdStart = fitnessStateMap.size < COLD_START_FITNESS_THRESHOLD;
350
364
  function minRelevanceFor(name) {
351
365
  const state = fitnessStateMap.get(name);
352
- return (state === 'champion' || state === 'active')
353
- ? MIN_INJECT_RELEVANCE_TRUSTED
354
- : MIN_INJECT_RELEVANCE;
366
+ if (state === 'champion' || state === 'active')
367
+ return MIN_INJECT_RELEVANCE_TRUSTED;
368
+ if (isColdStart)
369
+ return MIN_INJECT_RELEVANCE_COLD_START;
370
+ return MIN_INJECT_RELEVANCE;
355
371
  }
356
372
  let experimentCount = 0;
357
373
  const toInject = [];
@@ -530,7 +546,34 @@ async function main() {
530
546
  catch (e) {
531
547
  log.debug('outcome appendPending 실패', e);
532
548
  }
533
- console.log(approveWithContext(fullInjection, 'UserPromptSubmit'));
549
+ // H4: 양수 implicit-feedback — 솔루션이 실제로 사용자에게 surface 되었음을 기록.
550
+ // v0.4.0 의 enforcement 축은 block/violation 만 카운트했고 assist (solution 노출)
551
+ // 은 0건이었다. 이 emit 으로 forgen stats / session-quality-scorer 가 "오늘
552
+ // N개 surfaced" 를 계산할 수 있다.
553
+ try {
554
+ const now = new Date().toISOString();
555
+ for (const sol of effectiveToInject) {
556
+ appendImplicitFeedback({
557
+ type: 'recommendation_surfaced',
558
+ category: 'positive',
559
+ solution: sol.name,
560
+ match_score: sol.relevance,
561
+ at: now,
562
+ sessionId,
563
+ });
564
+ }
565
+ }
566
+ catch (e) {
567
+ log.debug('recommendation_surfaced emit 실패', e);
568
+ }
569
+ // H1: 사용자 UI 에 recall hit 1줄 노출. additionalContext 는 모델 전용이라
570
+ // v0.4.0 에서 8,000+ 주입이 발생했는데도 사용자는 0건을 봤다. systemMessage
571
+ // 로 "N개 솔루션 참조" 를 surface → 사용자가 어떤 축적 지식이 붙었는지 인식.
572
+ const topNames = effectiveToInject.slice(0, 3).map((s) => s.name);
573
+ const more = effectiveToInject.length - topNames.length;
574
+ const noticeNames = more > 0 ? `${topNames.join(', ')} (+${more})` : topNames.join(', ');
575
+ const userNotice = `[Forgen] 🔎 ${effectiveToInject.length} solution${effectiveToInject.length === 1 ? '' : 's'} recalled: ${noticeNames}`;
576
+ console.log(approveWithContext(fullInjection, 'UserPromptSubmit', userNotice));
534
577
  }
535
578
  finally {
536
579
  recordHookTiming('solution-injector', Date.now() - _hookStart, 'UserPromptSubmit');
@@ -538,5 +581,5 @@ async function main() {
538
581
  }
539
582
  main().catch((e) => {
540
583
  process.stderr.write(`[ch-hook] solution-injector: ${e instanceof Error ? e.message : String(e)}\n`);
541
- console.log(failOpenWithTracking('solution-injector'));
584
+ console.log(failOpenWithTracking('solution-injector', e));
542
585
  });
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forgen — Stop Guard (Mech-B prototype, spike/mech-b-a1)
4
+ *
5
+ * Stop hook: 어시스턴트 직전 응답에서 "완료 선언" 패턴을 감지하고, 연결된
6
+ * Mech-A(artifact_check) / Mech-B(self_check_prompt) 규칙을 평가하여
7
+ * 위반 시 blockStop 으로 세션을 재개시킨다.
8
+ *
9
+ * Prototype scope (spike only — NOT v0.4.0 final):
10
+ * - 규칙은 tests/spike/mech-b-inject/scenarios.json 에서 로드
11
+ * (FORGEN_SPIKE_RULES env 로 override 가능)
12
+ * - 어시스턴트 메시지는 transcript_path 에서 마지막 assistant 턴을 뽑거나
13
+ * FORGEN_SPIKE_LAST_MESSAGE env 로 주입 가능 (runner/단위테스트용)
14
+ * - artifact_check 는 `~/.forgen/state/<relative>` 경로를 기준으로 평가
15
+ *
16
+ * 설계 제약 (ADR-001, Day-1 verification):
17
+ * - self_check_prompt 질문은 **reason** 에 전체를 담는다 (모델 도달).
18
+ * - systemMessage 는 rule tag 한 줄만 (UI 표시 보조).
19
+ * - 외부 LLM API 호출 없음 (β1 유지).
20
+ */
21
+ import type { Rule } from '../store/types.js';
22
+ interface VerifierSpec {
23
+ kind: 'self_check_prompt' | 'artifact_check' | 'tool_arg_regex';
24
+ params: Record<string, string | number | boolean>;
25
+ }
26
+ interface SpikeRule {
27
+ id: string;
28
+ mech: 'A' | 'B' | 'C';
29
+ hook: 'Stop' | 'PreToolUse' | 'PostToolUse' | 'UserPromptSubmit';
30
+ trigger: {
31
+ response_keywords_regex?: string;
32
+ context_exclude_regex?: string;
33
+ };
34
+ verifier: VerifierSpec;
35
+ block_message?: string;
36
+ system_tag?: string;
37
+ }
38
+ /**
39
+ * 프로덕션 rule-store 로더.
40
+ * ~/.forgen/me/rules 의 Rule 중 `enforce_via` 에 `hook: 'Stop'` 이 있는 것만
41
+ * SpikeRule 내부 shape 로 변환해 반환한다.
42
+ *
43
+ * 변환 규칙:
44
+ * - `trigger_keywords_regex` 미지정 → DEFAULT_STOP_TRIGGER_RE (shared)
45
+ * - `trigger_exclude_regex` 미지정 → DEFAULT_STOP_EXCLUDE_RE (shared)
46
+ * - verifier.kind 는 `self_check_prompt` 또는 `artifact_check` 지원
47
+ * - 그 외 verifier 는 skip (PreToolUse 전용 tool_arg_regex 등)
48
+ */
49
+ export declare function rulesFromStore(rules: Rule[]): SpikeRule[];
50
+ /** Pure core — 단위 테스트용. stdin/IO 없음. */
51
+ export declare function evaluateStop(lastAssistantMessage: string, rules: SpikeRule[]): {
52
+ action: 'approve';
53
+ hit: null;
54
+ } | {
55
+ action: 'block';
56
+ hit: SpikeRule;
57
+ reason: string;
58
+ };
59
+ /**
60
+ * 같은 (session, rule) 조합의 연속 block 카운트. approve 가 일어나면 0 으로 초기화.
61
+ * export for tests. 부수효과: 디렉토리 생성 + 파일 쓰기.
62
+ */
63
+ export declare function incrementBlockCount(sessionId: string, ruleId: string): number;
64
+ export declare function resetBlockCount(sessionId: string, ruleId: string): void;
65
+ /**
66
+ * R9-PA2: approve 시점에 같은 session 의 pending block 을 찾아 ack 이벤트로 기록.
67
+ * Mech-B 의 핵심 가치(block → retract → pass)가 실제 작동했음을 관측 가능하게 한다.
68
+ * Best-effort: 실패해도 approve 자체는 영향받지 않는다.
69
+ *
70
+ * 기록 후 block-count 파일은 cleanup — 같은 session 의 같은 rule 이 다시 block 되면
71
+ * 새로운 카운트로 시작 (block-count 의미 보존).
72
+ */
73
+ export declare function acknowledgeSessionBlocks(sessionId: string): number;
74
+ export declare function logDriftEvent(event: {
75
+ kind: string;
76
+ session_id: string;
77
+ rule_id: string;
78
+ count: number;
79
+ reason_preview?: string;
80
+ message_preview?: string;
81
+ }): void;
82
+ export declare function getStuckLoopThreshold(): number;
83
+ export declare function main(): Promise<void>;
84
+ export {};