@wooojin/forgen 0.2.1 → 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 (124) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.ko.md +25 -14
  3. package/README.md +61 -17
  4. package/agents/analyst.md +48 -4
  5. package/agents/architect.md +39 -4
  6. package/agents/code-reviewer.md +107 -77
  7. package/agents/critic.md +47 -4
  8. package/agents/debugger.md +46 -4
  9. package/agents/designer.md +40 -4
  10. package/agents/executor.md +112 -30
  11. package/agents/explore.md +45 -5
  12. package/agents/git-master.md +48 -4
  13. package/agents/planner.md +121 -18
  14. package/agents/test-engineer.md +58 -4
  15. package/agents/verifier.md +92 -77
  16. package/commands/architecture-decision.md +127 -258
  17. package/commands/calibrate.md +225 -0
  18. package/commands/code-review.md +163 -178
  19. package/commands/compound.md +127 -68
  20. package/commands/deep-interview.md +212 -110
  21. package/commands/docker.md +68 -178
  22. package/commands/forge-loop.md +215 -0
  23. package/commands/learn.md +231 -0
  24. package/commands/retro.md +215 -0
  25. package/commands/ship.md +277 -0
  26. package/dist/cli.js +17 -9
  27. package/dist/core/auto-compound-runner.js +14 -0
  28. package/dist/core/config-injector.d.ts +2 -1
  29. package/dist/core/config-injector.js +2 -1
  30. package/dist/core/dashboard.d.ts +17 -0
  31. package/dist/core/dashboard.js +112 -2
  32. package/dist/core/harness.d.ts +6 -1
  33. package/dist/core/harness.js +75 -19
  34. package/dist/core/paths.d.ts +6 -1
  35. package/dist/core/paths.js +18 -2
  36. package/dist/core/spawn.d.ts +3 -2
  37. package/dist/core/spawn.js +27 -8
  38. package/dist/core/types.d.ts +34 -0
  39. package/dist/engine/compound-lifecycle.d.ts +4 -3
  40. package/dist/engine/compound-lifecycle.js +91 -46
  41. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  42. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  43. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  44. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  45. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  46. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  47. package/dist/engine/meta-learning/runner.d.ts +14 -0
  48. package/dist/engine/meta-learning/runner.js +90 -0
  49. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  50. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  51. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  52. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  53. package/dist/engine/meta-learning/types.d.ts +114 -0
  54. package/dist/engine/meta-learning/types.js +43 -0
  55. package/dist/engine/solution-format.d.ts +2 -2
  56. package/dist/engine/solution-format.js +249 -34
  57. package/dist/engine/solution-index.d.ts +1 -1
  58. package/dist/engine/solution-matcher.d.ts +7 -1
  59. package/dist/engine/solution-matcher.js +114 -37
  60. package/dist/fgx.js +12 -8
  61. package/dist/hooks/context-guard.d.ts +5 -0
  62. package/dist/hooks/context-guard.js +118 -2
  63. package/dist/hooks/hooks-generator.d.ts +3 -0
  64. package/dist/hooks/hooks-generator.js +23 -6
  65. package/dist/hooks/keyword-detector.js +16 -100
  66. package/dist/hooks/skill-injector.d.ts +4 -3
  67. package/dist/hooks/skill-injector.js +6 -4
  68. package/dist/host/codex-adapter.d.ts +10 -0
  69. package/dist/host/codex-adapter.js +154 -0
  70. package/dist/mcp/solution-reader.d.ts +5 -5
  71. package/dist/mcp/solution-reader.js +34 -24
  72. package/dist/services/session.d.ts +19 -0
  73. package/dist/services/session.js +62 -0
  74. package/hooks/hooks.json +2 -2
  75. package/package.json +2 -1
  76. package/skills/architecture-decision/SKILL.md +113 -257
  77. package/skills/calibrate/SKILL.md +207 -0
  78. package/skills/code-review/SKILL.md +151 -178
  79. package/skills/compound/SKILL.md +126 -68
  80. package/skills/deep-interview/SKILL.md +210 -110
  81. package/skills/docker/SKILL.md +57 -179
  82. package/skills/forge-loop/SKILL.md +198 -0
  83. package/skills/learn/SKILL.md +216 -0
  84. package/skills/retro/SKILL.md +199 -0
  85. package/skills/ship/SKILL.md +259 -0
  86. package/agents/code-simplifier.md +0 -197
  87. package/agents/performance-reviewer.md +0 -172
  88. package/agents/qa-tester.md +0 -158
  89. package/agents/refactoring-expert.md +0 -168
  90. package/agents/scientist.md +0 -144
  91. package/agents/security-reviewer.md +0 -137
  92. package/agents/writer.md +0 -184
  93. package/commands/api-design.md +0 -268
  94. package/commands/ci-cd.md +0 -270
  95. package/commands/database.md +0 -263
  96. package/commands/debug-detective.md +0 -99
  97. package/commands/documentation.md +0 -276
  98. package/commands/ecomode.md +0 -51
  99. package/commands/frontend.md +0 -271
  100. package/commands/git-master.md +0 -90
  101. package/commands/incident-response.md +0 -292
  102. package/commands/migrate.md +0 -101
  103. package/commands/performance.md +0 -288
  104. package/commands/refactor.md +0 -105
  105. package/commands/security-review.md +0 -288
  106. package/commands/specify.md +0 -128
  107. package/commands/tdd.md +0 -183
  108. package/commands/testing-strategy.md +0 -265
  109. package/skills/api-design/SKILL.md +0 -262
  110. package/skills/ci-cd/SKILL.md +0 -264
  111. package/skills/database/SKILL.md +0 -257
  112. package/skills/debug-detective/SKILL.md +0 -95
  113. package/skills/documentation/SKILL.md +0 -270
  114. package/skills/ecomode/SKILL.md +0 -46
  115. package/skills/frontend/SKILL.md +0 -265
  116. package/skills/git-master/SKILL.md +0 -86
  117. package/skills/incident-response/SKILL.md +0 -286
  118. package/skills/migrate/SKILL.md +0 -96
  119. package/skills/performance/SKILL.md +0 -282
  120. package/skills/refactor/SKILL.md +0 -100
  121. package/skills/security-review/SKILL.md +0 -282
  122. package/skills/specify/SKILL.md +0 -122
  123. package/skills/tdd/SKILL.md +0 -178
  124. package/skills/testing-strategy/SKILL.md +0 -260
