@wooojin/forgen 0.4.0 → 0.4.3

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 (187) hide show
  1. package/.claude-plugin/plugin.json +5 -5
  2. package/CHANGELOG.md +194 -15
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.ja.md +74 -9
  5. package/README.ko.md +77 -12
  6. package/README.md +127 -25
  7. package/README.zh.md +43 -9
  8. package/assets/README.md +86 -0
  9. package/assets/architecture.svg +100 -0
  10. package/assets/banner.png +0 -0
  11. package/assets/banner.svg +53 -0
  12. package/assets/demo/01-install.gif +0 -0
  13. package/assets/demo/01-install.tape +54 -0
  14. package/assets/demo/02-compound-learning.gif +0 -0
  15. package/assets/demo/02-compound-learning.tape +50 -0
  16. package/assets/demo/03-forge-personalization.gif +0 -0
  17. package/assets/demo/03-forge-personalization.tape +64 -0
  18. package/assets/demo/before-after.gif +0 -0
  19. package/assets/demo/before-after.tape +98 -0
  20. package/assets/demo-preview.svg +96 -0
  21. package/assets/icon.png +0 -0
  22. package/{hooks → assets/shared}/hook-registry.json +2 -1
  23. package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
  24. package/dist/checks/conclusion-verification-ratio.js +86 -0
  25. package/dist/checks/fact-vs-agreement.d.ts +47 -0
  26. package/dist/checks/fact-vs-agreement.js +92 -0
  27. package/dist/checks/self-score-deflation.d.ts +38 -0
  28. package/dist/checks/self-score-deflation.js +108 -0
  29. package/dist/cli.js +98 -6
  30. package/dist/core/auto-compound-runner.js +137 -49
  31. package/dist/core/behavior-classifier.d.ts +28 -0
  32. package/dist/core/behavior-classifier.js +46 -0
  33. package/dist/core/dashboard.d.ts +7 -0
  34. package/dist/core/dashboard.js +41 -2
  35. package/dist/core/doctor.js +118 -5
  36. package/dist/core/extraction-notice.d.ts +18 -0
  37. package/dist/core/extraction-notice.js +64 -0
  38. package/dist/core/git-stats.d.ts +36 -0
  39. package/dist/core/git-stats.js +79 -0
  40. package/dist/core/harness.d.ts +1 -1
  41. package/dist/core/harness.js +27 -20
  42. package/dist/core/host-detect.d.ts +42 -0
  43. package/dist/core/host-detect.js +68 -0
  44. package/dist/core/init-cli.d.ts +26 -0
  45. package/dist/core/init-cli.js +104 -0
  46. package/dist/core/init.js +17 -0
  47. package/dist/core/inspect-cli.js +1 -2
  48. package/dist/core/installer.js +2 -2
  49. package/dist/core/migrate-cli.d.ts +11 -0
  50. package/dist/core/migrate-cli.js +53 -0
  51. package/dist/core/migrate-evidence-host.d.ts +36 -0
  52. package/dist/core/migrate-evidence-host.js +49 -0
  53. package/dist/core/paths.d.ts +8 -1
  54. package/dist/core/paths.js +11 -2
  55. package/dist/core/recall-cli.d.ts +26 -0
  56. package/dist/core/recall-cli.js +125 -0
  57. package/dist/core/recall-reference-detector.d.ts +43 -0
  58. package/dist/core/recall-reference-detector.js +65 -0
  59. package/dist/core/settings-injector.js +4 -2
  60. package/dist/core/spawn.d.ts +1 -1
  61. package/dist/core/spawn.js +4 -11
  62. package/dist/core/stats-cli.d.ts +21 -0
  63. package/dist/core/stats-cli.js +133 -10
  64. package/dist/core/trust-layer-intent.d.ts +35 -0
  65. package/dist/core/trust-layer-intent.js +30 -0
  66. package/dist/core/types.d.ts +1 -1
  67. package/dist/core/uninstall.js +2 -1
  68. package/dist/engine/compound-cli.js +1 -0
  69. package/dist/engine/compound-export.js +8 -3
  70. package/dist/engine/compound-extractor.js +7 -9
  71. package/dist/engine/learn-cli.js +5 -6
  72. package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
  73. package/dist/engine/lifecycle/bypass-detector.js +57 -5
  74. package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
  75. package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
  76. package/dist/engine/lifecycle/orchestrator.js +2 -2
  77. package/dist/engine/lifecycle/signals.js +6 -6
  78. package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
  79. package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
  80. package/dist/engine/skill-promoter.js +3 -6
  81. package/dist/fgx.js +2 -1
  82. package/dist/forge/evidence-processor.js +12 -0
  83. package/dist/forge/onboarding.d.ts +3 -2
  84. package/dist/forge/onboarding.js +3 -2
  85. package/dist/hooks/context-guard.js +1 -1
  86. package/dist/hooks/dangerous-patterns.json +3 -3
  87. package/dist/hooks/db-guard.js +21 -5
  88. package/dist/hooks/forge-loop-progress.d.ts +9 -0
  89. package/dist/hooks/forge-loop-progress.js +38 -0
  90. package/dist/hooks/hook-registry.js +1 -1
  91. package/dist/hooks/hooks-generator.d.ts +15 -1
  92. package/dist/hooks/hooks-generator.js +18 -16
  93. package/dist/hooks/intent-classifier.js +1 -1
  94. package/dist/hooks/keyword-detector.js +2 -2
  95. package/dist/hooks/notepad-injector.js +1 -1
  96. package/dist/hooks/permission-handler.js +1 -1
  97. package/dist/hooks/post-tool-failure.js +1 -1
  98. package/dist/hooks/post-tool-use.d.ts +7 -1
  99. package/dist/hooks/post-tool-use.js +50 -23
  100. package/dist/hooks/pre-compact.js +2 -2
  101. package/dist/hooks/pre-tool-use.d.ts +7 -0
  102. package/dist/hooks/pre-tool-use.js +28 -10
  103. package/dist/hooks/rate-limiter.js +3 -3
  104. package/dist/hooks/secret-filter.js +1 -1
  105. package/dist/hooks/session-recovery.js +12 -1
  106. package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
  107. package/dist/hooks/shared/blocking-allowlist.js +38 -0
  108. package/dist/hooks/shared/command-parser.d.ts +44 -0
  109. package/dist/hooks/shared/command-parser.js +50 -0
  110. package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
  111. package/dist/hooks/shared/forge-loop-state.js +116 -0
  112. package/dist/hooks/shared/hook-response.d.ts +30 -2
  113. package/dist/hooks/shared/hook-response.js +61 -3
  114. package/dist/hooks/skill-injector.js +2 -2
  115. package/dist/hooks/slop-detector.js +2 -2
  116. package/dist/hooks/solution-injector.d.ts +9 -0
  117. package/dist/hooks/solution-injector.js +48 -5
  118. package/dist/hooks/stop-guard.js +152 -13
  119. package/dist/hooks/subagent-tracker.js +1 -1
  120. package/dist/host/capabilities-claude.d.ts +8 -0
  121. package/dist/host/capabilities-claude.js +46 -0
  122. package/dist/host/capabilities-codex.d.ts +11 -0
  123. package/dist/host/capabilities-codex.js +50 -0
  124. package/dist/host/capabilities-registry.d.ts +11 -0
  125. package/dist/host/capabilities-registry.js +30 -0
  126. package/dist/host/codex-adapter.d.ts +8 -5
  127. package/dist/host/codex-adapter.js +10 -82
  128. package/dist/host/codex-output-parser.d.ts +39 -0
  129. package/dist/host/codex-output-parser.js +75 -0
  130. package/dist/host/exec-host.d.ts +54 -0
  131. package/dist/host/exec-host.js +92 -0
  132. package/dist/host/host-runtime.d.ts +37 -0
  133. package/dist/host/host-runtime.js +51 -0
  134. package/dist/host/install-claude.d.ts +35 -0
  135. package/dist/host/install-claude.js +238 -0
  136. package/dist/host/install-codex.d.ts +44 -0
  137. package/dist/host/install-codex.js +276 -0
  138. package/dist/host/install-orchestrator.d.ts +34 -0
  139. package/dist/host/install-orchestrator.js +126 -0
  140. package/dist/host/invoke-agent.d.ts +27 -0
  141. package/dist/host/invoke-agent.js +115 -0
  142. package/dist/host/parity-harness.d.ts +62 -0
  143. package/dist/host/parity-harness.js +283 -0
  144. package/dist/host/projection.d.ts +35 -0
  145. package/dist/host/projection.js +126 -0
  146. package/dist/i18n/index.js +3 -5
  147. package/dist/mcp/server.js +11 -0
  148. package/dist/mcp/tools.js +47 -0
  149. package/dist/services/session.d.ts +6 -3
  150. package/dist/services/session.js +33 -4
  151. package/dist/store/evidence-store.d.ts +1 -0
  152. package/dist/store/evidence-store.js +45 -3
  153. package/dist/store/host-mismatch.d.ts +42 -0
  154. package/dist/store/host-mismatch.js +65 -0
  155. package/dist/store/implicit-feedback-store.d.ts +59 -0
  156. package/dist/store/implicit-feedback-store.js +153 -0
  157. package/dist/store/profile-store.d.ts +29 -0
  158. package/dist/store/profile-store.js +53 -0
  159. package/dist/store/rule-store.js +8 -0
  160. package/dist/store/types.d.ts +13 -0
  161. package/hooks/hooks.json +6 -1
  162. package/package.json +7 -5
  163. package/plugin.json +4 -4
  164. package/scripts/postinstall.js +100 -25
  165. /package/{agents → assets/claude/agents}/analyst.md +0 -0
  166. /package/{agents → assets/claude/agents}/architect.md +0 -0
  167. /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
  168. /package/{agents → assets/claude/agents}/critic.md +0 -0
  169. /package/{agents → assets/claude/agents}/debugger.md +0 -0
  170. /package/{agents → assets/claude/agents}/designer.md +0 -0
  171. /package/{agents → assets/claude/agents}/executor.md +0 -0
  172. /package/{agents → assets/claude/agents}/explore.md +0 -0
  173. /package/{agents → assets/claude/agents}/git-master.md +0 -0
  174. /package/{agents → assets/claude/agents}/planner.md +0 -0
  175. /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
  176. /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
  177. /package/{agents → assets/claude/agents}/verifier.md +0 -0
  178. /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
  179. /package/{commands → assets/claude/commands}/calibrate.md +0 -0
  180. /package/{commands → assets/claude/commands}/code-review.md +0 -0
  181. /package/{commands → assets/claude/commands}/compound.md +0 -0
  182. /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
  183. /package/{commands → assets/claude/commands}/docker.md +0 -0
  184. /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
  185. /package/{commands → assets/claude/commands}/learn.md +0 -0
  186. /package/{commands → assets/claude/commands}/retro.md +0 -0
  187. /package/{commands → assets/claude/commands}/ship.md +0 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Forge Loop State — RC6 가드 (US-M1)
