@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.
Files changed (166) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +261 -0
  3. package/README.ja.md +16 -12
  4. package/README.ko.md +12 -8
  5. package/README.md +90 -27
  6. package/README.zh.md +16 -12
  7. package/assets/claude/commands/forge-loop.md +7 -1
  8. package/assets/claude/commands/ship.md +18 -1
  9. package/assets/opencode/forgen.ts +52 -0
  10. package/assets/shared/hook-registry.json +210 -22
  11. package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
  12. package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
  13. package/dist/checks/_shared/model-profile.d.ts +25 -0
  14. package/dist/checks/_shared/model-profile.js +63 -0
  15. package/dist/cli.js +99 -153
  16. package/dist/core/auto-compound-runner.d.ts +0 -11
  17. package/dist/core/auto-compound-runner.js +199 -92
  18. package/dist/core/changelog-cli.d.ts +4 -1
  19. package/dist/core/changelog-cli.js +8 -7
  20. package/dist/core/compound-consent.d.ts +15 -0
  21. package/dist/core/compound-consent.js +48 -0
  22. package/dist/core/compound-sweep-cli.d.ts +60 -0
  23. package/dist/core/compound-sweep-cli.js +374 -0
  24. package/dist/core/config-injector.d.ts +16 -1
  25. package/dist/core/config-injector.js +47 -28
  26. package/dist/core/dashboard-cli.js +40 -16
  27. package/dist/core/dashboard.d.ts +3 -4
  28. package/dist/core/dashboard.js +15 -34
  29. package/dist/core/dev-cli.d.ts +13 -0
  30. package/dist/core/dev-cli.js +70 -0
  31. package/dist/core/doctor.d.ts +16 -5
  32. package/dist/core/doctor.js +190 -176
  33. package/dist/core/drift-score.d.ts +2 -0
  34. package/dist/core/drift-score.js +9 -1
  35. package/dist/core/harness.js +115 -43
  36. package/dist/core/health-cli.d.ts +2 -0
  37. package/dist/core/health-cli.js +6 -1
  38. package/dist/core/host-detect.d.ts +3 -1
  39. package/dist/core/host-detect.js +26 -1
  40. package/dist/core/migrate-cli.js +15 -0
  41. package/dist/core/migrate-evidence-host.d.ts +2 -1
  42. package/dist/core/migrate-tenetx.d.ts +50 -0
  43. package/dist/core/migrate-tenetx.js +262 -0
  44. package/dist/core/probe-workflow-cli.d.ts +3 -3
  45. package/dist/core/probe-workflow-cli.js +13 -13
  46. package/dist/core/recall-cli.js +1 -1
  47. package/dist/core/regress-map-cli.js +1 -1
  48. package/dist/core/rendered-rules-manifest.d.ts +29 -0
  49. package/dist/core/rendered-rules-manifest.js +60 -0
  50. package/dist/core/session-store.js +14 -3
  51. package/dist/core/settings-injector.d.ts +3 -0
  52. package/dist/core/settings-injector.js +12 -16
  53. package/dist/core/spawn.d.ts +37 -1
  54. package/dist/core/spawn.js +116 -9
  55. package/dist/core/state-gc.js +1 -0
  56. package/dist/core/status-cli.d.ts +20 -0
  57. package/dist/core/status-cli.js +100 -0
  58. package/dist/core/statusline-cli.d.ts +7 -0
  59. package/dist/core/statusline-cli.js +63 -19
  60. package/dist/core/transcript-summary.d.ts +18 -0
  61. package/dist/core/transcript-summary.js +81 -0
  62. package/dist/core/trust-layer-intent.d.ts +21 -1
  63. package/dist/core/trust-layer-intent.js +7 -0
  64. package/dist/core/types.d.ts +3 -2
  65. package/dist/core/uninstall.js +12 -0
  66. package/dist/core/usage-telemetry.d.ts +7 -1
  67. package/dist/core/usage-telemetry.js +7 -9
  68. package/dist/core/v1-bootstrap.js +1 -1
  69. package/dist/core/watch-cli.js +1 -1
  70. package/dist/engine/compound-extractor.js +10 -0
  71. package/dist/engine/compound-loop.js +55 -7
  72. package/dist/engine/compound-share.d.ts +85 -0
  73. package/dist/engine/compound-share.js +606 -0
  74. package/dist/engine/correction-cluster-runner.d.ts +38 -0
  75. package/dist/engine/correction-cluster-runner.js +188 -0
  76. package/dist/engine/correction-clustering.d.ts +80 -0
  77. package/dist/engine/correction-clustering.js +167 -0
  78. package/dist/engine/enforce-classifier.d.ts +10 -1
  79. package/dist/engine/enforce-classifier.js +111 -22
  80. package/dist/engine/extraction-session.js +10 -3
  81. package/dist/engine/private-filter.d.ts +36 -0
  82. package/dist/engine/private-filter.js +100 -0
  83. package/dist/engine/ranking-pipeline.js +4 -2
  84. package/dist/engine/relevance-gate.d.ts +12 -0
  85. package/dist/engine/relevance-gate.js +12 -0
  86. package/dist/engine/roi-demotion.d.ts +79 -0
  87. package/dist/engine/roi-demotion.js +159 -0
  88. package/dist/engine/solution-format.d.ts +1 -0
  89. package/dist/engine/solution-format.js +26 -0
  90. package/dist/engine/solution-matcher.js +10 -1
  91. package/dist/fgx.js +7 -6
  92. package/dist/forge/cli.js +8 -2
  93. package/dist/hooks/compound-reflection.js +6 -1
  94. package/dist/hooks/context-guard.d.ts +16 -1
  95. package/dist/hooks/context-guard.js +99 -49
  96. package/dist/hooks/hook-config.js +5 -0
  97. package/dist/hooks/hook-registry.d.ts +6 -1
  98. package/dist/hooks/hooks-generator.js +3 -0
  99. package/dist/hooks/post-tool-use.js +2 -3
  100. package/dist/hooks/pre-compact.js +14 -0
  101. package/dist/hooks/pre-tool-use.js +5 -1
  102. package/dist/hooks/session-end.d.ts +34 -0
  103. package/dist/hooks/session-end.js +100 -0
  104. package/dist/hooks/session-recovery.js +18 -1
  105. package/dist/hooks/shared/hook-timing.js +6 -1
  106. package/dist/hooks/shared/stop-triggers.d.ts +29 -2
  107. package/dist/hooks/shared/stop-triggers.js +35 -2
  108. package/dist/hooks/solution-injector.d.ts +28 -0
  109. package/dist/hooks/solution-injector.js +126 -28
  110. package/dist/hooks/stop-guard.js +5 -2
  111. package/dist/hooks/subagent-stop-guard.js +4 -1
  112. package/dist/host/capabilities-claude.js +1 -0
  113. package/dist/host/capabilities-codex.js +1 -0
  114. package/dist/host/capabilities-opencode.d.ts +26 -0
  115. package/dist/host/capabilities-opencode.js +78 -0
  116. package/dist/host/capabilities-registry.d.ts +7 -0
  117. package/dist/host/capabilities-registry.js +14 -0
  118. package/dist/host/codex-rules-context.d.ts +25 -0
  119. package/dist/host/codex-rules-context.js +62 -0
  120. package/dist/host/exec-host.d.ts +14 -3
  121. package/dist/host/exec-host.js +15 -2
  122. package/dist/host/host-binding.d.ts +27 -0
  123. package/dist/host/host-binding.js +11 -0
  124. package/dist/host/host-runtime.js +18 -0
  125. package/dist/host/install-codex.d.ts +66 -0
  126. package/dist/host/install-codex.js +293 -25
  127. package/dist/host/install-opencode.d.ts +38 -0
  128. package/dist/host/install-opencode.js +148 -0
  129. package/dist/host/install-orchestrator.d.ts +4 -1
  130. package/dist/host/install-orchestrator.js +30 -1
  131. package/dist/host/invoke-agent.d.ts +3 -2
  132. package/dist/host/invoke-agent.js +1 -0
  133. package/dist/host/opencode/context-cli.d.ts +15 -0
  134. package/dist/host/opencode/context-cli.js +25 -0
  135. package/dist/host/opencode/guard-cli.d.ts +13 -0
  136. package/dist/host/opencode/guard-cli.js +39 -0
  137. package/dist/host/opencode/plugin/forgen.d.ts +31 -0
  138. package/dist/host/opencode/plugin/forgen.js +60 -0
  139. package/dist/host/opencode/translate.d.ts +49 -0
  140. package/dist/host/opencode/translate.js +96 -0
  141. package/dist/host/parity-harness.d.ts +10 -2
  142. package/dist/host/parity-harness.js +7 -3
  143. package/dist/host/projection.d.ts +37 -3
  144. package/dist/host/projection.js +104 -42
  145. package/dist/mcp/tools.js +17 -4
  146. package/dist/store/evidence-store.d.ts +2 -6
  147. package/dist/store/evidence-store.js +164 -46
  148. package/dist/store/host-mismatch.d.ts +2 -1
  149. package/dist/store/host-mismatch.js +8 -8
  150. package/dist/store/profile-store.d.ts +3 -2
  151. package/dist/store/rule-store.d.ts +41 -0
  152. package/dist/store/rule-store.js +77 -2
  153. package/dist/store/types.d.ts +9 -2
  154. package/hooks/hooks.json +13 -1
  155. package/package.json +3 -2
  156. package/plugin.json +2 -2
  157. package/skills/architecture-decision/SKILL.md +18 -0
  158. package/skills/calibrate/SKILL.md +18 -0
  159. package/skills/code-review/SKILL.md +17 -0
  160. package/skills/compound/SKILL.md +17 -0
  161. package/skills/deep-interview/SKILL.md +7 -0
  162. package/skills/docker/SKILL.md +18 -0
  163. package/skills/forge-loop/SKILL.md +30 -1
  164. package/skills/learn/SKILL.md +15 -0
  165. package/skills/retro/SKILL.md +16 -0
  166. package/skills/ship/SKILL.md +36 -1
