@wooojin/forgen 0.2.0 → 0.3.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 (158) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.ja.md +79 -14
  3. package/README.ko.md +100 -14
  4. package/README.md +124 -17
  5. package/README.zh.md +79 -14
  6. package/agents/analyst.md +48 -4
  7. package/agents/architect.md +39 -4
  8. package/agents/code-reviewer.md +107 -77
  9. package/agents/critic.md +47 -4
  10. package/agents/debugger.md +46 -4
  11. package/agents/designer.md +40 -4
  12. package/agents/executor.md +112 -30
  13. package/agents/explore.md +45 -5
  14. package/agents/git-master.md +48 -4
  15. package/agents/planner.md +121 -18
  16. package/agents/test-engineer.md +58 -4
  17. package/agents/verifier.md +92 -77
  18. package/commands/architecture-decision.md +127 -258
  19. package/commands/calibrate.md +225 -0
  20. package/commands/code-review.md +163 -178
  21. package/commands/compound.md +127 -68
  22. package/commands/deep-interview.md +273 -0
  23. package/commands/docker.md +68 -178
  24. package/commands/forge-loop.md +215 -0
  25. package/commands/learn.md +231 -0
  26. package/commands/retro.md +215 -0
  27. package/commands/ship.md +277 -0
  28. package/dist/cli.js +26 -9
  29. package/dist/core/auto-compound-runner.js +14 -0
  30. package/dist/core/config-injector.d.ts +2 -1
  31. package/dist/core/config-injector.js +2 -1
  32. package/dist/core/dashboard.d.ts +108 -0
  33. package/dist/core/dashboard.js +495 -0
  34. package/dist/core/doctor.js +151 -21
  35. package/dist/core/drift-score.d.ts +49 -0
  36. package/dist/core/drift-score.js +87 -0
  37. package/dist/core/harness.d.ts +6 -1
  38. package/dist/core/harness.js +75 -19
  39. package/dist/core/mcp-config.d.ts +2 -0
  40. package/dist/core/mcp-config.js +6 -1
  41. package/dist/core/paths.d.ts +6 -1
  42. package/dist/core/paths.js +18 -2
  43. package/dist/core/spawn.d.ts +3 -2
  44. package/dist/core/spawn.js +27 -8
  45. package/dist/core/types.d.ts +34 -0
  46. package/dist/engine/compound-export.d.ts +41 -0
  47. package/dist/engine/compound-export.js +169 -0
  48. package/dist/engine/compound-lifecycle.d.ts +4 -3
  49. package/dist/engine/compound-lifecycle.js +91 -46
  50. package/dist/engine/compound-loop.js +18 -0
  51. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  52. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  53. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  54. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  55. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  56. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  57. package/dist/engine/meta-learning/runner.d.ts +14 -0
  58. package/dist/engine/meta-learning/runner.js +90 -0
  59. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  60. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  61. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  62. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  63. package/dist/engine/meta-learning/types.d.ts +114 -0
  64. package/dist/engine/meta-learning/types.js +43 -0
  65. package/dist/engine/solution-format.d.ts +2 -2
  66. package/dist/engine/solution-format.js +249 -34
  67. package/dist/engine/solution-index.d.ts +1 -1
  68. package/dist/engine/solution-matcher.d.ts +30 -1
  69. package/dist/engine/solution-matcher.js +235 -45
  70. package/dist/fgx.js +12 -8
  71. package/dist/hooks/context-guard.d.ts +15 -0
  72. package/dist/hooks/context-guard.js +218 -56
  73. package/dist/hooks/db-guard.js +2 -2
  74. package/dist/hooks/hook-config.d.ts +27 -1
  75. package/dist/hooks/hook-config.js +72 -12
  76. package/dist/hooks/hooks-generator.d.ts +3 -0
  77. package/dist/hooks/hooks-generator.js +23 -6
  78. package/dist/hooks/intent-classifier.d.ts +0 -2
  79. package/dist/hooks/intent-classifier.js +32 -18
  80. package/dist/hooks/keyword-detector.js +126 -204
  81. package/dist/hooks/notepad-injector.js +2 -2
  82. package/dist/hooks/permission-handler.js +2 -2
  83. package/dist/hooks/post-tool-failure.js +12 -6
  84. package/dist/hooks/post-tool-handlers.d.ts +1 -1
  85. package/dist/hooks/post-tool-handlers.js +14 -11
  86. package/dist/hooks/post-tool-use.d.ts +11 -0
  87. package/dist/hooks/post-tool-use.js +184 -71
  88. package/dist/hooks/pre-compact.d.ts +11 -1
  89. package/dist/hooks/pre-compact.js +112 -37
  90. package/dist/hooks/pre-tool-use.js +86 -56
  91. package/dist/hooks/rate-limiter.js +3 -3
  92. package/dist/hooks/secret-filter.js +2 -2
  93. package/dist/hooks/session-recovery.js +256 -236
  94. package/dist/hooks/shared/hook-response.d.ts +4 -4
  95. package/dist/hooks/shared/hook-response.js +13 -24
  96. package/dist/hooks/shared/hook-timing.d.ts +15 -0
  97. package/dist/hooks/shared/hook-timing.js +64 -0
  98. package/dist/hooks/skill-injector.d.ts +4 -3
  99. package/dist/hooks/skill-injector.js +47 -16
  100. package/dist/hooks/slop-detector.js +3 -3
  101. package/dist/hooks/solution-injector.js +224 -197
  102. package/dist/hooks/subagent-tracker.js +2 -2
  103. package/dist/host/codex-adapter.d.ts +10 -0
  104. package/dist/host/codex-adapter.js +154 -0
  105. package/dist/mcp/solution-reader.d.ts +5 -5
  106. package/dist/mcp/solution-reader.js +34 -24
  107. package/dist/renderer/rule-renderer.js +9 -11
  108. package/dist/services/session.d.ts +19 -0
  109. package/dist/services/session.js +62 -0
  110. package/hooks/hooks.json +2 -2
  111. package/package.json +2 -1
  112. package/skills/architecture-decision/SKILL.md +113 -257
  113. package/skills/calibrate/SKILL.md +207 -0
  114. package/skills/code-review/SKILL.md +151 -178
  115. package/skills/compound/SKILL.md +126 -68
  116. package/skills/deep-interview/SKILL.md +266 -0
  117. package/skills/docker/SKILL.md +57 -179
  118. package/skills/forge-loop/SKILL.md +198 -0
  119. package/skills/learn/SKILL.md +216 -0
  120. package/skills/retro/SKILL.md +199 -0
  121. package/skills/ship/SKILL.md +259 -0
  122. package/agents/code-simplifier.md +0 -197
  123. package/agents/performance-reviewer.md +0 -172
  124. package/agents/qa-tester.md +0 -158
  125. package/agents/refactoring-expert.md +0 -168
  126. package/agents/scientist.md +0 -144
  127. package/agents/security-reviewer.md +0 -137
  128. package/agents/writer.md +0 -184
  129. package/commands/api-design.md +0 -268
  130. package/commands/ci-cd.md +0 -270
  131. package/commands/database.md +0 -263
  132. package/commands/debug-detective.md +0 -99
  133. package/commands/documentation.md +0 -276
  134. package/commands/ecomode.md +0 -51
  135. package/commands/frontend.md +0 -271
  136. package/commands/git-master.md +0 -90
  137. package/commands/incident-response.md +0 -292
  138. package/commands/migrate.md +0 -101
  139. package/commands/performance.md +0 -288
  140. package/commands/refactor.md +0 -105
  141. package/commands/security-review.md +0 -288
  142. package/commands/tdd.md +0 -183
  143. package/commands/testing-strategy.md +0 -265
  144. package/skills/api-design/SKILL.md +0 -262
  145. package/skills/ci-cd/SKILL.md +0 -264
  146. package/skills/database/SKILL.md +0 -257
  147. package/skills/debug-detective/SKILL.md +0 -95
  148. package/skills/documentation/SKILL.md +0 -270
  149. package/skills/ecomode/SKILL.md +0 -46
  150. package/skills/frontend/SKILL.md +0 -265
  151. package/skills/git-master/SKILL.md +0 -86
  152. package/skills/incident-response/SKILL.md +0 -286
  153. package/skills/migrate/SKILL.md +0 -96
  154. package/skills/performance/SKILL.md +0 -282
  155. package/skills/refactor/SKILL.md +0 -100
  156. package/skills/security-review/SKILL.md +0 -282
  157. package/skills/tdd/SKILL.md +0 -178
  158. package/skills/testing-strategy/SKILL.md +0 -260