3
+ *
4
+ * 직전 forge-loop 의 findings 또는 진행 중 stories 를 ≤1KB 요약으로 렌더한다.
5
+ * SessionStart 와 UserPromptSubmit 두 hook 이 공유하는 단일 진입점.
6
+ *
7
+ * RC6 자기증거: 본 세션 R1 에서 head -80 으로 forge-loop.json 을 읽어 findings
8
+ * 8줄(line 92~99)이 잘렸음. 결과적으로 직전 결론을 컨텍스트에 못 가져 같은
9
+ * 가설을 재발. 이 모듈은 그 회귀를 시스템 레벨에서 차단한다.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { STATE_DIR } from '../../core/paths.js';
14
+ const FORGE_LOOP_PATH = path.join(STATE_DIR, 'forge-loop.json');
15
+ const SOFT_STALE_MS = 24 * 60 * 60 * 1000;
16
+ const HARD_STALE_MS = 7 * 24 * 60 * 60 * 1000;
17
+ const MAX_INJECT_BYTES = 1024;
18
+ const MAX_TASK_CHARS = 240;
19
+ const MAX_FINDING_CHARS = 240;
20
+ const MAX_PENDING = 5;
21
+ export function readForgeLoopState(filePath = FORGE_LOOP_PATH) {
22
+ try {
23
+ if (!fs.existsSync(filePath))
24
+ return null;
25
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
26
+ if (typeof raw !== 'object' || raw === null)
27
+ return null;
28
+ return raw;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function ageMs(state, now = Date.now()) {
35
+ const ts = state.completedAt ?? state.startedAt;
36
+ if (!ts)
37
+ return Number.POSITIVE_INFINITY;
38
+ const t = new Date(ts).getTime();
39
+ if (Number.isNaN(t))
40
+ return Number.POSITIVE_INFINITY;
41
+ return now - t;
42
+ }
43
+ function clipBlock(block) {
44
+ if (block.length <= MAX_INJECT_BYTES)
45
+ return block;
46
+ return `${block.slice(0, MAX_INJECT_BYTES - 3)}...`;
47
+ }
48
+ function escXml(s) {
49
+ return s.replace(/[<>&"]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' })[c] ?? c);
50
+ }
51
+ /** SessionStart 용 — 완료된 forge-loop 의 findings 또는 진행 중 stories 요약. */
52
+ export function renderForgeLoopForSession(state, now = Date.now()) {
53
+ if (!state)
54
+ return null;
55
+ const age = ageMs(state, now);
56
+ if (age > HARD_STALE_MS)
57
+ return null;
58
+ const stale = age > SOFT_STALE_MS;
59
+ const lines = [];
60
+ const task = String(state.task ?? '').trim();
61
+ if (task)
62
+ lines.push(`Task: ${escXml(task.slice(0, MAX_TASK_CHARS))}`);
63
+ if (state.active && Array.isArray(state.stories)) {
64
+ const total = state.stories.length;
65
+ const done = state.stories.filter(s => s?.passes).length;
66
+ lines.push(`Status: in-progress ${done}/${total}${stale ? ' (stale)' : ''}`);
67
+ const pending = state.stories
68
+ .filter(s => !s?.passes)
69
+ .slice(0, MAX_PENDING)
70
+ .map(s => `- ${escXml(String(s.id))}: ${escXml(String(s.title))}`);
71
+ if (pending.length) {
72
+ lines.push('Pending:');
73
+ lines.push(...pending);
74
+ }
75
+ }
76
+ else if (state.findings && typeof state.findings === 'object') {
77
+ lines.push(`Status: completed${stale ? ' (stale)' : ''}`);
78
+ for (const [id, val] of Object.entries(state.findings)) {
79
+ const text = String(val ?? '').slice(0, MAX_FINDING_CHARS);
80
+ if (text)
81
+ lines.push(`- ${escXml(id)}: ${escXml(text)}`);
82
+ }
83
+ }
84
+ else {
85
+ return null;
86
+ }
87
+ if (lines.length === 0)
88
+ return null;
89
+ const tag = stale ? '<forge-loop-state stale="true">' : '<forge-loop-state>';
90
+ const body = lines.join('\n');
91
+ return clipBlock(`${tag}\n${body}\n</forge-loop-state>`);
92
+ }
93
+ /** UserPromptSubmit 용 — active=true 시에만 짧은 진행 상황 1~2줄. */
94
+ export function renderForgeLoopForPrompt(state, now = Date.now()) {
95
+ if (!state || !state.active || !Array.isArray(state.stories))
96
+ return null;
97
+ const age = ageMs(state, now);
98
+ if (age > HARD_STALE_MS)
99
+ return null;
100
+ const total = state.stories.length;
101
+ const done = state.stories.filter(s => s?.passes).length;
102
+ const next = state.stories.find(s => !s?.passes);
103
+ if (!next)
104
+ return null;
105
+ const stale = age > SOFT_STALE_MS;
106
+ const tag = stale ? '<forge-loop-active stale="true">' : '<forge-loop-active>';
107
+ const body = `Progress: ${done}/${total} | next: ${escXml(String(next.id))} ${escXml(String(next.title))}`;
108
+ return clipBlock(`${tag}\n${body}\n</forge-loop-active>`);
109
+ }
110
+ /** 테스트 노출용 상수 — 회귀 시 임계값 변경 즉시 감지. */
111
+ export const FORGE_LOOP_LIMITS = {
112
+ SOFT_STALE_MS,
113
+ HARD_STALE_MS,
114
+ MAX_INJECT_BYTES,
115
+ MAX_PENDING,
116
+ };
@@ -18,8 +18,13 @@ export declare function approve(): string;
18
18
  /**
19
19
  * 통과 + 모델에 컨텍스트 주입.
20
20
  * UserPromptSubmit, SessionStart 이벤트에서만 모델에 도달함.
21
+ *
22
+ * H1 (v0.4.1): optional `userNotice` 로 사용자 UI (systemMessage) 에도 동시
23
+ * 1줄 노출. additionalContext 는 모델 전용이라 기존 recall hit 이 8,000+ 번
24
+ * 주입되었는데도 사용자는 0 건을 봤음. userNotice 로 같은 hit 을 사용자
25
+ * 에게 가시화한다.
21
26
  */
22
- export declare function approveWithContext(context: string, eventName: string): string;
27
+ export declare function approveWithContext(context: string, eventName: string, userNotice?: string): string;
23
28
  /**
24
29
  * 통과 + UI 경고 표시 (모델에는 전달되지 않음).
25
30
  * PostToolUse, PreToolUse 경고 등 모델 도달이 불필요한 경우 사용.
@@ -29,6 +34,24 @@ export declare function approveWithWarning(warning: string): string;
29
34
  export declare function deny(reason: string): string;
30
35
  /** 사용자 확인 요청 (PreToolUse 전용) */
31
36
  export declare function ask(reason: string): string;
37
+ /**
38
+ * P3' enforcement helper (2026-04-27)
39
+ *
40
+ * ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
41
+ *
42
+ * 사용:
43
+ * ```ts
44
+ * import { canBlock } from './blocking-allowlist.js';
45
+ * if (someCondition) {
46
+ * console.log(blockOrObserve(hookName, 'reason', logCallback));
47
+ * return;
48
+ * }
49
+ * ```
50
+ *
51
+ * 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
52
+ * 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
53
+ */
54
+ export declare function denyOrObserve(hookName: string, reason: string, observer?: (msg: string) => void): string;
32
55
  /**
33
56
  * Stop hook only — block the agent from stopping and feed a self-check
34
57
  * question back to Claude so the current session resumes with new guidance.
@@ -44,6 +67,11 @@ export declare function blockStop(reason: string, systemMessage?: string): strin
44
67
  * fail-open with error tracking: 에러 시 안전하게 통과하되, 실패 정보를 기록.
45
68
  * forgen doctor의 Hook Health 섹션에서 실패 이력을 표시할 수 있도록 JSONL 로그에 기록.
46
69
  *
70
+ * v0.4.1 (2026-04-24): optional `err` 매개변수 추가. 실 데이터상 106건의 hook 에러가
71
+ * 누적됐으나 전부 `{hook,at}` 만이라 근원 조사 불가했다. 이제 `error`/`stack` 을
72
+ * 함께 기록해 `forgen doctor` 가 원인 카테고리별로 빈도 surface 가능.
73
+ * payload 는 한 줄 cap(400자)로 잘라 JSONL 크기 폭주 방지.
74
+ *
47
75
  * @fail-open: hook failure must never block the user's workflow
48
76
  */
49
- export declare function failOpenWithTracking(hookName: string): string;
77
+ export declare function failOpenWithTracking(hookName: string, err?: unknown): string;
@@ -16,6 +16,7 @@
16
16
  import * as fs from 'node:fs';
17
17
  import * as path from 'node:path';
18
18
  import { STATE_DIR } from '../../core/paths.js';
19
+ import { canBlock } from './blocking-allowlist.js';
19
20
  /** 통과 응답 (컨텍스트 없음, 모든 이벤트 공통) */
20
21
  export function approve() {
21
22
  return JSON.stringify({ continue: true });
@@ -23,11 +24,17 @@ export function approve() {
23
24
  /**
24
25
  * 통과 + 모델에 컨텍스트 주입.
25
26
  * UserPromptSubmit, SessionStart 이벤트에서만 모델에 도달함.
27
+ *
28
+ * H1 (v0.4.1): optional `userNotice` 로 사용자 UI (systemMessage) 에도 동시
29
+ * 1줄 노출. additionalContext 는 모델 전용이라 기존 recall hit 이 8,000+ 번
30
+ * 주입되었는데도 사용자는 0 건을 봤음. userNotice 로 같은 hit 을 사용자
31
+ * 에게 가시화한다.
26
32
  */
27
- export function approveWithContext(context, eventName) {
33
+ export function approveWithContext(context, eventName, userNotice) {
28
34
  return JSON.stringify({
29
35
  continue: true,
30
36
  hookSpecificOutput: { hookEventName: eventName, additionalContext: context },
37
+ ...(userNotice ? { systemMessage: userNotice } : {}),
31
38
  });
32
39
  }
33
40
  /**
@@ -59,6 +66,36 @@ export function ask(reason) {
59
66
  },
60
67
  });
61
68
  }
69
+ /**
70
+ * P3' enforcement helper (2026-04-27)
71
+ *
72
+ * ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
73
+ *
74
+ * 사용:
75
+ * ```ts
76
+ * import { canBlock } from './blocking-allowlist.js';
77
+ * if (someCondition) {
78
+ * console.log(blockOrObserve(hookName, 'reason', logCallback));
79
+ * return;
80
+ * }
81
+ * ```
82
+ *
83
+ * 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
84
+ * 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
85
+ */
86
+ export function denyOrObserve(hookName, reason, observer) {
87
+ if (canBlock(hookName)) {
88
+ return deny(reason);
89
+ }
90
+ // ALLOW-LIST 외 — log only, approve
91
+ if (observer) {
92
+ try {
93
+ observer(`[${hookName}] would-deny (observe-only): ${reason}`);
94
+ }
95
+ catch { /* fail-open */ }
96
+ }
97
+ return approve();
98
+ }
62
99
  /**
63
100
  * Stop hook only — block the agent from stopping and feed a self-check
64
101
  * question back to Claude so the current session resumes with new guidance.
@@ -81,13 +118,34 @@ export function blockStop(reason, systemMessage) {
81
118
  * fail-open with error tracking: 에러 시 안전하게 통과하되, 실패 정보를 기록.
82
119
  * forgen doctor의 Hook Health 섹션에서 실패 이력을 표시할 수 있도록 JSONL 로그에 기록.
83
120
  *
121
+ * v0.4.1 (2026-04-24): optional `err` 매개변수 추가. 실 데이터상 106건의 hook 에러가
122
+ * 누적됐으나 전부 `{hook,at}` 만이라 근원 조사 불가했다. 이제 `error`/`stack` 을
123
+ * 함께 기록해 `forgen doctor` 가 원인 카테고리별로 빈도 surface 가능.
124
+ * payload 는 한 줄 cap(400자)로 잘라 JSONL 크기 폭주 방지.
125
+ *
84
126
  * @fail-open: hook failure must never block the user's workflow
85
127
  */
86
- export function failOpenWithTracking(hookName) {
128
+ export function failOpenWithTracking(hookName, err) {
87
129
  try {
88
130
  fs.mkdirSync(STATE_DIR, { recursive: true });
89
131
  const logPath = path.join(STATE_DIR, 'hook-errors.jsonl');
90
- const entry = JSON.stringify({ hook: hookName, at: Date.now() });
132
+ const payload = { hook: hookName, at: Date.now() };
133
+ if (err !== undefined && err !== null) {
134
+ if (err instanceof Error) {
135
+ payload.error = err.message.slice(0, 400);
136
+ if (err.stack) {
137
+ // 스택 첫 3줄만 — 어느 파일/라인에서 throw 됐는지만 알면 충분.
138
+ payload.stack = err.stack.split('\n').slice(0, 3).join(' | ').slice(0, 400);
139
+ }
140
+ const maybeCode = err.code;
141
+ if (typeof maybeCode === 'string')
142
+ payload.code = maybeCode;
143
+ }
144
+ else {
145
+ payload.error = String(err).slice(0, 400);
146
+ }
147
+ }
148
+ const entry = JSON.stringify(payload);
91
149
  fs.appendFileSync(logPath, entry + '\n');
92
150
  }
93
151
  catch { /* fail-open: tracking itself must not throw */ }
@@ -181,7 +181,7 @@ function collectSkills() {
181
181
  const skills = [];
182
182
  const seen = new Map(); // name → source dir
183
183
  // 패키지 내장 스킬 경로 (dist/../skills/)
184
- const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
184
+ const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'claude', 'commands');
185
185
  // 프로젝트 .forgen > 프로젝트 .compound > 개인 > 글로벌 > 패키지 내장
186
186
  const dirs = [
187
187
  path.join(process.cwd(), '.forgen', 'skills'),
@@ -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
  });
@@ -22,7 +22,16 @@ import * as fs from 'node:fs';
22
22
  import * as path from 'node:path';
23
23
  import * as os from 'node:os';
24
24
  import { readStdinJSON } from './shared/read-stdin.js';
25
- import { approve, blockStop, failOpenWithTracking } from './shared/hook-response.js';
25
+ import { approve, approveWithWarning, blockStop, failOpenWithTracking } from './shared/hook-response.js';
26
+ import { takeLastExtractionNotice } from '../core/extraction-notice.js';
27
+ import { checkConclusionVerificationRatio } from '../checks/conclusion-verification-ratio.js';
28
+ import { checkSelfScoreInflation } from '../checks/self-score-deflation.js';
29
+ import { checkFactVsAgreement } from '../checks/fact-vs-agreement.js';
30
+ import { STATE_DIR } from '../core/paths.js';
31
+ import { sanitizeId } from './shared/sanitize-id.js';
32
+ import { detectRecallReferences } from '../core/recall-reference-detector.js';
33
+ import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
34
+ import { atomicWriteJSON } from './shared/atomic-write.js';
26
35
  import { recordHookTiming } from './shared/hook-timing.js';
27
36
  import { isHookEnabled } from './hook-config.js';
28
37
  import { loadActiveRules } from '../store/rule-store.js';
@@ -38,9 +47,9 @@ import { DEFAULT_STOP_TRIGGER_RE, DEFAULT_STOP_EXCLUDE_RE } from './shared/stop-
38
47
  * ADR-002 Meta 트리거(규칙 자동 강등)로 연결한다.
39
48
  */
40
49
  const STUCK_LOOP_THRESHOLD = 3;
41
- const BLOCK_COUNT_DIR = path.join(os.homedir(), '.forgen', 'state', 'enforcement', 'block-count');
42
- const DRIFT_LOG = path.join(os.homedir(), '.forgen', 'state', 'enforcement', 'drift.jsonl');
43
- const ACK_LOG = path.join(os.homedir(), '.forgen', 'state', 'enforcement', 'acknowledgments.jsonl');
50
+ const BLOCK_COUNT_DIR = path.join(STATE_DIR, 'enforcement', 'block-count');
51
+ const DRIFT_LOG = path.join(STATE_DIR, 'enforcement', 'drift.jsonl');
52
+ const ACK_LOG = path.join(STATE_DIR, 'enforcement', 'acknowledgments.jsonl');
44
53
  /**
45
54
  * Spike scenarios.json 로더 — FORGEN_SPIKE_RULES 명시 시에만 로드.
46
55
  * H1 (2026-04-22): 이전에는 process.cwd()/tests/spike/... 를 기본 폴백했으나,
@@ -213,7 +222,7 @@ function evaluateVerifier(rule) {
213
222
  * 루트 밖 경로는 존재 여부와 무관하게 false 반환.
214
223
  */
215
224
  function artifactFresh(relOrAbs, maxAgeS) {
216
- const homeBase = path.join(os.homedir(), '.forgen', 'state');
225
+ const homeBase = STATE_DIR;
217
226
  const projectBase = path.resolve(process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd(), '.forgen', 'state');
218
227
  const allowedRoots = [homeBase, projectBase];
219
228
  let p = relOrAbs;
@@ -397,22 +406,152 @@ export function getStuckLoopThreshold() {
397
406
  return env;
398
407
  return STUCK_LOOP_THRESHOLD;
399
408
  }
409
+ /**
410
+ * H4 완결 (2026-04-24): recall_referenced emit 경로.
411
+ * Stop hook 에서 Claude 의 직전 응답 텍스트와 이 세션의 injection-cache 를 대조해,
412
+ * 주입된 솔루션 이름이 응답에 등장하면 `recall_referenced` 이벤트 기록. 동일
413
+ * 솔루션 중복 emit 방지용으로 injection-cache 의 해당 엔트리에 `_referenced: true`
414
+ * 플래그 세팅 후 atomic 재기록. fail-open — 어떤 단계든 throw 시 스킵.
415
+ */
416
+ function emitRecallReferencesFailOpen(sessionId, lastMessage) {
417
+ try {
418
+ const cachePath = path.join(STATE_DIR, `injection-cache-${sanitizeId(sessionId)}.json`);
419
+ if (!fs.existsSync(cachePath))
420
+ return;
421
+ const raw = fs.readFileSync(cachePath, 'utf-8');
422
+ const cache = JSON.parse(raw);
423
+ const sols = Array.isArray(cache.solutions) ? cache.solutions : [];
424
+ if (sols.length === 0)
425
+ return;
426
+ const { newlyReferenced } = detectRecallReferences(lastMessage, sols);
427
+ if (newlyReferenced.length === 0)
428
+ return;
429
+ const now = new Date().toISOString();
430
+ for (const name of newlyReferenced) {
431
+ appendImplicitFeedback({
432
+ type: 'recall_referenced',
433
+ category: 'positive',
434
+ solution: name,
435
+ at: now,
436
+ sessionId,
437
+ });
438
+ }
439
+ // 중복 emit 방지 — cache 에 플래그 저장 후 재기록
440
+ const refSet = new Set(newlyReferenced);
441
+ const updated = {
442
+ ...cache,
443
+ solutions: sols.map((s) => (refSet.has(s.name) ? { ...s, _referenced: true } : s)),
444
+ updatedAt: now,
445
+ };
446
+ atomicWriteJSON(cachePath, updated, { mode: 0o600, dirMode: 0o700 });
447
+ }
448
+ catch {
449
+ /* fail-open */
450
+ }
451
+ }
452
+ /**
453
+ * TEST-2 support: post-tool-use 가 저장한 modified-files-{sessionId}.json 에서
454
+ * recentToolNames 윈도우를 로드. 파일이 없거나 깨져도 빈 배열로 fail-open.
455
+ */
456
+ function loadRecentToolNames(sessionId) {
457
+ try {
458
+ const p = path.join(STATE_DIR, `modified-files-${sanitizeId(sessionId)}.json`);
459
+ if (!fs.existsSync(p))
460
+ return [];
461
+ const raw = fs.readFileSync(p, 'utf-8');
462
+ const data = JSON.parse(raw);
463
+ if (Array.isArray(data.recentToolNames)) {
464
+ return data.recentToolNames.filter((n) => typeof n === 'string');
465
+ }
466
+ return [];
467
+ }
468
+ catch {
469
+ return [];
470
+ }
471
+ }
472
+ /**
473
+ * H2: Stop hook approve 시 이전 세션의 auto-compound 추출 결과를 1회만 surface.
474
+ * takeLastExtractionNotice 가 null 이면 일반 approve, 아니면 systemMessage 포함.
475
+ */
476
+ function approveWithOptionalExtractionNotice() {
477
+ const notice = takeLastExtractionNotice();
478
+ if (notice)
479
+ return approveWithWarning(notice);
480
+ return approve();
481
+ }
400
482
  export async function main() {
401
483
  const started = Date.now();
402
484
  try {
403
485
  if (!isHookEnabled(HOOK_NAME)) {
404
- console.log(approve());
486
+ console.log(approveWithOptionalExtractionNotice());
405
487
  return;
406
488
  }
407
489
  const input = await readStdinJSON();
408
490
  const lastMessage = readLastAssistantMessage(input);
409
491
  if (!lastMessage) {
410
- console.log(approve());
492
+ console.log(approveWithOptionalExtractionNotice());
411
493
  return;
412
494
  }
495
+ // H4 완결: 응답 텍스트와 injection-cache 대조해 recall_referenced emit.
496
+ // block/approve 어느 경로이든 동일하게 기록 (참조는 응답 내용이 결정).
497
+ const sessionIdForRef = input?.session_id ?? 'unknown';
498
+ emitRecallReferencesFailOpen(sessionIdForRef, lastMessage);
499
+ // TEST-2/3: rule-free meta guards — FORGEN_USER_CONFIRMED=1 우회 공통.
500
+ if (process.env.FORGEN_USER_CONFIRMED !== '1') {
501
+ const sessionId = input?.session_id ?? 'unknown';
502
+ // TEST-2 (자가 점수 인플레이션): 숫자 점수 상승 선언 + 측정 도구 0회 → block.
503
+ // TEST-3 보다 강한 신호라 먼저 평가.
504
+ const recentTools = loadRecentToolNames(sessionId);
505
+ const score = checkSelfScoreInflation({ text: lastMessage, recentTools });
506
+ if (score.block) {
507
+ recordViolation({
508
+ rule_id: 'builtin:self-score-inflation',
509
+ session_id: sessionId,
510
+ source: 'stop-guard',
511
+ kind: 'block',
512
+ message_preview: lastMessage.slice(0, 120),
513
+ });
514
+ const reasonText = `[forgen:stop-guard/self-score-inflation] ${score.reason}
515
+
516
+ (Override this turn: set FORGEN_USER_CONFIRMED=1 (audited).)`;
517
+ console.log(blockStop(reasonText, 'rule:TEST-2 — self-score inflation'));
518
+ return;
519
+ }
520
+ // TEST-3: 결론/검증 비율 — Claude 가 실제 측정 도구는 돌렸지만 서술이
521
+ // 결론-편향이면 여전히 block.
522
+ const ratio = checkConclusionVerificationRatio({ text: lastMessage });
523
+ if (ratio.block) {
524
+ recordViolation({
525
+ rule_id: 'builtin:conclusion-verification-ratio',
526
+ session_id: sessionId,
527
+ source: 'stop-guard',
528
+ kind: 'block',
529
+ message_preview: lastMessage.slice(0, 120),
530
+ });
531
+ const reasonText = `[forgen:stop-guard/conclusion-ratio] ${ratio.reason}
532
+
533
+ (Override this turn: set FORGEN_USER_CONFIRMED=1 (audited).)`;
534
+ console.log(blockStop(reasonText, 'rule:TEST-3 — conclusion/verification ratio'));
535
+ return;
536
+ }
537
+ // TEST-1: 사실 vs 합의 — fact assertion 키워드가 있으나 측정 도구 호출 0건.
538
+ // 원 design intent (per fact-vs-agreement.ts): alert level only — block 은 TEST-2/3.
539
+ // 여기서는 measurement 신호를 violations.jsonl 에 'alert' kind 로 기록만 (block 안 함).
540
+ // wiring gap 발견 (forgen-eval introspect) → 측정 가능하게 wired up.
541
+ const fva = checkFactVsAgreement({ text: lastMessage, recentTools, minMeasurements: 1 });
542
+ if (fva.alert) {
543
+ recordViolation({
544
+ rule_id: 'builtin:fact-vs-agreement',
545
+ session_id: sessionId,
546
+ source: 'stop-guard',
547
+ kind: 'correction', // alert-level signal (not block) per fact-vs-agreement.ts design
548
+ message_preview: lastMessage.slice(0, 120),
549
+ });
550
+ }
551
+ }
413
552
  const rules = loadStopRules();
414
553
  if (rules.length === 0) {
415
- console.log(approve());
554
+ console.log(approveWithOptionalExtractionNotice());
416
555
  return;
417
556
  }
418
557
  const result = evaluateStop(lastMessage, rules);
@@ -421,7 +560,7 @@ export async function main() {
421
560
  // R9-PA2: 같은 session 에 pending block 이 있었다면 retract→pass 루프가
422
561
  // 실제 작동한 것 — acknowledgment 이벤트로 기록. block-count 는 cleanup.
423
562
  acknowledgeSessionBlocks(sessionId);
424
- console.log(approve());
563
+ console.log(approveWithOptionalExtractionNotice());
425
564
  return;
426
565
  }
427
566
  const { hit, reason } = result;
@@ -433,7 +572,7 @@ export async function main() {
433
572
  kind: 'correction',
434
573
  message_preview: `[FORGEN_USER_CONFIRMED=1 bypass] ${lastMessage.slice(0, 100)}`,
435
574
  });
436
- console.log(approve());
575
+ console.log(approveWithOptionalExtractionNotice());
437
576
  return;
438
577
  }
439
578
  // T2 signal: block 은 rule 위반 증거 — violations.jsonl 에 기록.
@@ -464,13 +603,13 @@ export async function main() {
464
603
  message_preview: lastMessage.slice(0, 120),
465
604
  });
466
605
  resetBlockCount(sessionId, hit.id);
467
- console.log(approve());
606
+ console.log(approveWithOptionalExtractionNotice());
468
607
  return;
469
608
  }
470
609
  console.log(blockStop(reasonWithHint, hit.system_tag));
471
610
  }
472
- catch {
473
- console.log(failOpenWithTracking(HOOK_NAME));
611
+ catch (e) {
612
+ console.log(failOpenWithTracking(HOOK_NAME, e));
474
613
  }
475
614
  finally {
476
615
  recordHookTiming(HOOK_NAME, Date.now() - started, 'Stop');
@@ -86,5 +86,5 @@ async function main() {
86
86
  }
87
87
  main().catch((e) => {
88
88
  process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
89
- console.log(failOpenWithTracking('subagent-tracker'));
89
+ console.log(failOpenWithTracking('subagent-tracker', e));
90
90
  });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Claude HostCapabilities — Multi-Host Core Design §9.0
3
+ *
4
+ * Claude 는 reference host. 모든 TrustLayerIntent 가 supported (identity binding).
5
+ * 본 선언은 *spec 정의 그 자체* 의 코드 표현 — 변경 시 spec §9.0 도 같이 갱신해야 한다.
6
+ */
7
+ import type { HostCapabilities } from '../core/trust-layer-intent.js';
8
+ export declare const claudeCapabilities: HostCapabilities;