@@ -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();
@@ -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", "specify"];
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', 'specify',
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>;
@@ -12,6 +12,9 @@ const log = createLogger('spawn');
12
12
  function findClaude() {
13
13
  return 'claude';
14
14
  }
15
+ function findRuntimeLauncher(runtime) {
16
+ return runtime === 'codex' ? 'codex' : findClaude();
17
+ }
15
18
  /**
16
19
  * 가장 최근 transcript 파일을 찾는다.
17
20
  * Claude Code는 세션 대화를 ~/.claude/projects/{sanitized-cwd}/{uuid}.jsonl에 저장.
@@ -60,32 +63,43 @@ async function indexTranscriptToFTS(cwd, transcriptPath, sessionId) {
60
63
  }
61
64
  }
62
65
  /** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
63
- export async function spawnClaude(args, context) {
64
- const claudePath = findClaude();
65
- const env = buildEnv(context.cwd);
66
+ export async function spawnClaude(args, context, runtime = 'claude') {
67
+ const launcher = findRuntimeLauncher(runtime);
68
+ const env = buildEnv(context.cwd, context.v1.session?.session_id, runtime);
66
69
  const cleanArgs = [...args];
67
70
  // config.json에서 dangerouslySkipPermissions 기본값 적용
68
71
  const globalConfig = loadGlobalConfig();
69
- if (globalConfig.dangerouslySkipPermissions && !cleanArgs.includes('--dangerously-skip-permissions')) {
72
+ if (runtime === 'claude' &&
73
+ globalConfig.dangerouslySkipPermissions &&
74
+ !cleanArgs.includes('--dangerously-skip-permissions')) {
70
75
  cleanArgs.unshift('--dangerously-skip-permissions');
71
76
  }
72
77
  // 세션 시작 전 timestamp 기록 (종료 후 transcript 찾기 위해)
73
78
  const sessionStartTime = Date.now();
74
79
  return new Promise((resolve, reject) => {
75
- const child = spawn(claudePath, cleanArgs, {
80
+ const child = spawn(launcher, cleanArgs, {
76
81
  stdio: 'inherit',
77
82
  env: { ...process.env, ...env },
78
83
  cwd: context.cwd,
79
84
  });
80
85
  child.on('error', (err) => {
81
86
  if (err.code === 'ENOENT') {
82
- reject(new Error('Claude Code is not installed. npm install -g @anthropic-ai/claude-code'));
87
+ if (runtime === 'codex') {
88
+ reject(new Error('Codex is not installed.'));
89
+ }
90
+ else {
91
+ reject(new Error('Claude Code is not installed. npm install -g @anthropic-ai/claude-code'));
92
+ }
83
93
  }
84
94
  else {
85
95
  reject(err);
86
96
  }
87
97
  });
88
98
  child.on('exit', async (code) => {
99
+ if (runtime !== 'claude') {
100
+ resolve(code ?? 0);
101
+ return;
102
+ }
89
103
  // 세션 종료 후 하네스 작업
90
104
  try {
91
105
  const transcript = findLatestTranscript(context.cwd);
@@ -135,11 +149,16 @@ const MAX_RESUMES = 3;
135
149
  * 토큰 한도 도달 시 자동 재시작을 지원하는 claude 실행 래퍼.
136
150
  * context-guard가 pending-resume.json 마커를 생성하면 쿨다운 후 재시작.
137
151
  */
