@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
@@ -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
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Forgen — Compound Knowledge Export/Import
3
+ *
4
+ * Provides backup, migration, and sharing of accumulated personal knowledge
5
+ * stored under ~/.forgen/me/ (solutions/, rules/, behavior/).
6
+ *
7
+ * Export creates a tar.gz archive; Import extracts it while skipping existing
8
+ * files to prevent accidental overwrites.
9
+ */
10
+ export interface ExportResult {
11
+ outputPath: string;
12
+ counts: Record<string, number>;
13
+ totalFiles: number;
14
+ }
15
+ export interface ImportResult {
16
+ imported: number;
17
+ skipped: number;
18
+ details: {
19
+ file: string;
20
+ action: 'imported' | 'skipped';
21
+ }[];
22
+ }
23
+ /**
24
+ * Export knowledge directories to a tar.gz archive.
25
+ *
26
+ * Uses `tar czf` via child_process for simplicity and reliability.
27
+ * Only archives solutions/, rules/, behavior/ under ME_DIR.
28
+ */
29
+ export declare function exportKnowledge(outputPath?: string): ExportResult;
30
+ /**
31
+ * Import knowledge from a tar.gz archive.
32
+ *
33
+ * For each file in the archive, if a file with the same name already exists
34
+ * in the target directory, it is SKIPPED (no overwrite). Only new files are
35
+ * added.
36
+ */
37
+ export declare function importKnowledge(archivePath: string): ImportResult;
38
+ /** CLI handler: forgen compound export */
39
+ export declare function handleExport(args: string[]): Promise<void>;
40
+ /** CLI handler: forgen compound import */
41
+ export declare function handleImport(args: string[]): Promise<void>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Forgen — Compound Knowledge Export/Import
3
+ *
4
+ * Provides backup, migration, and sharing of accumulated personal knowledge
5
+ * stored under ~/.forgen/me/ (solutions/, rules/, behavior/).
6
+ *
7
+ * Export creates a tar.gz archive; Import extracts it while skipping existing
8
+ * files to prevent accidental overwrites.
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import * as os from 'node:os';
12
+ import * as path from 'node:path';
13
+ import { execFileSync } from 'node:child_process';
14
+ import { ME_DIR } from '../core/paths.js';
15
+ /** Directories within ME_DIR to include in the archive. */
16
+ const KNOWLEDGE_DIRS = ['solutions', 'rules', 'behavior'];
17
+ /**
18
+ * Count .md files in a directory (non-recursive).
19
+ * Returns 0 if the directory does not exist.
20
+ */
21
+ function countFiles(dir) {
22
+ try {
23
+ if (!fs.existsSync(dir))
24
+ return 0;
25
+ return fs.readdirSync(dir).filter(f => f.endsWith('.md')).length;
26
+ }
27
+ catch {
28
+ return 0;
29
+ }
30
+ }
31
+ /**
32
+ * Export knowledge directories to a tar.gz archive.
33
+ *
34
+ * Uses `tar czf` via child_process for simplicity and reliability.
35
+ * Only archives solutions/, rules/, behavior/ under ME_DIR.
36
+ */
37
+ export function exportKnowledge(outputPath) {
38
+ const date = new Date().toISOString().split('T')[0];
39
+ const resolved = outputPath ?? path.join(process.cwd(), `forgen-knowledge-${date}.tar.gz`);
40
+ // Gather counts before archiving
41
+ const counts = {};
42
+ const existingDirs = [];
43
+ for (const name of KNOWLEDGE_DIRS) {
44
+ const dir = path.join(ME_DIR, name);
45
+ const count = countFiles(dir);
46
+ counts[name] = count;
47
+ if (fs.existsSync(dir)) {
48
+ existingDirs.push(name);
49
+ }
50
+ }
51
+ const totalFiles = Object.values(counts).reduce((a, b) => a + b, 0);
52
+ if (existingDirs.length === 0) {
53
+ throw new Error('No knowledge directories found to export.');
54
+ }
55
+ // Ensure output directory exists
56
+ const outDir = path.dirname(resolved);
57
+ fs.mkdirSync(outDir, { recursive: true });
58
+ // Create tar.gz relative to ME_DIR so archive paths are solutions/*, rules/*, behavior/*
59
+ execFileSync('tar', ['czf', resolved, ...existingDirs], {
60
+ cwd: ME_DIR,
61
+ timeout: 30000,
62
+ stdio: ['pipe', 'pipe', 'pipe'],
63
+ });
64
+ return { outputPath: resolved, counts, totalFiles };
65
+ }
66
+ /**
67
+ * Import knowledge from a tar.gz archive.
68
+ *
69
+ * For each file in the archive, if a file with the same name already exists
70
+ * in the target directory, it is SKIPPED (no overwrite). Only new files are
71
+ * added.
72
+ */
73
+ export function importKnowledge(archivePath) {
74
+ if (!fs.existsSync(archivePath)) {
75
+ throw new Error(`Archive not found: ${archivePath}`);
76
+ }
77
+ // List files in the archive
78
+ const listOutput = execFileSync('tar', ['tzf', archivePath], {
79
+ timeout: 30000,
80
+ encoding: 'utf-8',
81
+ stdio: ['pipe', 'pipe', 'pipe'],
82
+ });
83
+ const archiveFiles = listOutput
84
+ .split('\n')
85
+ .map(f => f.trim())
86
+ .filter(f => f && !f.endsWith('/'));
87
+ // Extract to a temp directory first, then selectively copy
88
+ const tmpDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'forgen-import-'));
89
+ try {
90
+ execFileSync('tar', ['xzf', archivePath, '-C', tmpDir], {
91
+ timeout: 30000,
92
+ stdio: ['pipe', 'pipe', 'pipe'],
93
+ });
94
+ const result = { imported: 0, skipped: 0, details: [] };
95
+ for (const relFile of archiveFiles) {
96
+ const srcPath = path.join(tmpDir, relFile);
97
+ const destPath = path.join(ME_DIR, relFile);
98
+ // Security: ensure the dest path stays within ME_DIR
99
+ const realDest = path.resolve(destPath);
100
+ if (!realDest.startsWith(ME_DIR)) {
101
+ result.skipped++;
102
+ result.details.push({ file: relFile, action: 'skipped' });
103
+ continue;
104
+ }
105
+ if (fs.existsSync(destPath)) {
106
+ result.skipped++;
107
+ result.details.push({ file: relFile, action: 'skipped' });
108
+ }
109
+ else {
110
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
111
+ fs.copyFileSync(srcPath, destPath);
112
+ result.imported++;
113
+ result.details.push({ file: relFile, action: 'imported' });
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ finally {
119
+ // Clean up temp directory
120
+ fs.rmSync(tmpDir, { recursive: true, force: true });
121
+ }
122
+ }
123
+ /** CLI handler: forgen compound export */
124
+ export async function handleExport(args) {
125
+ const outputIdx = args.indexOf('--output');
126
+ const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : undefined;
127
+ try {
128
+ const result = exportKnowledge(outputPath);
129
+ console.log('\n Compound Knowledge Export\n');
130
+ console.log(` Output: ${result.outputPath}`);
131
+ console.log();
132
+ for (const [category, count] of Object.entries(result.counts)) {
133
+ console.log(` ${category}: ${count} files`);
134
+ }
135
+ console.log(`\n Total: ${result.totalFiles} files exported.\n`);
136
+ }
137
+ catch (e) {
138
+ console.error(`\n Export failed: ${e.message}\n`);
139
+ process.exit(1);
140
+ }
141
+ }
142
+ /** CLI handler: forgen compound import */
143
+ export async function handleImport(args) {
144
+ const archivePath = args[0];
145
+ if (!archivePath || archivePath.startsWith('--')) {
146
+ console.log(' Usage: forgen compound import <path-to-archive>\n');
147
+ return;
148
+ }
149
+ try {
150
+ const resolved = path.resolve(archivePath);
151
+ const result = importKnowledge(resolved);
152
+ console.log('\n Compound Knowledge Import\n');
153
+ console.log(` Archive: ${resolved}`);
154
+ console.log(` Imported: ${result.imported} new files`);
155
+ console.log(` Skipped: ${result.skipped} existing files`);
156
+ if (result.details.length > 0 && result.details.length <= 20) {
157
+ console.log();
158
+ for (const d of result.details) {
159
+ const icon = d.action === 'imported' ? '+' : '-';
160
+ console.log(` ${icon} ${d.file}`);
161
+ }
162
+ }
163
+ console.log();
164
+ }
165
+ catch (e) {
166
+ console.error(`\n Import failed: ${e.message}\n`);
167
+ process.exit(1);
168
+ }
169
+ }
@@ -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
  }
@@ -190,6 +190,11 @@ export async function handleCompound(args) {
190
190
  forgen compound --lifecycle Run promotion/demotion/circuit-breaker check
191
191
  forgen compound --verify <name> Manually promote solution to verified
192
192
 
193
+ Export/Import:
194
+ forgen compound export [--output path]
195
+ Export knowledge to tar.gz archive
196
+ forgen compound import <path> Import knowledge from archive (skip existing)
197
+
193
198
  Auto-extraction:
194
199
  forgen compound --pause-auto Pause auto-extraction
195
200
  forgen compound --resume-auto Resume auto-extraction
@@ -199,6 +204,18 @@ export async function handleCompound(args) {
199
204
  `);
200
205
  return;
201
206
  }
207
+ // --- export command ---
208
+ if (args[0] === 'export') {
209
+ const { handleExport } = await import('./compound-export.js');
210
+ await handleExport(args.slice(1));
211
+ return;
212
+ }
213
+ // --- import command ---
214
+ if (args[0] === 'import') {
215
+ const { handleImport } = await import('./compound-export.js');
216
+ await handleImport(args.slice(1));
217
+ return;
218
+ }
202
219
  // --pause-auto / --resume-auto
203
220
  if (args.includes('--pause-auto') || args.includes('pause-auto')) {
204
221
  const { pauseExtraction } = await import('./compound-extractor.js');
@@ -375,6 +392,7 @@ export async function handleCompound(args) {
375
392
  '--lifecycle', '--verify', '--save', '--interactive',
376
393
  'list', 'inspect', 'remove', 'rollback', 'retag', 'lifecycle',
377
394
  '--list', '--inspect', '--remove', '--rollback', '--retag', '--since', 'interactive',
395
+ 'export', 'import', '--output',
378
396
  ];
379
397
  const hasTypeFlag = knownFlags.some(f => args.includes(f));
380
398
  if (!hasTypeFlag) {