@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.
- package/CHANGELOG.md +72 -0
- package/README.ja.md +79 -14
- package/README.ko.md +100 -14
- package/README.md +124 -17
- package/README.zh.md +79 -14
- package/agents/analyst.md +48 -4
- package/agents/architect.md +39 -4
- package/agents/code-reviewer.md +107 -77
- package/agents/critic.md +47 -4
- package/agents/debugger.md +46 -4
- package/agents/designer.md +40 -4
- package/agents/executor.md +112 -30
- package/agents/explore.md +45 -5
- package/agents/git-master.md +48 -4
- package/agents/planner.md +121 -18
- package/agents/test-engineer.md +58 -4
- package/agents/verifier.md +92 -77
- package/commands/architecture-decision.md +127 -258
- package/commands/calibrate.md +225 -0
- package/commands/code-review.md +163 -178
- package/commands/compound.md +127 -68
- package/commands/deep-interview.md +273 -0
- package/commands/docker.md +68 -178
- package/commands/forge-loop.md +215 -0
- package/commands/learn.md +231 -0
- package/commands/retro.md +215 -0
- package/commands/ship.md +277 -0
- package/dist/cli.js +26 -9
- package/dist/core/auto-compound-runner.js +14 -0
- package/dist/core/config-injector.d.ts +2 -1
- package/dist/core/config-injector.js +2 -1
- package/dist/core/dashboard.d.ts +108 -0
- package/dist/core/dashboard.js +495 -0
- package/dist/core/doctor.js +151 -21
- package/dist/core/drift-score.d.ts +49 -0
- package/dist/core/drift-score.js +87 -0
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/mcp-config.d.ts +2 -0
- package/dist/core/mcp-config.js +6 -1
- package/dist/core/paths.d.ts +6 -1
- package/dist/core/paths.js +18 -2
- package/dist/core/spawn.d.ts +3 -2
- package/dist/core/spawn.js +27 -8
- package/dist/core/types.d.ts +34 -0
- package/dist/engine/compound-export.d.ts +41 -0
- package/dist/engine/compound-export.js +169 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/compound-loop.js +18 -0
- package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
- package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
- package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
- package/dist/engine/meta-learning/extraction-tuner.js +99 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
- package/dist/engine/meta-learning/runner.d.ts +14 -0
- package/dist/engine/meta-learning/runner.js +90 -0
- package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
- package/dist/engine/meta-learning/scope-promoter.js +84 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
- package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
- package/dist/engine/meta-learning/types.d.ts +114 -0
- package/dist/engine/meta-learning/types.js +43 -0
- package/dist/engine/solution-format.d.ts +2 -2
- package/dist/engine/solution-format.js +249 -34
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-matcher.d.ts +30 -1
- package/dist/engine/solution-matcher.js +235 -45
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +15 -0
- package/dist/hooks/context-guard.js +218 -56
- package/dist/hooks/db-guard.js +2 -2
- package/dist/hooks/hook-config.d.ts +27 -1
- package/dist/hooks/hook-config.js +72 -12
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/intent-classifier.d.ts +0 -2
- package/dist/hooks/intent-classifier.js +32 -18
- package/dist/hooks/keyword-detector.js +126 -204
- package/dist/hooks/notepad-injector.js +2 -2
- package/dist/hooks/permission-handler.js +2 -2
- package/dist/hooks/post-tool-failure.js +12 -6
- package/dist/hooks/post-tool-handlers.d.ts +1 -1
- package/dist/hooks/post-tool-handlers.js +14 -11
- package/dist/hooks/post-tool-use.d.ts +11 -0
- package/dist/hooks/post-tool-use.js +184 -71
- package/dist/hooks/pre-compact.d.ts +11 -1
- package/dist/hooks/pre-compact.js +112 -37
- package/dist/hooks/pre-tool-use.js +86 -56
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/session-recovery.js +256 -236
- package/dist/hooks/shared/hook-response.d.ts +4 -4
- package/dist/hooks/shared/hook-response.js +13 -24
- package/dist/hooks/shared/hook-timing.d.ts +15 -0
- package/dist/hooks/shared/hook-timing.js +64 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +47 -16
- package/dist/hooks/slop-detector.js +3 -3
- package/dist/hooks/solution-injector.js +224 -197
- package/dist/hooks/subagent-tracker.js +2 -2
- package/dist/host/codex-adapter.d.ts +10 -0
- package/dist/host/codex-adapter.js +154 -0
- package/dist/mcp/solution-reader.d.ts +5 -5
- package/dist/mcp/solution-reader.js +34 -24
- package/dist/renderer/rule-renderer.js +9 -11
- package/dist/services/session.d.ts +19 -0
- package/dist/services/session.js +62 -0
- package/hooks/hooks.json +2 -2
- package/package.json +2 -1
- package/skills/architecture-decision/SKILL.md +113 -257
- package/skills/calibrate/SKILL.md +207 -0
- package/skills/code-review/SKILL.md +151 -178
- package/skills/compound/SKILL.md +126 -68
- package/skills/deep-interview/SKILL.md +266 -0
- package/skills/docker/SKILL.md +57 -179
- package/skills/forge-loop/SKILL.md +198 -0
- package/skills/learn/SKILL.md +216 -0
- package/skills/retro/SKILL.md +199 -0
- package/skills/ship/SKILL.md +259 -0
- package/agents/code-simplifier.md +0 -197
- package/agents/performance-reviewer.md +0 -172
- package/agents/qa-tester.md +0 -158
- package/agents/refactoring-expert.md +0 -168
- package/agents/scientist.md +0 -144
- package/agents/security-reviewer.md +0 -137
- package/agents/writer.md +0 -184
- package/commands/api-design.md +0 -268
- package/commands/ci-cd.md +0 -270
- package/commands/database.md +0 -263
- package/commands/debug-detective.md +0 -99
- package/commands/documentation.md +0 -276
- package/commands/ecomode.md +0 -51
- package/commands/frontend.md +0 -271
- package/commands/git-master.md +0 -90
- package/commands/incident-response.md +0 -292
- package/commands/migrate.md +0 -101
- package/commands/performance.md +0 -288
- package/commands/refactor.md +0 -105
- package/commands/security-review.md +0 -288
- package/commands/tdd.md +0 -183
- package/commands/testing-strategy.md +0 -265
- package/skills/api-design/SKILL.md +0 -262
- package/skills/ci-cd/SKILL.md +0 -264
- package/skills/database/SKILL.md +0 -257
- package/skills/debug-detective/SKILL.md +0 -95
- package/skills/documentation/SKILL.md +0 -270
- package/skills/ecomode/SKILL.md +0 -46
- package/skills/frontend/SKILL.md +0 -265
- package/skills/git-master/SKILL.md +0 -86
- package/skills/incident-response/SKILL.md +0 -286
- package/skills/migrate/SKILL.md +0 -96
- package/skills/performance/SKILL.md +0 -282
- package/skills/refactor/SKILL.md +0 -100
- package/skills/security-review/SKILL.md +0 -282
- package/skills/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
package/dist/core/spawn.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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)
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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(
|
|
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 {
|
|
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,
|
|
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':
|
|
33
|
-
|
|
34
|
-
case '
|
|
35
|
-
|
|
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':
|
|
44
|
-
|
|
45
|
-
case '
|
|
46
|
-
|
|
47
|
-
case '
|
|
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
|
-
/**
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
case '
|
|
65
|
-
|
|
66
|
-
|
|
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',
|
|
100
|
-
'
|
|
101
|
-
'--include=*.
|
|
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',
|
|
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(
|
|
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, {
|
|
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.
|
|
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 {
|
|
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 {
|
|
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) {
|