138
- export async function spawnClaudeWithResume(args, context, contextFactory) {
152
+ export async function spawnClaudeWithResume(args, context, contextFactory, runtime = 'claude') {
139
153
  let resumeCount = 0;
140
154
  let currentContext = context;
141
155
  while (true) {
142
- const exitCode = await spawnClaude(args, currentContext);
156
+ const exitCode = await spawnClaude(args, currentContext, runtime);
157
+ if (runtime !== 'claude') {
158
+ if (exitCode !== 0)
159
+ process.exit(exitCode);
160
+ break;
161
+ }
143
162
  const resumePath = path.join(STATE_DIR, 'pending-resume.json');
144
163
  if (!fs.existsSync(resumePath)) {
145
164
  if (exitCode !== 0)
@@ -106,3 +106,37 @@ export interface HarnessContext {
106
106
  /** 모델 라우팅 프리셋 (default, cost-saving, max-quality) */
107
107
  routingPreset?: string;
108
108
  }
109
+ /** 런타임 Host */
110
+ export type RuntimeHost = 'claude' | 'codex';
111
+ /** 런칭 컨텍스트 — CLI에서 runtime/args 결정을 모델화 */
112
+ export interface LaunchContext {
113
+ runtime: RuntimeHost;
114
+ args: string[];
115
+ runtimeSource: 'flag' | 'env' | 'default';
116
+ }
117
+ /** 훅 입력 이벤트 스키마 (버전 간 상위 호환용 최소 스펙) */
118
+ export interface HookEventInput {
119
+ hookEventName?: string;
120
+ event?: string;
121
+ session_id?: string;
122
+ sessionId?: string;
123
+ tool_name?: string;
124
+ toolName?: string;
125
+ tool_input?: Record<string, unknown>;
126
+ toolInput?: Record<string, unknown>;
127
+ [key: string]: unknown;
128
+ }
129
+ /** 훅 출력 스키마 (Claude/Codex 정규화용 공통 뷰) */
130
+ export interface HookEventOutput {
131
+ continue?: boolean;
132
+ suppressOutput?: boolean;
133
+ systemMessage?: string;
134
+ hookSpecificOutput?: {
135
+ hookEventName?: string;
136
+ permissionDecision?: string;
137
+ permissionDecisionReason?: string;
138
+ additionalContext?: string;
139
+ [key: string]: unknown;
140
+ };
141
+ [key: string]: unknown;
142
+ }
@@ -1,4 +1,5 @@
1
1
  import type { SolutionFrontmatter, SolutionStatus } from './solution-format.js';
2
+ import type { AdaptiveLifecycleThresholds } from './meta-learning/types.js';
2
3
  export interface LifecycleResult {
3
4
  promoted: string[];
4
5
  demoted: string[];
@@ -11,8 +12,8 @@ export declare function nextStatus(current: SolutionStatus): SolutionStatus | nu
11
12
  * Spacing: 0.25 between levels for meaningful differentiation in matching scores.
12
13
  * Previous: 0.3/0.6/0.8/0.85 had only 0.05 gap between verified and mature. */
13
14
  export declare function statusConfidence(status: SolutionStatus): number;
14
- /** Check promotion eligibility */
15
- export declare function checkPromotion(fm: SolutionFrontmatter): boolean;
15
+ /** Check promotion eligibility (with optional adaptive thresholds) */
16
+ export declare function checkPromotion(fm: SolutionFrontmatter, thresholds?: AdaptiveLifecycleThresholds | null): boolean;
16
17
  /** Check if solution should be demoted due to confidence-status mismatch */
17
18
  export declare function checkConfidenceDemotion(fm: SolutionFrontmatter): SolutionStatus | null;
18
19
  /** Check if solution identifiers still exist in codebase (staleness detection) */
@@ -25,7 +26,7 @@ export declare function isStale(fm: SolutionFrontmatter): boolean;
25
26
  */
26
27
  export declare function updateSolutionFile(filePath: string, updates: Partial<SolutionFrontmatter>): boolean;
27
28
  /** Run lifecycle check on all solutions */
28
- export declare function runLifecycleCheck(sessionId?: string): LifecycleResult;
29
+ export declare function runLifecycleCheck(_sessionId?: string): LifecycleResult;
29
30
  /** Detect contradictions between solutions */
30
31
  export declare function detectContradictions(dirs: string[]): string[];
31
32
  /** Manual verify command: immediately promote to verified */
@@ -1,11 +1,12 @@
1
+ import { execFileSync } from 'node:child_process';
1
2
  import * as fs from 'node:fs';
2
3
  import * as path from 'node:path';
3
- import { execFileSync } from 'node:child_process';
4
+ import { createLogger } from '../core/logger.js';
4
5
  import { parseFrontmatterOnly } from './solution-format.js';
5
6
  import { mutateSolutionFile } from './solution-writer.js';
6
- import { createLogger } from '../core/logger.js';
7
7
  const log = createLogger('compound-lifecycle');
8
- import { ME_SOLUTIONS, ME_RULES } from '../core/paths.js';
8
+ import { ME_RULES, ME_SOLUTIONS, META_LEARNING_DIR } from '../core/paths.js';
9
+ import { safeReadJSON } from '../hooks/shared/atomic-write.js';
9
10
  /** Circuit breaker negative thresholds by status */
10
11
  const CIRCUIT_BREAKER_THRESHOLDS = {
11
12
  experiment: 2,
@@ -29,10 +30,14 @@ const STATUS_CONFIDENCE_MIN = {
29
30
  /** Get the next promotion status */
30
31
  export function nextStatus(current) {
31
32
  switch (current) {
32
- case 'experiment': return 'candidate';
33
- case 'candidate': return 'verified';
34
- case 'verified': return 'mature';
35
- default: return null;
33
+ case 'experiment':
34
+ return 'candidate';
35
+ case 'candidate':
36
+ return 'verified';
37
+ case 'verified':
38
+ return 'mature';
39
+ default:
40
+ return null;
36
41
  }
37
42
  }
38
43
  /** Get confidence for a status level.
@@ -40,30 +45,54 @@ export function nextStatus(current) {
40
45
  * Previous: 0.3/0.6/0.8/0.85 had only 0.05 gap between verified and mature. */
41
46
  export function statusConfidence(status) {
42
47
  switch (status) {
43
- case 'experiment': return 0.3;
44
- case 'candidate': return 0.55;
45
- case 'verified': return 0.75;
46
- case 'mature': return 0.90;
47
- case 'retired': return 0;
48
+ case 'experiment':
49
+ return 0.3;
50
+ case 'candidate':
51
+ return 0.55;
52
+ case 'verified':
53
+ return 0.75;
54
+ case 'mature':
55
+ return 0.9;
56
+ case 'retired':
57
+ return 0;
48
58
  }
49
59
  }
50
- /** Check promotion eligibility */
51
- export function checkPromotion(fm) {
60
+ /** Load adaptive thresholds from meta-learning state (returns null if not tuned) */
61
+ function loadAdaptiveThresholds() {
62
+ const thresholdsPath = path.join(META_LEARNING_DIR, 'lifecycle-thresholds.json');
63
+ return safeReadJSON(thresholdsPath, null);
64
+ }
65
+ /** Check promotion eligibility (with optional adaptive thresholds) */
66
+ export function checkPromotion(fm, thresholds) {
52
67
  const ev = fm.evidence;
68
+ const t = thresholds ?? null;
53
69
  switch (fm.status) {
54
- case 'experiment':
55
- // A: reflected >= 3 AND negative == 0 AND sessions >= 3 (Beta(4,1) → P(rate>0.5)=0.94)
56
- // B: reExtracted >= 2 AND negative == 0 AND reflected >= 1 (prevents trivial re-extraction)
57
- return (ev.negative === 0) && ((ev.reflected >= 3 && ev.sessions >= 3) ||
58
- (ev.reExtracted >= 2 && ev.reflected >= 1));
59
- case 'candidate':
60
- // A: reflected >= 4 AND negative == 0 AND sessions >= 3
61
- // B: reExtracted >= 2 AND negative == 0
62
- return (ev.negative === 0) && ((ev.reflected >= 4 && ev.sessions >= 3) ||
63
- (ev.reExtracted >= 2));
64
- case 'verified':
65
- // reflected >= 8, negative <= 1, sessions >= 5
66
- return ev.reflected >= 8 && ev.negative <= 1 && ev.sessions >= 5;
70
+ case 'experiment': {
71
+ const minReflected = t?.experiment.reflected ?? 3;
72
+ const minSessions = t?.experiment.sessions ?? 3;
73
+ const minReExtracted = t?.experiment.reExtracted ?? 2;
74
+ // A: reflected >= threshold AND negative == 0 AND sessions >= threshold
75
+ // B: reExtracted >= threshold AND negative == 0 AND reflected >= 1
76
+ return (ev.negative === 0 &&
77
+ ((ev.reflected >= minReflected && ev.sessions >= minSessions) ||
78
+ (ev.reExtracted >= minReExtracted && ev.reflected >= 1)));
79
+ }
80
+ case 'candidate': {
81
+ const minReflected = t?.candidate.reflected ?? 4;
82
+ const minSessions = t?.candidate.sessions ?? 3;
83
+ const minReExtracted = t?.candidate.reExtracted ?? 2;
84
+ // A: reflected >= threshold AND negative == 0 AND sessions >= threshold
85
+ // B: reExtracted >= threshold AND negative == 0
86
+ return (ev.negative === 0 &&
87
+ ((ev.reflected >= minReflected && ev.sessions >= minSessions) ||
88
+ ev.reExtracted >= minReExtracted));
89
+ }
90
+ case 'verified': {
91
+ const minReflected = t?.verified.reflected ?? 8;
92
+ const minSessions = t?.verified.sessions ?? 5;
93
+ const maxNegative = t?.verified.negative ?? 1;
94
+ return (ev.reflected >= minReflected && ev.negative <= maxNegative && ev.sessions >= minSessions);
95
+ }
67
96
  default:
68
97
  return false;
69
98
  }
@@ -88,21 +117,28 @@ export function checkIdentifierStaleness(fm, cwd) {
88
117
  if (fm.identifiers.length === 0)
89
118
  return false; // no identifiers to check
90
119
  try {
91
- const validIds = fm.identifiers.slice(0, 5).filter(id => id.length >= 6);
120
+ const validIds = fm.identifiers.slice(0, 5).filter((id) => id.length >= 6);
92
121
  // All identifiers were too short — nothing to grep, treat as stale (matches original behavior)
93
122
  if (validIds.length === 0)
94
123
  return true;
95
124
  // Escape regex metacharacters and join with OR for a single grep call
96
125
  // (previously: one execFileSync per identifier — up to 15s worst case)
97
- const pattern = validIds.map(id => id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
126
+ const pattern = validIds.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
98
127
  execFileSync('grep', [
99
- '-r', '-E',
100
- '--include=*.ts', '--include=*.tsx',
101
- '--include=*.js', '--include=*.jsx',
128
+ '-r',
129
+ '-E',
130
+ '--include=*.ts',
131
+ '--include=*.tsx',
132
+ '--include=*.js',
133
+ '--include=*.jsx',
102
134
  '--exclude-dir=node_modules',
103
135
  '--exclude-dir=dist',
104
136
  '--exclude-dir=.git',
105
- '-l', '-m', '1', pattern, '.',
137
+ '-l',
138
+ '-m',
139
+ '1',
140
+ pattern,
141
+ '.',
106
142
  ], { cwd, encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
107
143
  return false; // grep exit 0 = at least one identifier found = not stale
108
144
  }
@@ -143,7 +179,7 @@ export function isStale(fm) {
143
179
  * PR2b: solution-writer.mutateSolutionFile로 통합. lock + fresh re-read + atomic write.
144
180
  */
145
181
  export function updateSolutionFile(filePath, updates) {
146
- return mutateSolutionFile(filePath, sol => {
182
+ return mutateSolutionFile(filePath, (sol) => {
147
183
  sol.frontmatter = {
148
184
  ...sol.frontmatter,
149
185
  ...updates,
@@ -152,15 +188,17 @@ export function updateSolutionFile(filePath, updates) {
152
188
  });
153
189
  }
154
190
  /** Run lifecycle check on all solutions */
155
- export function runLifecycleCheck(sessionId = 'system') {
191
+ export function runLifecycleCheck(_sessionId = 'system') {
156
192
  const result = { promoted: [], demoted: [], retired: [], contradictions: [] };
193
+ // Meta-learning: load adaptive thresholds if available
194
+ const adaptiveThresholds = loadAdaptiveThresholds();
157
195
  const dirs = [ME_SOLUTIONS, ME_RULES];
158
196
  for (const dir of dirs) {
159
197
  if (!fs.existsSync(dir))
160
198
  continue;
161
199
  let files;
162
200
  try {
163
- files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
201
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
164
202
  }
165
203
  catch {
166
204
  continue;
@@ -182,7 +220,10 @@ export function runLifecycleCheck(sessionId = 'system') {
182
220
  // 2. Check confidence-status consistency
183
221
  const demoteTo = checkConfidenceDemotion(fm);
184
222
  if (demoteTo) {
185
- if (updateSolutionFile(filePath, { status: demoteTo, confidence: statusConfidence(demoteTo) })) {
223
+ if (updateSolutionFile(filePath, {
224
+ status: demoteTo,
225
+ confidence: statusConfidence(demoteTo),
226
+ })) {
186
227
  result.demoted.push(`${fm.name}: ${fm.status} → ${demoteTo}`);
187
228
  }
188
229
  continue;
@@ -198,7 +239,7 @@ export function runLifecycleCheck(sessionId = 'system') {
198
239
  // 4. Check promotion FIRST (with minimum age gate based on updated timestamp)
199
240
  // Promotion must run before identifier staleness to give solutions a chance
200
241
  // to be promoted before being penalized for stale identifiers.
201
- if (checkPromotion(fm)) {
242
+ if (checkPromotion(fm, adaptiveThresholds)) {
202
243
  const minAgeMs = MIN_AGE_FOR_PROMOTION[fm.status] ?? 0;
203
244
  const ageMs = Date.now() - new Date(fm.updated || fm.created).getTime();
204
245
  if (ageMs >= minAgeMs) {
@@ -215,7 +256,7 @@ export function runLifecycleCheck(sessionId = 'system') {
215
256
  if (fm.identifiers.length > 0) {
216
257
  const effectiveCwd = process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
217
258
  if (checkIdentifierStaleness(fm, effectiveCwd)) {
218
- const newConf = Math.max(0, fm.confidence - 0.20);
259
+ const newConf = Math.max(0, fm.confidence - 0.2);
219
260
  if (updateSolutionFile(filePath, { confidence: newConf })) {
220
261
  result.demoted.push(`${fm.name}: identifier-stale (confidence → ${newConf})`);
221
262
  }
@@ -239,7 +280,7 @@ export function detectContradictions(dirs) {
239
280
  if (!fs.existsSync(dir))
240
281
  continue;
241
282
  try {
242
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
283
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
243
284
  for (const file of files) {
244
285
  const content = fs.readFileSync(path.join(dir, file), 'utf-8');
245
286
  const fm = parseFrontmatterOnly(content);
@@ -248,22 +289,24 @@ export function detectContradictions(dirs) {
248
289
  solutions.push({ name: fm.name, tags: fm.tags, identifiers: fm.identifiers });
249
290
  }
250
291
  }
251
- catch { /* 솔루션 파일 파싱 실패 무시 — 중복 감지는 best-effort */ }
292
+ catch {
293
+ /* 솔루션 파일 파싱 실패 무시 — 중복 감지는 best-effort */
294
+ }
252
295
  }
253
296
  // Pre-build tag Sets for O(1) lookup — avoids O(m²) per pair
254
- const tagSets = solutions.map(s => new Set(s.tags));
297
+ const tagSets = solutions.map((s) => new Set(s.tags));
255
298
  // Pairwise comparison
256
299
  for (let i = 0; i < solutions.length; i++) {
257
300
  for (let j = i + 1; j < solutions.length; j++) {
258
301
  const a = solutions[i];
259
302
  const b = solutions[j];
260
303
  // Tags overlap > 70%
261
- const overlap = a.tags.filter(t => tagSets[j].has(t));
304
+ const overlap = a.tags.filter((t) => tagSets[j].has(t));
262
305
  const overlapRatio = overlap.length / Math.max(a.tags.length, b.tags.length, 1);
263
306
  if (overlapRatio < 0.7)
264
307
  continue;
265
308
  // Identifiers completely different
266
- const idOverlap = a.identifiers.filter(id => b.identifiers.includes(id));
309
+ const idOverlap = a.identifiers.filter((id) => b.identifiers.includes(id));
267
310
  if (idOverlap.length === 0 && a.identifiers.length > 0 && b.identifiers.length > 0) {
268
311
  contradictions.push(`${a.name} vs ${b.name} (tags ${(overlapRatio * 100).toFixed(0)}% overlap, identifiers disjoint)`);
269
312
  }
@@ -278,7 +321,7 @@ export function verifySolution(solutionName) {
278
321
  if (!fs.existsSync(dir))
279
322
  continue;
280
323
  try {
281
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
324
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
282
325
  for (const file of files) {
283
326
  const filePath = path.join(dir, file);
284
327
  // PR2c-4 (security L-1): symlink을 통한 임의 파일 read 차단.
@@ -299,7 +342,9 @@ export function verifySolution(solutionName) {
299
342
  return updateSolutionFile(filePath, { status: 'verified', confidence: 0.8 });
300
343
  }
301
344
  }
302
- catch { /* 솔루션 파일 읽기/업데이트 실패 무시 — false 반환으로 재시도 가능 */ }
345
+ catch {
346
+ /* 솔루션 파일 읽기/업데이트 실패 무시 — false 반환으로 재시도 가능 */
347
+ }
303
348
  }
304
349
  return false;
305
350
  }