@@ -3,7 +3,7 @@ import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR, V1_SESSIONS_DIR } from './paths.js';
6
+ import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, PACKS_DIR, SESSIONS_DIR, STATE_DIR, V1_SESSIONS_DIR } from './paths.js';
7
7
  import { getTimingStats } from '../hooks/shared/hook-timing.js';
8
8
  import { countSessionScopedFiles, pruneState } from './state-gc.js';
9
9
  import { summarizeAllByHost } from '../store/host-mismatch.js';
@@ -52,12 +52,39 @@ function relativeTime(isoString) {
52
52
  }
53
53
  return `${diffDays}d ago`;
54
54
  }
55
+ /** ADR-014 D4 — [Codex Hooks] 섹션: hooks.json forgen 엔트리의 Codex trust 기록 대조 */
56
+ async function renderCodexHookTrust() {
57
+ try {
58
+ const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
59
+ const hooksPath = path.join(codexHome, 'hooks.json');
60
+ if (!fs.existsSync(hooksPath))
61
+ return; // Codex 미설치/미등록 — 섹션 생략
62
+ const { auditCodexHookTrust } = await import('../host/install-codex.js');
63
+ // pkgRoot: dist/core/doctor.js → 두 단계 위. 어차피 FORGEN_HOOK_SCRIPT_MARKER fallback 이 있어 정확도 비의존.
64
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
65
+ const t = auditCodexHookTrust({ hooksPath, configTomlPath: path.join(codexHome, 'config.toml'), pkgRoot });
66
+ console.log(' [Codex Hooks]');
67
+ if (t.total === 0) {
68
+ console.log(' △ hooks.json 에 forgen hook 없음 — forgen install codex');
69
+ }
70
+ else if (t.trusted === t.total) {
71
+ console.log(` ✓ ${t.trusted}/${t.total} forgen hooks trusted by Codex`);
72
+ }
73
+ else if (t.noStateRecorded) {
74
+ console.log(` △ ${t.total} forgen hooks registered, Codex trust 기록 없음 — codex 안에서 /hooks 로 승인 (미승인 훅은 skip 됨)`);
75
+ }
76
+ else {
77
+ console.log(` ✗ ${t.untrusted.length}/${t.total} forgen hooks untrusted (${t.untrusted.slice(0, 4).join(', ')}${t.untrusted.length > 4 ? ', …' : ''}) — codex 안에서 /hooks 로 승인`);
78
+ }
79
+ }
80
+ catch { /* fail-open */ }
81
+ }
55
82
  /** [Codex Parity] 섹션 렌더링 — ~/.forgen/state/parity-result.json 신선도 검사 */
