@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.
- package/CHANGELOG.md +44 -0
- package/README.ko.md +25 -14
- package/README.md +61 -17
- 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 +212 -110
- 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 +17 -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 +17 -0
- package/dist/core/dashboard.js +112 -2
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- 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-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- 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 +7 -1
- package/dist/engine/solution-matcher.js +114 -37
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +5 -0
- package/dist/hooks/context-guard.js +118 -2
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/keyword-detector.js +16 -100
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +6 -4
- 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/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 +210 -110
- 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/specify.md +0 -128
- 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/specify/SKILL.md +0 -122
- package/skills/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
package/dist/core/harness.js
CHANGED
|
@@ -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 (
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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(
|
|
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();
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -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", "
|
|
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 — 팀 팩 연결 파일 */
|
package/dist/core/paths.js
CHANGED
|
@@ -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',
|
|
84
|
-
'
|
|
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) {
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -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
|
|
9
|
+
export declare function spawnClaudeWithResume(args: string[], context: V1HarnessContext, contextFactory: () => Promise<V1HarnessContext>, runtime?: RuntimeHost): Promise<void>;
|
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
|
+
}
|
|
@@ -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
|
}
|