@@ -2,7 +2,8 @@ import * as fs from 'node:fs';
2
2
  import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
4
  import { execFileSync } from 'node:child_process';
5
- import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_PHILOSOPHY, ME_SOLUTIONS, ME_RULES, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
5
+ import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_PHILOSOPHY, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
6
+ import { getTimingStats } from '../hooks/shared/hook-timing.js';
6
7
  /** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
7
8
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
8
9
  function check(label, condition, hint) {
@@ -104,6 +105,60 @@ export async function runDoctor() {
104
105
  }
105
106
  console.log(` Claude Code sessions: ${CLAUDE_PROJECTS_DIR}`);
106
107
  console.log();
108
+ // Hook Health: recent error tracking
109
+ console.log(' [Hook Health]');
110
+ try {
111
+ const hookErrorsPath = path.join(STATE_DIR, 'hook-errors.jsonl');
112
+ if (exists(hookErrorsPath)) {
113
+ const content = fs.readFileSync(hookErrorsPath, 'utf-8');
114
+ const entries = content.trim().split('\n')
115
+ .map(line => { try {
116
+ return JSON.parse(line);
117
+ }
118
+ catch {
119
+ return null;
120
+ } })
121
+ .filter(Boolean);
122
+ const byHook = new Map();
123
+ for (const e of entries) {
124
+ byHook.set(e.hook, (byHook.get(e.hook) ?? 0) + 1);
125
+ }
126
+ if (byHook.size === 0) {
127
+ console.log(' No hook errors recorded.');
128
+ }
129
+ else {
130
+ for (const [hook, count] of [...byHook.entries()].sort((a, b) => b[1] - a[1])) {
131
+ console.log(` ${hook}: ${count} error(s)`);
132
+ }
133
+ }
134
+ }
135
+ else {
136
+ console.log(' No hook errors recorded.');
137
+ }
138
+ }
139
+ catch {
140
+ console.log(' Unable to read hook error log.');
141
+ }
142
+ console.log();
143
+ // Hook Timing: performance stats
144
+ console.log(' [Hook Timing]');
145
+ const timingStats = getTimingStats();
146
+ if (timingStats.length === 0) {
147
+ console.log(' No timing data collected yet.');
148
+ }
149
+ else {
150
+ console.log(' Hook Count p50ms p95ms max ms');
151
+ console.log(' ' + '-'.repeat(56));
152
+ for (const s of timingStats) {
153
+ const hook = s.hook.padEnd(22);
154
+ const count = String(s.count).padStart(5);
155
+ const p50 = String(s.p50).padStart(7);
156
+ const p95 = String(s.p95).padStart(7);
157
+ const max = String(s.max).padStart(8);
158
+ console.log(` ${hook}${count}${p50}${p95}${max}`);
159
+ }
160
+ }
161
+ console.log();
107
162
  console.log();
108
163
  // v1: 팀 팩 시스템 제거. 개인 모드만 지원.
109
164
  console.log(' [Pack Connections]');
@@ -149,32 +204,107 @@ export async function runDoctor() {
149
204
  console.log();
150
205
  }
151
206
  }
152
- // 에러 카운트
153
- console.log(' [Hook Health]');
154
- const hookErrorPath = path.join(STATE_DIR, 'hook-errors.json');
155
- if (fs.existsSync(hookErrorPath)) {
156
- try {
157
- const errors = JSON.parse(fs.readFileSync(hookErrorPath, 'utf-8'));
158
- const entries = Object.entries(errors);
159
- if (entries.length === 0) {
160
- console.log(' No hook errors recorded');
161
- }
162
- else {
163
- for (const [hookName, { count, lastAt }] of entries) {
164
- const icon = count === 0 ? '✓' : '⚠';
165
- const lastDate = lastAt ? lastAt.split('T')[0] : 'unknown';
166
- console.log(` ${icon} ${hookName}: ${count} error${count !== 1 ? 's' : ''}${count > 0 ? ` (last: ${lastDate})` : ''}`);
207
+ // Harness Maturity section
208
+ console.log(' [Harness Maturity]');
209
+ const cwd = process.cwd();
210
+ // 1. Preparation
211
+ const hasClaude = fs.existsSync(path.join(cwd, 'CLAUDE.md'));
212
+ let rulesCount = 0;
213
+ try {
214
+ const rulesDir = path.join(cwd, '.claude', 'rules');
215
+ if (fs.existsSync(rulesDir)) {
216
+ rulesCount = fs.readdirSync(rulesDir).filter(f => f.endsWith('.md')).length;
217
+ }
218
+ }
219
+ catch { /* fail-open */ }
220
+ let hooksActive = 0;
221
+ try {
222
+ const hooksJsonPath = path.join(cwd, 'hooks', 'hooks.json');
223
+ if (fs.existsSync(hooksJsonPath)) {
224
+ const hooksData = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
225
+ if (hooksData.hooks && typeof hooksData.hooks === 'object') {
226
+ for (const eventHooks of Object.values(hooksData.hooks)) {
227
+ if (Array.isArray(eventHooks)) {
228
+ for (const group of eventHooks) {
229
+ if (Array.isArray(group.hooks)) {
230
+ hooksActive += (group.hooks).length;
231
+ }
232
+ }
233
+ }
167
234
  }
168
235
  }
169
236
  }
170
- catch {
171
- console.log(' (hook-errors.json read failed)');
172
- }
173
237
  }