56
83
  function renderCodexParity() {
57
84
  console.log(' [Codex Parity]');
58
85
  const parityPath = path.join(STATE_DIR, 'parity-result.json');
59
86
  if (!fs.existsSync(parityPath)) {
60
- console.log(' △ Codex parity 미실행 — tests/e2e/codex/run-parity.sh 또는 forgen parity codex');
87
+ console.log(' △ Codex parity 미실행 — tests/e2e/codex/run-parity.sh 또는 forgen dev parity codex');
61
88
  return;
62
89
  }
63
90
  let data;
@@ -90,10 +117,51 @@ function renderCodexParity() {
90
117
  console.log(` ✓ Codex parity green (last run: ${timeStr},${version})`);
91
118
  }
92
119
  }
120
+ /** plugin cache 디렉토리에 버전 엔트리가 하나 이상 있는가 (check + repair 재검증 공용) */
121
+ export function pluginCacheOk() {
122
+ const pluginCacheBase = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'forgen-local', 'forgen');
123
+ if (!exists(pluginCacheBase))
124
+ return false;
125
+ try {
126
+ return fs.readdirSync(pluginCacheBase).some(f => {
127
+ try {
128
+ const lstat = fs.lstatSync(path.join(pluginCacheBase, f));
129
+ return lstat.isDirectory() || lstat.isSymbolicLink();
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ });
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ /** installed_plugins.json 의 forgen entry 가 존재하며 installPath 가 살아있는가 */
141
+ export function pluginRegisteredOk() {
142
+ const installedPluginsPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
143
+ if (!exists(installedPluginsPath))
144
+ return false;
145
+ try {
146
+ const installed = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
147
+ const entry = installed?.plugins?.['forgen@forgen-local'];
148
+ if (Array.isArray(entry) && entry.length > 0) {
149
+ const installPath = entry[0]?.installPath;
150
+ return !!installPath && exists(installPath);
151
+ }
152
+ }
153
+ catch { /* ignore */ }
154
+ return false;
155
+ }
93
156
  /**
94
157
  * v0.4.8 (E3): plugin cache / installPath 진단이 실패했을 때 자동 복구.
95
- * forgen 패키지 디렉토리에서 `npm run build` + postinstall 을 차례로 실행.
96
158
  * 실패해도 doctor 자체는 계속 진행 (fail-open).
159
+ *
160
+ * W1-4 수정 (ADR-010, 실측 2026-07-16): 이전 구현은 무조건 `npm run build` 를
161
+ * 먼저 실행했는데, 글로벌 설치엔 devDeps(tsc)가 없어 MODULE_NOT_FOUND 로 실패
162
+ * → postinstall 에 도달하지 못해 캐시가 영원히 복구되지 않았다. published
163
+ * 패키지는 dist 가 이미 있으므로 build 는 dist 부재(dev checkout)시에만 시도.
164
+ * 또한 "실행했다"가 아니라 "복구됐다"를 재검증 후 보고한다.
97
165
  */
98
166
  function attemptPluginRepair() {
99
167
  try {
@@ -102,20 +170,53 @@ function attemptPluginRepair() {
102
170
  const here = path.dirname(fileURLToPath(import.meta.url));
103
171
  const pkgRoot = path.resolve(here, '..', '..');
104
172
  console.log(`\n [Repair] forgen 패키지 자가복구 시도 — ${pkgRoot}`);
105
- execFileSync('npm', ['run', 'build'], { cwd: pkgRoot, stdio: 'inherit' });
173
+ if (!exists(path.join(pkgRoot, 'dist', 'cli.js'))) {
174
+ // dev checkout 등 dist 부재 시에만 build (devDeps 가 있는 환경)
175
+ execFileSync('npm', ['run', 'build'], { cwd: pkgRoot, stdio: 'inherit' });
176
+ }
106
177
  execFileSync('node', ['scripts/postinstall.js'], { cwd: pkgRoot, stdio: 'inherit' });
107
- console.log(' [Repair] 완료. 진단 재실행 권장: forgen doctor');
108
- return true;
178
+ // 결과 재검증 — 성공 주장은 검증된 상태에만 한다.
179
+ const ok = pluginCacheOk() && pluginRegisteredOk();
180
+ if (ok) {
181
+ console.log(' [Repair] ✓ 복구 확인 — plugin cache/registry 재검증 통과');
182
+ }
183
+ else {
184
+ console.warn(' [Repair] ✗ postinstall 은 실행됐지만 재검증 실패 — 수동 확인 필요');
185
+ console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> && node scripts/postinstall.js');
186
+ }
187
+ return ok;
109
188
  }
110
189
  catch (e) {
111
190
  console.warn(` [Repair] 실패: ${e instanceof Error ? e.message : String(e)}`);
112
- console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> && npm run build && node scripts/postinstall.js');
191
+ console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> && node scripts/postinstall.js (dist 부재 시 npm run build 선행)');
113
192
  return false;
114
193
  }
115
194
  }
195
+ /** W1-1: --reclaim 스캔 (읽기 전용) — full/quick 양쪽에서 재사용 */
196
+ async function runReclaimScan() {
197
+ try {
198
+ const { runReclaim, printReclaimResult } = await import('./migrate-tenetx.js');
199
+ printReclaimResult(runReclaim({ cwd: process.cwd(), dryRun: true }));
200
+ console.log(' 실제 회수: forgen migrate tenetx [--yes] [--apply-settings]');
201
+ }
202
+ catch (e) {
203
+ console.warn(` [reclaim] 스캔 실패: ${e instanceof Error ? e.message : String(e)}`);
204
+ }
205
+ console.log();
206
+ }
207
+ /** W1-4: repair 성공 시 Summary 에서 걷어내는 대상 — check() 라벨과 단일 소스 */
208
+ const REPAIRABLE_PLUGIN_LABELS = Object.freeze({
209
+ cache: 'forgen plugin cache',
210
+ registered: 'forgen plugin registered & installPath exists',
211
+ });
116
212
  export async function runDoctor(opts = {}) {
117
213
  failedChecks = [];
118
- console.log('\n Forgen — Diagnostics\n');
214
+ console.log('\n Forgen — Diagnostics');
215
+ // ADR-010 W2-1 경계 선언: 일반 환경 건강(컨텍스트 비용, unused skill/MCP,
216
+ // CLAUDE.md 정리, slow hook)은 native /doctor 의 영역. 이 명령은 forgen
217
+ // 자체 기계(플러그인 캐시·훅 오류·상태 위생)와 효과-측정 게이트만 본다.
218
+ console.log(' 환경 전반 진단은 Claude Code native /doctor 를 사용하세요.');
219
+ console.log(' 이 명령은 forgen 자체 기계와 효과-측정 게이트를 검사합니다.\n');
119
220
  section('Tools');
120
221
  check('claude CLI', commandExists('claude'));
121
222
  check('tmux', commandExists('tmux'));
@@ -125,46 +226,24 @@ export async function runDoctor(opts = {}) {
125
226
  section('Plugins');
126
227
  const ralphLoopInstalled = exists(path.join(os.homedir(), '.claude', 'plugins', 'cache', 'claude-plugins-official', 'ralph-loop'));
127
228
  check('ralph-loop plugin', ralphLoopInstalled, 'Required for ralph mode auto-iteration. Install: claude plugins install ralph-loop');
128
- // forgen 플러그인 캐시 디렉토리 확인 — 훅 실행의 필수 전제
129
- const pluginCacheBase = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'forgen-local', 'forgen');
130
- let forgenPluginCacheOk = false;
131
- if (exists(pluginCacheBase)) {
132
- const versions = fs.readdirSync(pluginCacheBase).filter(f => {
133
- try {
134
- const lstat = fs.lstatSync(path.join(pluginCacheBase, f));
135
- return lstat.isDirectory() || lstat.isSymbolicLink();
136
- }
137
- catch {
138
- return false;
139
- }
140
- });
141
- forgenPluginCacheOk = versions.length > 0;
142
- }
143
- check('forgen plugin cache', forgenPluginCacheOk, opts.repair
229
+ // forgen 플러그인 캐시 / registry 정합성 — 훅 실행의 필수 전제
230
+ const forgenPluginCacheOk = pluginCacheOk();
231
+ check(REPAIRABLE_PLUGIN_LABELS.cache, forgenPluginCacheOk, opts.repair
144
232
  ? 'Hook execution requires plugin cache. Attempting auto-repair (--repair)…'
145
- : 'Hook execution requires plugin cache. Fix: npm run build && node scripts/postinstall.js (or rerun with --repair)');
146
- // installed_plugins.json 정합성 확인
147
- const installedPluginsPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
148
- let pluginRegistered = false;
149
- if (exists(installedPluginsPath)) {
150
- try {
151
- const installed = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
152
- const entry = installed?.plugins?.['forgen@forgen-local'];
153
- if (Array.isArray(entry) && entry.length > 0) {
154
- const installPath = entry[0]?.installPath;
155
- pluginRegistered = !!installPath && exists(installPath);
156
- }
157
- }
158
- catch { /* ignore */ }
159
- }
160
- check('forgen plugin registered & installPath exists', pluginRegistered, opts.repair
233
+ : 'Hook execution requires plugin cache. Fix: node scripts/postinstall.js in the forgen package (or rerun with --repair)');
234
+ const pluginRegistered = pluginRegisteredOk();
235
+ check(REPAIRABLE_PLUGIN_LABELS.registered, pluginRegistered, opts.repair
161
236
  ? 'Plugin registered but installPath missing on disk. Attempting auto-repair (--repair)…'
162
- : 'Plugin registered but installPath missing on disk. Fix: npm run build && node scripts/postinstall.js (or rerun with --repair)');
163
- // v0.4.8 (E3): plugin cache 또는 installPath 가 깨졌고 --repair 가 켜져
164
- // 있으면 build + postinstall 자동 실행. doctor 진단 자체는 계속 진행하여
165
- // 사용자가 다른 health 항목도 한 번에 확인 가능.
237
+ : 'Plugin registered but installPath missing on disk. Fix: node scripts/postinstall.js in the forgen package (or rerun with --repair)');
238
+ // v0.4.8 (E3) + W1-4: plugin cache 또는 installPath 가 깨졌고 --repair 가
239
+ // 켜져 있으면 자가복구. 재검증까지 통과하면 failedChecks 에서 해당 항목을
240
+ // 걷어내 Summary 가 복구된 상태를 정직하게 반영하게 한다.
166
241
  if (opts.repair && (!forgenPluginCacheOk || !pluginRegistered)) {
167
- attemptPluginRepair();
242
+ const repaired = attemptPluginRepair();
243
+ if (repaired) {
244
+ const pluginLabels = new Set(Object.values(REPAIRABLE_PLUGIN_LABELS));
245
+ failedChecks = failedChecks.filter(f => !pluginLabels.has(f.label));
246
+ }
168
247
  }
169
248
  console.log();
170
249
  section('Directories');
@@ -204,6 +283,9 @@ export async function runDoctor(opts = {}) {
204
283
  }
205
284
  console.log();
206
285
  if (opts.quick) {
286
+ // --reclaim 은 읽기 전용 스캔이라 --quick 과 조합 가능 (silent-ignore 방지)
287
+ if (opts.reclaim)
288
+ await runReclaimScan();
207
289
  console.log();
208
290
  if (failedChecks.length === 0) {
209
291
  console.log(' All essential checks passed.\n');
@@ -213,9 +295,32 @@ export async function runDoctor(opts = {}) {
213
295
  }
214
296
  return;
215
297
  }
298
+ // 리뷰 SEV-3 (c): critic-review 룰의 트리거가 stale(skip-review 시그널 미포함)이면 발견 nudge.
299
+ // "고쳤는데 기존 유저에겐 안 닿는" 상태 방지 — 재-bake 명령을 안내한다.
300
+ section('Rules');
301
+ try {
302
+ const [{ loadActiveRules }, { needsCriticTriggerMigration }] = await Promise.all([
303
+ import('../store/rule-store.js'),
304
+ import('../engine/enforce-classifier.js'),
305
+ ]);
306
+ const outdated = loadActiveRules().filter(needsCriticTriggerMigration);
307
+ check('critic-review 룰 트리거 최신', outdated.length === 0, `${outdated.length}개 critic 룰이 구 트리거(skip-review 미포함) — \`forgen rule classify --apply --force\` 로 갱신 권장`);
308
+ }
309
+ catch {
310
+ /* rule-store 로드 실패는 무해 — 스킵 */
311
+ }
312
+ console.log();
216
313
  section('Environment');
217
314
  check('Inside tmux session', !!process.env.TMUX, 'FORGEN auto-compound relies on tmux. Launch: tmux new -s forgen');
218
315
  check('FORGEN_HARNESS env var', (process.env.FORGEN_HARNESS ?? process.env.COMPOUND_HARNESS) === '1', 'Set by `forgen` / `fgx` launcher. Hooks assume harness mode is active.');
316
+ // ADR-012: auto-compound Haiku 추출 동의 상태 고지 (transcript 요약 API 전송 여부).
317
+ try {
318
+ const { getHaikuConsentState } = await import('./compound-consent.js');
319
+ const s = getHaikuConsentState();
320
+ console.log(` auto-compound Haiku 추출: ${s.enabled ? 'ON' : 'OFF'} (${s.source}) — ` +
321
+ `${s.enabled ? '세션 요약이 Haiku 로 전송됨' : '결정론 룰 승급만(egress 0)'}. 변경: forgen compound consent on|off`);
322
+ }
323
+ catch { /* noop */ }
219
324
  console.log();
220
325
  // v0.4.1 파일 확장자 버그 수정: rules 는 .json, behavior 도 대부분 .json 포맷.
221
326
  // 이전에 .md 만 count 해서 실 rules 4개인데 0 으로 표시되는 incident 관찰.
@@ -301,26 +406,28 @@ export async function runDoctor(opts = {}) {
301
406
  console.log(' Unable to read hook error log.');
302
407
  }
303
408
  console.log();
304
- // Hook Timing: performance stats
305
- console.log(' [Hook Timing]');
306
- const timingStats = getTimingStats();
307
- if (timingStats.length === 0) {
308
- console.log(' No timing data collected yet.');
309
- }
310
- else {
311
- console.log(' Hook Count p50ms p95ms max ms');
312
- console.log(` ${'-'.repeat(56)}`);
313
- for (const s of timingStats) {
314
- const hook = s.hook.padEnd(22);
315
- const count = String(s.count).padStart(5);
316
- const p50 = String(s.p50).padStart(7);
317
- const p95 = String(s.p95).padStart(7);
318
- const max = String(s.max).padStart(8);
319
- console.log(` ${hook}${count}${p50}${p95}${max}`);
409
+ // Hook Timing: W2-1 (ADR-010) — slow hook 감지는 native /doctor 가 하므로
410
+ // 표시는 --verbose 로 강등. 수집(hook-timing.jsonl)은 자체 회귀 테스트용으로 유지.
411
+ if (opts.verbose) {
412
+ console.log(' [Hook Timing]');
413
+ const timingStats = getTimingStats();
414
+ if (timingStats.length === 0) {
415
+ console.log(' No timing data collected yet.');
320
416
  }
417
+ else {
418
+ console.log(' Hook Count p50ms p95ms max ms');
419
+ console.log(` ${'-'.repeat(56)}`);
420
+ for (const s of timingStats) {
421
+ const hook = s.hook.padEnd(22);
422
+ const count = String(s.count).padStart(5);
423
+ const p50 = String(s.p50).padStart(7);
424
+ const p95 = String(s.p95).padStart(7);
425
+ const max = String(s.max).padStart(8);
426
+ console.log(` ${hook}${count}${p50}${p95}${max}`);
427
+ }
428
+ }
429
+ console.log();
321
430
  }
322
- console.log();
323
- console.log();
324
431
  // v1: 팀 팩 시스템 제거. 개인 모드만 지원.
325
432
  console.log(' [Pack Connections]');
326
433
  console.log(' v1: Personal mode only (team packs removed)');
@@ -365,107 +472,9 @@ export async function runDoctor(opts = {}) {
365
472
  console.log();
366
473
  }
367
474
  }
368
- // Harness Maturity section
369
- console.log(' [Harness Maturity]');
370
- const cwd = process.cwd();
371
- // 1. Preparation
372
- const hasClaude = fs.existsSync(path.join(cwd, 'CLAUDE.md'));
373
- let rulesCount = 0;
374
- try {
375
- const rulesDir = path.join(cwd, '.claude', 'rules');
376
- if (fs.existsSync(rulesDir)) {
377
- rulesCount = fs.readdirSync(rulesDir).filter(f => f.endsWith('.md')).length;
378
- }
379
- }
380
- catch { /* fail-open */ }
381
- let hooksActive = 0;
382
- try {
383
- const hooksJsonPath = path.join(cwd, 'hooks', 'hooks.json');
384
- if (fs.existsSync(hooksJsonPath)) {
385
- const hooksData = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
386
- if (hooksData.hooks && typeof hooksData.hooks === 'object') {
387
- for (const eventHooks of Object.values(hooksData.hooks)) {
388
- if (Array.isArray(eventHooks)) {
389
- for (const group of eventHooks) {
390
- if (Array.isArray(group.hooks)) {
391
- hooksActive += (group.hooks).length;
392
- }
393
- }
394
- }
395
- }
396
- }
397
- }
398
- }
399
- catch { /* fail-open */ }
400
- const prepL = hasClaude && rulesCount >= 3 && hooksActive > 0 ? 'L3' : hasClaude && hooksActive > 0 ? 'L2' : hasClaude ? 'L1' : 'L0';
401
- // 2. Context
402
- let solutionsCount = 0;
403
- try {
404
- if (exists(ME_SOLUTIONS))
405
- solutionsCount = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md')).length;
406
- }
407
- catch { /* fail-open */ }
408
- let behaviorCount = 0;
409
- try {
410
- if (exists(ME_BEHAVIOR))
411
- behaviorCount = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.md')).length;
412
- }
413
- catch { /* fail-open */ }
414
- const ctxL = solutionsCount >= 5 && behaviorCount >= 3 ? 'L3' : solutionsCount >= 3 || behaviorCount >= 1 ? 'L2' : solutionsCount > 0 || behaviorCount > 0 ? 'L1' : 'L0';
415
- // 3. Execution
416
- const hasSkills = exists(ME_SKILLS);
417
- const execL = hasSkills ? 'L2' : 'L1';
418
- // 4. Validation
419
- const hasTests = fs.existsSync(path.join(cwd, 'tests'));
420
- const hasCI = fs.existsSync(path.join(cwd, '.github', 'workflows'));
421
- const validL = hasTests && hasCI ? 'L3' : hasTests ? 'L2' : 'L1';
422
- // 5. Improvement: reflection rate from solutions
423
- let reflectionRate = 0;
424
- try {
425
- if (exists(ME_SOLUTIONS)) {
426
- const solFiles = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
427
- if (solFiles.length > 0) {
428
- let reflected = 0;
429
- for (const f of solFiles) {
430
- try {
431
- const content = fs.readFileSync(path.join(ME_SOLUTIONS, f), 'utf-8');
432
- const match = content.match(/reflected:\s*(\d+)/);
433
- if (match && parseInt(match[1], 10) > 0)
434
- reflected++;
435
- }
436
- catch { /* skip */ }
437
- }
438
- reflectionRate = Math.round((reflected / solFiles.length) * 100);
439
- }
440
- }
441
- }
442
- catch { /* fail-open */ }
443
- const improvL = reflectionRate > 0 ? 'L3' : solutionsCount > 0 ? 'L2' : 'L1';
444
- const levelIcon = (l) => l === 'L3' ? '✓' : l === 'L2' ? '✓' : l === 'L1' ? '✗' : '✗';
445
- console.log(` Axis Level Detail`);
446
- console.log(` ${'─'.repeat(55)}`);
447
- console.log(` ${levelIcon(prepL)} Preparation ${prepL} CLAUDE.md:${hasClaude ? 'yes' : 'no'}, rules:${rulesCount}, hooks:${hooksActive}`);
448
- console.log(` ${levelIcon(ctxL)} Context ${ctxL} solutions:${solutionsCount}, behavior:${behaviorCount}`);
449
- console.log(` ${levelIcon(execL)} Execution ${execL} skills:${hasSkills ? 'yes' : 'no'}`);
450
- console.log(` ${levelIcon(validL)} Validation ${validL} tests:${hasTests ? 'yes' : 'no'}, CI:${hasCI ? 'yes' : 'no'}`);
451
- console.log(` ${levelIcon(improvL)} Improvement ${improvL} reflection:${reflectionRate}%`);
452
- console.log();
453
- // Quick wins: suggest for lowest scoring axes
454
- const axes = [
455
- { name: 'Preparation', level: prepL, hint: 'Add CLAUDE.md + .claude/rules/ files' },
456
- { name: 'Context', level: ctxL, hint: 'Run /compound to accumulate solutions' },
457
- { name: 'Execution', level: execL, hint: 'Promote solutions to skills' },
458
- { name: 'Validation', level: validL, hint: 'Add tests/ dir and .github/workflows' },
459
- { name: 'Improvement', level: improvL, hint: 'Reflect on existing solutions' },
460
- ];
461
- const quickWins = axes.filter(a => a.level === 'L0' || a.level === 'L1').slice(0, 3);
462
- if (quickWins.length > 0) {
463
- console.log(' Quick Wins (Top 3):');
464
- for (const win of quickWins) {
465
- console.log(` → ${win.name}: ${win.hint}`);
466
- }
467
- console.log();
468
- }
475
+ // W2-1 (ADR-010): Harness Maturity / Quick Wins 섹션 제거 —
476
+ // "CLAUDE.md 추가하세요" 류 일반 셋업 조언은 native /doctor 영역.
477
+ // forgen 고유 진단(아래 State Hygiene, ψ-long, parity 등)만 유지한다.
469
478
  // State bloat check — session-scoped files accumulate until pruned.
470
479
  console.log(' [State Hygiene]');
471
480
  const sessionFiles = countSessionScopedFiles();
@@ -525,7 +534,7 @@ export async function runDoctor(opts = {}) {
525
534
  if (map.available && map.hotspots.length > 0) {
526
535
  const top = map.hotspots.map((h) => `${h.path} (${h.fixHits})`).join(', ');
527
536
  console.log(` → 진앙 후보 top 3: ${top}`);
528
- console.log(' → 전체 보기: forgen regress-map');
537
+ console.log(' → 전체 보기: forgen dev regress-map');
529
538
  }
530
539
  }
531
540
  }
@@ -556,6 +565,8 @@ export async function runDoctor(opts = {}) {
556
565
  console.log(' Unable to read host evidence data.');
557
566
  }
558
567
  console.log();
568
+ // [Codex Hooks] — ADR-014 D4 trust 감사
569
+ await renderCodexHookTrust();
559
570
  // [Codex Parity] — parity-result.json 신선도 검사 (v0.4.2 패턴 확장)
560
571
  renderCodexParity();
561
572
  console.log();
@@ -589,20 +600,23 @@ export async function runDoctor(opts = {}) {
589
600
  console.log(` Unable to read psi-long state: ${e instanceof Error ? e.message : 'unknown'}`);
590
601
  }
591
602
  console.log();
592
- // [Effort (Opus 4.8)] — ADR-009 §5. nudge-only: forgen 은 effort 를 직접 설정할 수
593
- // 없으므로 long-running 컨텍스트(forge-loop)에서 xhigh/ultracode 를 권고만 한다.
594
- console.log(' [Effort (Opus 4.8)]');
603
+ // [Effort] — ADR-009 §5 nudge-only. W2-1 (ADR-010): Sonnet 5 는 adaptive
604
+ // thinking 으로 effort 를 자체 관리하므로, 권고가 실제로 의미 있는 상황
605
+ // (forge-loop 활성 = long-running → xhigh 넛지)에서만 표시한다.
595
606
  try {
596
607
  const loopActive = !!readForgeLoopState()?.active;
597
- const adv = effortAdvisory({ longRunningActive: loopActive });
598
- const icon = adv.recommend === 'xhigh' ? '→' : '✓';
599
- console.log(` ${icon} recommend: ${adv.recommend}`);
600
- console.log(` ${adv.reason}`);
601
- }
602
- catch {
603
- console.log(' Unable to compute effort advisory.');
608
+ if (loopActive) {
609
+ const adv = effortAdvisory({ longRunningActive: true });
610
+ console.log(' [Effort]');
611
+ console.log(` → recommend: ${adv.recommend}`);
612
+ console.log(` ${adv.reason}`);
613
+ console.log();
614
+ }
604
615
  }
605
- console.log();
616
+ catch { /* effort 권고 실패는 침묵 — 부가 정보일 뿐 */ }
617
+ // W1-1 (ADR-010): --reclaim → legacy 규칙 스프롤 스캔 (읽기 전용).
618
+ if (opts.reclaim)
619
+ await runReclaimScan();
606
620
  // [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
607
621
  console.log(' [Summary]');
608
622
  if (failedChecks.length === 0) {
@@ -20,6 +20,8 @@ export interface DriftState {
20
20
  lastWarningAt: number;
21
21
  lastCriticalAt: number;
22
22
  hardCapReached: boolean;
23
+ /** 최근 hardcap 발화 timestamp (쿨다운용, ADR-013). */
24
+ lastHardcapAt?: number;
23
25
  }
24
26
  export interface DriftResult {
25
27
  level: 'normal' | 'warning' | 'critical' | 'hardcap';
@@ -54,9 +54,17 @@ export function evaluateDrift(state, isEdit, isRevert, thresholds = {}) {
54
54
  // Calculate drift score: edit rate 65% + revert rate 35%
55
55
  const rawScore = (state.ewmaEditRate * 65) + (state.ewmaRevertRate * 35);
56
56
  const score = Math.min(100, Math.max(0, Math.round(rawScore)));
57
- // Hard cap
57
+ // Hard cap — ADR-013 버그수정: 이전엔 cooldown 이 없어 50편집 넘으면 **매 편집마다**
58
+ // hardcap(=drift_critical) 을 발화 → drift_critical 이 세션길이 카운터로 전락(2067/2100
59
+ // 이벤트가 score100). critical 처럼 cooldown 게이트를 둬 반복 발화를 억제. 상태
60
+ // (hardCapReached)는 항상 갱신하되, message 는 쿨다운 밖에서만 낸다(null=emitter skip).
58
61
  if (state.totalEdits >= t.hardCapEdits) {
59
62
  state.hardCapReached = true;
63
+ const withinCooldown = now - (state.lastHardcapAt ?? 0) <= t.criticalCooldownMs;
64
+ if (withinCooldown) {
65
+ return { level: 'hardcap', score: 100, message: null };
66
+ }
67
+ state.lastHardcapAt = now;
60
68
  return {
61
69
  level: 'hardcap',
62
70
  score: 100,