174
- else {
175
- console.log(' No hook errors recorded');
238
+ catch { /* fail-open */ }
239
+ const prepL = hasClaude && rulesCount >= 3 && hooksActive > 0 ? 'L3' : hasClaude && hooksActive > 0 ? 'L2' : hasClaude ? 'L1' : 'L0';
240
+ // 2. Context
241
+ let solutionsCount = 0;
242
+ try {
243
+ if (exists(ME_SOLUTIONS))
244
+ solutionsCount = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md')).length;
245
+ }
246
+ catch { /* fail-open */ }
247
+ let behaviorCount = 0;
248
+ try {
249
+ if (exists(ME_BEHAVIOR))
250
+ behaviorCount = fs.readdirSync(ME_BEHAVIOR).filter(f => f.endsWith('.md')).length;
176
251
  }
252
+ catch { /* fail-open */ }
253
+ const ctxL = solutionsCount >= 5 && behaviorCount >= 3 ? 'L3' : solutionsCount >= 3 || behaviorCount >= 1 ? 'L2' : solutionsCount > 0 || behaviorCount > 0 ? 'L1' : 'L0';
254
+ // 3. Execution
255
+ const hasSkills = exists(ME_SKILLS);
256
+ const execL = hasSkills ? 'L2' : 'L1';
257
+ // 4. Validation
258
+ const hasTests = fs.existsSync(path.join(cwd, 'tests'));
259
+ const hasCI = fs.existsSync(path.join(cwd, '.github', 'workflows'));
260
+ const validL = hasTests && hasCI ? 'L3' : hasTests ? 'L2' : 'L1';
261
+ // 5. Improvement: reflection rate from solutions
262
+ let reflectionRate = 0;
263
+ try {
264
+ if (exists(ME_SOLUTIONS)) {
265
+ const solFiles = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
266
+ if (solFiles.length > 0) {
267
+ let reflected = 0;
268
+ for (const f of solFiles) {
269
+ try {
270
+ const content = fs.readFileSync(path.join(ME_SOLUTIONS, f), 'utf-8');
271
+ const match = content.match(/reflected:\s*(\d+)/);
272
+ if (match && parseInt(match[1], 10) > 0)
273
+ reflected++;
274
+ }
275
+ catch { /* skip */ }
276
+ }
277
+ reflectionRate = Math.round((reflected / solFiles.length) * 100);
278
+ }
279
+ }
280
+ }
281
+ catch { /* fail-open */ }
282
+ const improvL = reflectionRate > 0 ? 'L3' : solutionsCount > 0 ? 'L2' : 'L1';
283
+ const levelIcon = (l) => l === 'L3' ? '✓' : l === 'L2' ? '✓' : l === 'L1' ? '✗' : '✗';
284
+ console.log(` Axis Level Detail`);
285
+ console.log(` ${'─'.repeat(55)}`);
286
+ console.log(` ${levelIcon(prepL)} Preparation ${prepL} CLAUDE.md:${hasClaude ? 'yes' : 'no'}, rules:${rulesCount}, hooks:${hooksActive}`);
287
+ console.log(` ${levelIcon(ctxL)} Context ${ctxL} solutions:${solutionsCount}, behavior:${behaviorCount}`);
288
+ console.log(` ${levelIcon(execL)} Execution ${execL} skills:${hasSkills ? 'yes' : 'no'}`);
289
+ console.log(` ${levelIcon(validL)} Validation ${validL} tests:${hasTests ? 'yes' : 'no'}, CI:${hasCI ? 'yes' : 'no'}`);
290
+ console.log(` ${levelIcon(improvL)} Improvement ${improvL} reflection:${reflectionRate}%`);
177
291
  console.log();
292
+ // Quick wins: suggest for lowest scoring axes
293
+ const axes = [
294
+ { name: 'Preparation', level: prepL, hint: 'Add CLAUDE.md + .claude/rules/ files' },
295
+ { name: 'Context', level: ctxL, hint: 'Run /compound to accumulate solutions' },
296
+ { name: 'Execution', level: execL, hint: 'Promote solutions to skills' },
297
+ { name: 'Validation', level: validL, hint: 'Add tests/ dir and .github/workflows' },
298
+ { name: 'Improvement', level: improvL, hint: 'Reflect on existing solutions' },
299
+ ];
300
+ const quickWins = axes.filter(a => a.level === 'L0' || a.level === 'L1').slice(0, 3);
301
+ if (quickWins.length > 0) {
302
+ console.log(' Quick Wins (Top 3):');
303
+ for (const win of quickWins) {
304
+ console.log(` → ${win.name}: ${win.hint}`);
305
+ }
306
+ console.log();
307
+ }
178
308
  // 현재 디렉토리 git 정보
179
309
  console.log(' [Git]');
180
310
  try {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Forgen — Drift Score (Session Drift Detection)
3
+ *
4
+ * 세션 내 수정 패턴을 추적하여 drift(산만/반복 수정)를 감지.
5
+ * EWMA(Exponentially Weighted Moving Average) 기반 이동평균으로
6
+ * 최근 수정 강도를 측정하고, 임계값 초과 시 경고.
7
+ *
8
+ * Codex 합의: DriftState + evaluateDrift 2개만. 최소 인터페이스.
9
+ */
10
+ /** Drift 상태 (세션 단위, STATE_DIR에 저장) */
11
+ export interface DriftState {
12
+ sessionId: string;
13
+ totalEdits: number;
14
+ totalReverts: number;
15
+ /** EWMA edit rate (0~1, 높을수록 최근 수정 빈도 높음) */
16
+ ewmaEditRate: number;
17
+ /** EWMA revert rate (0~1) */
18
+ ewmaRevertRate: number;
19
+ /** 최근 경고 timestamp (쿨다운용) */
20
+ lastWarningAt: number;
21
+ lastCriticalAt: number;
22
+ hardCapReached: boolean;
23
+ }
24
+ export interface DriftResult {
25
+ level: 'normal' | 'warning' | 'critical' | 'hardcap';
26
+ score: number;
27
+ message: string | null;
28
+ }
29
+ export interface DriftThresholds {
30
+ alpha?: number;
31
+ warningEdits?: number;
32
+ criticalEdits?: number;
33
+ criticalReverts?: number;
34
+ hardCapEdits?: number;
35
+ warningCooldownMs?: number;
36
+ criticalCooldownMs?: number;
37
+ }
38
+ /** EWMA 업데이트 (순수 함수) */
39
+ export declare function updateEwma(prev: number, sample: number, alpha: number): number;
40
+ /** 새 DriftState 생성 */
41
+ export declare function createDriftState(sessionId: string): DriftState;
42
+ /**
43
+ * 도구 호출 이벤트로 drift 상태를 갱신하고 평가 결과를 반환.
44
+ * @param state 현재 상태 (mutate됨)
45
+ * @param isEdit Write/Edit 도구 호출 여부
46
+ * @param isRevert revert 감지 여부
47
+ * @param thresholds 커스텀 임계치 (hook-config에서 로드)
48
+ */
49
+ export declare function evaluateDrift(state: DriftState, isEdit: boolean, isRevert: boolean, thresholds?: DriftThresholds): DriftResult;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Forgen — Drift Score (Session Drift Detection)
3
+ *
4
+ * 세션 내 수정 패턴을 추적하여 drift(산만/반복 수정)를 감지.
5
+ * EWMA(Exponentially Weighted Moving Average) 기반 이동평균으로
6
+ * 최근 수정 강도를 측정하고, 임계값 초과 시 경고.
7
+ *
8
+ * Codex 합의: DriftState + evaluateDrift 2개만. 최소 인터페이스.
9
+ */
10
+ const DEFAULTS = {
11
+ alpha: 0.35,
12
+ warningEdits: 15,
13
+ criticalEdits: 30,
14
+ criticalReverts: 2,
15
+ hardCapEdits: 50,
16
+ warningCooldownMs: 5 * 60 * 1000,
17
+ criticalCooldownMs: 10 * 60 * 1000,
18
+ };
19
+ /** EWMA 업데이트 (순수 함수) */
20
+ export function updateEwma(prev, sample, alpha) {
21
+ return alpha * sample + (1 - alpha) * prev;
22
+ }
23
+ /** 새 DriftState 생성 */
24
+ export function createDriftState(sessionId) {
25
+ return {
26
+ sessionId,
27
+ totalEdits: 0,
28
+ totalReverts: 0,
29
+ ewmaEditRate: 0,
30
+ ewmaRevertRate: 0,
31
+ lastWarningAt: 0,
32
+ lastCriticalAt: 0,
33
+ hardCapReached: false,
34
+ };
35
+ }
36
+ /**
37
+ * 도구 호출 이벤트로 drift 상태를 갱신하고 평가 결과를 반환.
38
+ * @param state 현재 상태 (mutate됨)
39
+ * @param isEdit Write/Edit 도구 호출 여부
40
+ * @param isRevert revert 감지 여부
41
+ * @param thresholds 커스텀 임계치 (hook-config에서 로드)
42
+ */
43
+ export function evaluateDrift(state, isEdit, isRevert, thresholds = {}) {
44
+ const t = { ...DEFAULTS, ...thresholds };
45
+ const now = Date.now();
46
+ // Update counters
47
+ if (isEdit)
48
+ state.totalEdits++;
49
+ if (isRevert)
50
+ state.totalReverts++;
51
+ // Update EWMA
52
+ state.ewmaEditRate = updateEwma(state.ewmaEditRate, isEdit ? 1 : 0, t.alpha);
53
+ state.ewmaRevertRate = updateEwma(state.ewmaRevertRate, isRevert ? 1 : 0, t.alpha);
54
+ // Calculate drift score: edit rate 65% + revert rate 35%
55
+ const rawScore = (state.ewmaEditRate * 65) + (state.ewmaRevertRate * 35);
56
+ const score = Math.min(100, Math.max(0, Math.round(rawScore)));
57
+ // Hard cap
58
+ if (state.totalEdits >= t.hardCapEdits) {
59
+ state.hardCapReached = true;
60
+ return {
61
+ level: 'hardcap',
62
+ score: 100,
63
+ message: `[Forgen] ⛔ Session drift hard cap reached (${state.totalEdits} edits). Stop and reassess the approach before continuing.`,
64
+ };
65
+ }
66
+ // Critical: 2+ reverts OR 30+ edits OR score >= 78
67
+ if ((state.totalReverts >= t.criticalReverts || state.totalEdits >= t.criticalEdits || score >= 78) &&
68
+ (now - state.lastCriticalAt > t.criticalCooldownMs)) {
69
+ state.lastCriticalAt = now;
70
+ return {
71
+ level: 'critical',
72
+ score,
73
+ message: `[Forgen] ⚠ High drift detected (score: ${score}, edits: ${state.totalEdits}, reverts: ${state.totalReverts}). Consider stopping to redesign the approach.`,
74
+ };
75
+ }
76
+ // Warning: 15+ edits OR score >= 52
77
+ if ((state.totalEdits >= t.warningEdits || score >= 52) &&
78
+ (now - state.lastWarningAt > t.warningCooldownMs)) {
79
+ state.lastWarningAt = now;
80
+ return {
81
+ level: 'warning',
82
+ score,
83
+ message: `[Forgen] Drift building up (score: ${score}, edits: ${state.totalEdits}). Review your approach if changes feel repetitive.`,
84
+ };
85
+ }
86
+ return { level: 'normal', score, message: null };
87
+ }
@@ -11,14 +11,19 @@
11
11
  * - Lines 400-550: Rule file injection, gitignore, compound memory
12
12
  * - Lines 550+: prepareHarness — main orchestration
13
13
  */
14
+ import { type RuntimeHost } from './types.js';
14
15
  import { rollbackSettings } from './settings-lock.js';
15
16
  import { type V1BootstrapResult } from './v1-bootstrap.js';
16
17
  export interface V1HarnessContext {
17
18
  cwd: string;
18
19
  inTmux: boolean;
19
20
  v1: V1BootstrapResult;
21
+ runtime: RuntimeHost;
20
22
  }
21
23
  /** 최초 실행 여부: ~/.forgen/ 디렉토리가 없으면 true */
22
24
  export declare function isFirstRun(): boolean;
23
25
  export { rollbackSettings };
24
- export declare function prepareHarness(cwd: string): Promise<V1HarnessContext>;
26
+ interface PrepareHarnessOptions {
27
+ runtime?: RuntimeHost;
28
+ }
29
+ export declare function prepareHarness(cwd: string, options?: PrepareHarnessOptions): Promise<V1HarnessContext>;
@@ -20,6 +20,7 @@ import { buildEnv, generateClaudeRuleFiles, registerTmuxBindings } from './confi
20
20
  import { createLogger } from './logger.js';
21
21
  import { HANDOFFS_DIR, ME_BEHAVIOR, ME_DIR, ME_RULES, ME_SKILLS, ME_SOLUTIONS, SESSIONS_DIR, STATE_DIR, FORGEN_HOME } from './paths.js';
22
22
  import { RULE_FILE_CAPS } from '../hooks/shared/injection-caps.js';
23
+ import { generateHooksJson } from '../hooks/hooks-generator.js';
23
24
  import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
24
25
  import { ConfigError } from './errors.js';
25
26
  import { bootstrapV1Session, ensureV1Directories } from './v1-bootstrap.js';
@@ -104,7 +105,7 @@ function isForgenHookEntry(entry, pkgRoot) {
104
105
  return Array.isArray(hooks) && hooks.some(h => typeof h.command === 'string' && matchesPath(h.command));
105
106
  }
106
107
  /** Strip existing forgen hooks from settings, merge fresh hooks.json. */
107
- function mergeHooksIntoSettings(settings) {
108
+ function mergeHooksIntoSettings(settings, runtime, cwd) {
108
109
  const pkgRoot = getPackageRoot();
109
110
  const hooksConfig = settings.hooks ?? {};
110
111
  // Remove existing forgen hooks (clean slate before re-inject)
@@ -117,18 +118,28 @@ function mergeHooksIntoSettings(settings) {
117
118
  else
118
119
  hooksConfig[event] = filtered;
119
120
  }
120
- // Read hooks.json and inject, replacing ${CLAUDE_PLUGIN_ROOT}
121
- const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
122
121
  try {
123
- if (fs.existsSync(hooksJsonPath)) {
124
- const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
125
- const hooksData = hooksJson.hooks;
126
- if (hooksData) {
127
- const resolved = JSON.parse(JSON.stringify(hooksData).replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pkgRoot));
128
- for (const [event, handlers] of Object.entries(resolved)) {
129
- if (!hooksConfig[event])
130
- hooksConfig[event] = [];
131
- hooksConfig[event].push(...handlers);
122
+ if (runtime === 'codex') {
123
+ const generated = generateHooksJson({ cwd, runtime, pluginRoot: path.join(pkgRoot, 'dist') });
124
+ for (const [event, handlers] of Object.entries(generated.hooks)) {
125
+ if (!hooksConfig[event])
126
+ hooksConfig[event] = [];
127
+ hooksConfig[event].push(...handlers);
128
+ }
129
+ }
130
+ else {
131
+ // Read hooks.json and inject, replacing ${CLAUDE_PLUGIN_ROOT}
132
+ const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
133
+ if (fs.existsSync(hooksJsonPath)) {
134
+ const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
135
+ const hooksData = hooksJson.hooks;
136
+ if (hooksData) {
137
+ const resolved = JSON.parse(JSON.stringify(hooksData).replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pkgRoot));
138
+ for (const [event, handlers] of Object.entries(resolved)) {
139
+ if (!hooksConfig[event])
140
+ hooksConfig[event] = [];
141
+ hooksConfig[event].push(...handlers);
142
+ }
132
143
  }
133
144
  }
134
145
  }
@@ -177,14 +188,14 @@ function applyTrustPolicyPermissions(settings, v1Result) {
177
188
  * atomic write). Each phase is now a named function with a single
178
189
  * responsibility, testable in isolation if needed.
179
190
  */
180
- function injectSettings(env, v1Result) {
191
+ function injectSettings(env, v1Result, runtime, cwd) {
181
192
  fs.mkdirSync(CLAUDE_DIR, { recursive: true });
182
193
  acquireLock();
183
194
  const settings = readSettingsWithBackup();
184
195
  // Merge env vars
185
196
  settings.env = { ...(settings.env ?? {}), ...env };
186
197
  applyStatusLine(settings);
187
- mergeHooksIntoSettings(settings);
198
+ mergeHooksIntoSettings(settings, runtime, cwd);
188
199
  applyTrustPolicyPermissions(settings, v1Result);
189
200
  try {
190
201
  atomicWriteFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
@@ -252,14 +263,58 @@ function installAgentsFromDir(sourceDir, targetDir, prefix, hashes) {
252
263
  hashes[dstName] = newHash;
253
264
  }
254
265
  }
266
+ /**
267
+ * 현재 source에 없는 stale ch-*.md 에이전트 파일을 정리.
268
+ * forgen-managed 마커가 있는 파일만 삭제 (사용자 수정 파일 보호).
269
+ */
270
+ function cleanupStaleAgents(sourceDir, targetDir, prefix, hashes) {
271
+ if (!fs.existsSync(targetDir))
272
+ return;
273
+ if (!fs.existsSync(sourceDir))
274
+ return;
275
+ // 현재 source의 유효한 파일 목록
276
+ const validFiles = new Set(fs.readdirSync(sourceDir)
277
+ .filter((f) => f.endsWith('.md'))
278
+ .map((f) => `${prefix}${f}`));
279
+ // targetDir에서 prefix로 시작하지만 유효 목록에 없는 파일 삭제
280
+ for (const existing of fs.readdirSync(targetDir)) {
281
+ if (!existing.startsWith(prefix) || !existing.endsWith('.md'))
282
+ continue;
283
+ if (validFiles.has(existing))
284
+ continue;
285
+ const filePath = path.join(targetDir, existing);
286
+ try {
287
+ const content = fs.readFileSync(filePath, 'utf-8');
288
+ // 사용자 수정 보호: forgen-managed 마커가 있고 hash가 기록된 경우만 삭제
289
+ const recordedHash = hashes[existing];
290
+ const hasMarker = content.includes('<!-- forgen-managed -->');
291
+ if (!hasMarker) {
292
+ log.debug(`에이전트 삭제 스킵: ${existing} (forgen-managed 마커 없음)`);
293
+ continue;
294
+ }
295
+ if (recordedHash && contentHash(content) !== recordedHash) {
296
+ log.debug(`에이전트 삭제 스킵: ${existing} (사용자 수정 감지)`);
297
+ continue;
298
+ }
299
+ fs.unlinkSync(filePath);
300
+ delete hashes[existing];
301
+ log.debug(`stale 에이전트 삭제: ${existing}`);
302
+ }
303
+ catch (e) {
304
+ log.debug(`에이전트 삭제 실패: ${existing}`, e);
305
+ }
306
+ }
307
+ }
255
308
  /** 에이전트 정의 파일 설치 (패키지 내장만) */
256
309
  function installAgents(cwd) {
257
310
  const pkgRoot = getPackageRoot();
258
311
  const targetDir = path.join(cwd, '.claude', 'agents');
259
312
  fs.mkdirSync(targetDir, { recursive: true });
260
313
  const hashes = loadAgentHashes();
314
+ const sourceDir = path.join(pkgRoot, 'agents');
261
315
  try {
262
- installAgentsFromDir(path.join(pkgRoot, 'agents'), targetDir, 'ch-', hashes);
316
+ installAgentsFromDir(sourceDir, targetDir, 'ch-', hashes);
317
+ cleanupStaleAgents(sourceDir, targetDir, 'ch-', hashes);
263
318
  saveAgentHashes(hashes);
264
319
  }
265
320
  catch (e) {
@@ -560,7 +615,8 @@ function checkCompoundStaleness() {
560
615
  log.debug('Staleness check failed (non-fatal)', e);
561
616
  }
562
617
  }
563
- export async function prepareHarness(cwd) {
618
+ export async function prepareHarness(cwd, options = {}) {
619
+ const runtime = options.runtime ?? 'claude';
564
620
  try {
565
621
  // 0. 스토리지 마이그레이션 (v5.1: ~/.compound/ → ~/.forgen/)
566
622
  migrateToForgen();
@@ -591,8 +647,8 @@ export async function prepareHarness(cwd) {
591
647
  // 3. 환경 확인
592
648
  const inTmux = !!process.env.TMUX;
593
649
  // 4. Claude Code 설정 주입 (환경변수 + trust 기반 permissions)
594
- const env = buildEnv(cwd, v1Result.session?.session_id);
595
- injectSettings(env, v1Result);
650
+ const env = buildEnv(cwd, v1Result.session?.session_id, runtime);
651
+ injectSettings(env, v1Result, runtime, cwd);
596
652
  // 5. 에이전트 설치
597
653
  installAgents(cwd);
598
654
  // 6. 규칙 파일 생성 및 주입 (v1 부트스트랩 결과의 renderedRules를 직접 전달)
@@ -612,7 +668,7 @@ export async function prepareHarness(cwd) {
612
668
  await startLegacySessionLog(cwd, inTmux, v1Result);
613
669
  // 12. Compound staleness guard
614
670
  checkCompoundStaleness();
615
- return { cwd, inTmux, v1: v1Result };
671
+ return { cwd, inTmux, v1: v1Result, runtime };
616
672
  }
617
673
  catch (err) {
618
674
  rollbackSettings();
@@ -11,6 +11,8 @@ export interface McpServerConfig {
11
11
  command: string;
12
12
  args: string[];
13
13
  env?: Record<string, string>;
14
+ /** HTTP/SSE transport URL (alternative to command+args) */
15
+ url?: string;
14
16
  }
15
17
  /**
16
18
  * 기본 MCP 서버 템플릿 목록 반환
@@ -101,7 +101,12 @@ export async function handleMcp(args) {
101
101
  for (const name of names) {
102
102
  const cfg = installed[name];
103
103
  console.log(` ${name}`);
104
- console.log(` command: ${cfg.command} ${cfg.args.join(' ')}`);
104
+ if (cfg.command) {
105
+ console.log(` command: ${cfg.command} ${(cfg.args ?? []).join(' ')}`);
106
+ }
107
+ else if (cfg.url) {
108
+ console.log(` url: ${cfg.url}`);
109
+ }
105
110
  if (cfg.env && Object.keys(cfg.env).length > 0) {
106
111
  console.log(` env: ${JSON.stringify(cfg.env)}`);
107
112
  }
@@ -1,5 +1,6 @@
1
1
  /** ~/.claude/ — Claude Code 설정 디렉토리 */
2
2
  export declare const CLAUDE_DIR: string;
3
+ export declare const CODEX_DIR: string;
3
4
  /** ~/.claude/settings.json — Claude Code 설정 파일 */
4
5
  export declare const SETTINGS_PATH: string;
5
6
  /**
@@ -47,6 +48,10 @@ export declare const MATCH_EVAL_LOG_PATH: string;
47
48
  export declare const SESSIONS_DIR: string;
48
49
  /** ~/.forgen/config.json — 글로벌 설정 */
49
50
  export declare const GLOBAL_CONFIG: string;
51
+ /** ~/.forgen/state/session-quality/ — 세션 품질 점수 */
52
+ export declare const SESSION_QUALITY_DIR: string;
53
+ /** ~/.forgen/state/meta-learning/ — 메타학습 상태 파일 */
54
+ export declare const META_LEARNING_DIR: string;
50
55
  /** ~/.forgen/lab/ — Lab 적응형 최적화 엔진 데이터 */
51
56
  export declare const LAB_DIR: string;
52
57
  /** ~/.forgen/lab/events.jsonl — Lab 이벤트 로그 (JSONL) */
@@ -74,7 +79,7 @@ export declare const V1_RAW_LOGS_DIR: string;
74
79
  /** @deprecated use GLOBAL_CONFIG */
75
80
  export declare const V1_GLOBAL_CONFIG: string;
76
81
  /** 모든 실행 모드 이름 (cancel/recovery 시 사용) */
77
- export declare const ALL_MODES: readonly ["ralph", "autopilot", "ultrawork", "team", "pipeline", "ccg", "ralplan", "deep-interview", "ecomode"];
82
+ export declare const ALL_MODES: readonly ["ralph", "autopilot", "ultrawork", "team", "pipeline", "ccg", "ralplan", "deep-interview", "forge-loop", "ship", "retro", "learn", "calibrate"];
78
83
  /** {repo}/.compound/ — 프로젝트 로컬 디렉토리 */
79
84
  export declare function projectDir(cwd: string): string;
80
85
  /** {repo}/.compound/pack.link — 팀 팩 연결 파일 */
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  const HOME = os.homedir();
4
4
  /** ~/.claude/ — Claude Code 설정 디렉토리 */
5
5
  export const CLAUDE_DIR = path.join(HOME, '.claude');
6
+ export const CODEX_DIR = path.join(HOME, '.codex');
6
7
  /** ~/.claude/settings.json — Claude Code 설정 파일 */
7
8
  export const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
8
9
  /**
@@ -50,6 +51,10 @@ export const MATCH_EVAL_LOG_PATH = path.join(STATE_DIR, 'match-eval-log.jsonl');
50
51
  export const SESSIONS_DIR = path.join(FORGEN_HOME, 'sessions');
51
52
  /** ~/.forgen/config.json — 글로벌 설정 */
52
53
  export const GLOBAL_CONFIG = path.join(FORGEN_HOME, 'config.json');
54
+ /** ~/.forgen/state/session-quality/ — 세션 품질 점수 */
55
+ export const SESSION_QUALITY_DIR = path.join(STATE_DIR, 'session-quality');
56
+ /** ~/.forgen/state/meta-learning/ — 메타학습 상태 파일 */
57
+ export const META_LEARNING_DIR = path.join(STATE_DIR, 'meta-learning');
53
58
  /** ~/.forgen/lab/ — Lab 적응형 최적화 엔진 데이터 */
54
59
  export const LAB_DIR = path.join(FORGEN_HOME, 'lab');
55
60
  /** ~/.forgen/lab/events.jsonl — Lab 이벤트 로그 (JSONL) */
@@ -80,8 +85,19 @@ export const V1_GLOBAL_CONFIG = GLOBAL_CONFIG;
80
85
  // ── 레거시 ──
81
86
  /** 모든 실행 모드 이름 (cancel/recovery 시 사용) */
82
87
  export const ALL_MODES = [
83
- 'ralph', 'autopilot', 'ultrawork', 'team', 'pipeline',
84
- 'ccg', 'ralplan', 'deep-interview', 'ecomode',
88
+ 'ralph',
89
+ 'autopilot',
90
+ 'ultrawork',
91
+ 'team',
92
+ 'pipeline',
93
+ 'ccg',
94
+ 'ralplan',
95
+ 'deep-interview',
96
+ 'forge-loop',
97
+ 'ship',
98
+ 'retro',
99
+ 'learn',
100
+ 'calibrate',
85
101
  ];
86
102
  /** {repo}/.compound/ — 프로젝트 로컬 디렉토리 */
87
103
  export function projectDir(cwd) {
@@ -1,8 +1,9 @@
1
1
  import type { V1HarnessContext } from './harness.js';
2
+ import { type RuntimeHost } from './types.js';
2
3
  /** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
3
- export declare function spawnClaude(args: string[], context: V1HarnessContext): Promise<number>;
4
+ export declare function spawnClaude(args: string[], context: V1HarnessContext, runtime?: RuntimeHost): Promise<number>;
4
5
  /**
5
6
  * 토큰 한도 도달 시 자동 재시작을 지원하는 claude 실행 래퍼.
6
7
  * context-guard가 pending-resume.json 마커를 생성하면 쿨다운 후 재시작.
7
8
  */
8
- export declare function spawnClaudeWithResume(args: string[], context: V1HarnessContext, contextFactory: () => Promise<V1HarnessContext>): Promise<void>;
9
+ export declare function spawnClaudeWithResume(args: string[], context: V1HarnessContext, contextFactory: () => Promise<V1HarnessContext>, runtime?: RuntimeHost): Promise<void>;