@wooojin/forgen 0.4.1 → 0.4.3
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/.claude-plugin/plugin.json +5 -5
- package/CHANGELOG.md +164 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +17 -9
- package/README.ko.md +20 -12
- package/README.md +46 -12
- package/README.zh.md +17 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- package/dist/cli.js +78 -6
- package/dist/core/auto-compound-runner.js +62 -38
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +32 -0
- package/dist/core/doctor.js +92 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -0
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +1 -0
- package/dist/core/migrate-cli.js +19 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.js +12 -0
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +4 -2
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/db-guard.js +3 -3
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +1 -1
- package/dist/hooks/post-tool-use.js +13 -4
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.js +4 -4
- package/dist/hooks/rate-limiter.js +2 -2
- package/dist/hooks/session-recovery.js +11 -0
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +18 -0
- package/dist/hooks/shared/hook-response.js +31 -0
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/stop-guard.js +15 -0
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +34 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +6 -4
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/calibrate.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/retro.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
package/dist/core/harness.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Lines 50-120: Rule file injection, gitignore, compound memory
|
|
10
10
|
* - Lines 120+: prepareHarness — main orchestration
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import type { RuntimeHost } from './types.js';
|
|
13
13
|
import { rollbackSettings } from './settings-lock.js';
|
|
14
14
|
import { type V1BootstrapResult } from './v1-bootstrap.js';
|
|
15
15
|
export interface V1HarnessContext {
|
package/dist/core/harness.js
CHANGED
|
@@ -326,32 +326,39 @@ export async function prepareHarness(cwd, options = {}) {
|
|
|
326
326
|
}
|
|
327
327
|
// 3. 환경 확인
|
|
328
328
|
const inTmux = !!process.env.TMUX;
|
|
329
|
-
// 4. Claude
|
|
329
|
+
// 4-7. Claude artifact 작업 (settings.json + agents + rules + slash commands).
|
|
330
330
|
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
331
|
+
// feat/codex-support P1-7 (2026-04-27): runtime === 'codex' 시 *.claude/* 계열
|
|
332
|
+
// 작업은 *no-op*. Codex 측 동치 prep 은 Phase 3 (install-codex.ts 의 prompts +
|
|
333
|
+
// AGENTS.md inject) 에서 처리. 본 분기는 *Claude artifact 가 Codex 환경을
|
|
334
|
+
// 오염시키지 않도록* 보호하는 비대칭 게이트.
|
|
334
335
|
const pkgRoot = getPackageRoot();
|
|
335
336
|
const env = buildEnv(cwd, v1Result.session?.session_id, runtime);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
341
|
-
if (msg.includes('settings.json lock') || msg.includes('SettingsLockError')) {
|
|
342
|
-
console.error(`[forgen] ${msg} — settings 갱신 스킵, 이전 값 유지`);
|
|
337
|
+
if (runtime === 'claude') {
|
|
338
|
+
// 4. settings.json 인젝션
|
|
339
|
+
try {
|
|
340
|
+
injectSettings(env, v1Result, runtime, cwd, pkgRoot);
|
|
343
341
|
}
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
catch (e) {
|
|
343
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
344
|
+
if (msg.includes('settings.json lock') || msg.includes('SettingsLockError')) {
|
|
345
|
+
console.error(`[forgen] ${msg} — settings 갱신 스킵, 이전 값 유지`);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
throw e;
|
|
349
|
+
}
|
|
346
350
|
}
|
|
351
|
+
// 5. 에이전트 설치
|
|
352
|
+
installAgents(cwd, pkgRoot);
|
|
353
|
+
// 6. 규칙 파일 생성 + 주입
|
|
354
|
+
const ruleFiles = generateClaudeRuleFiles(cwd, v1Result.renderedRules);
|
|
355
|
+
injectClaudeRuleFiles(cwd, ruleFiles);
|
|
356
|
+
// 7. 슬래시 명령 설치
|
|
357
|
+
installSlashCommands(cwd, pkgRoot);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
log.debug(`prepareHarness: runtime=${runtime} — Claude artifact prep skipped (Phase 3 handles Codex prep)`);
|
|
347
361
|
}
|
|
348
|
-
// 5. 에이전트 설치
|
|
349
|
-
installAgents(cwd, pkgRoot);
|
|
350
|
-
// 6. 규칙 파일 생성 및 주입 (v1 부트스트랩 결과의 renderedRules를 직접 전달)
|
|
351
|
-
const ruleFiles = generateClaudeRuleFiles(cwd, v1Result.renderedRules);
|
|
352
|
-
injectClaudeRuleFiles(cwd, ruleFiles);
|
|
353
|
-
// 7. 슬래시 명령 설치
|
|
354
|
-
installSlashCommands(cwd, pkgRoot);
|
|
355
362
|
// 8. tmux 바인딩 등록
|
|
356
363
|
if (inTmux) {
|
|
357
364
|
await registerTmuxBindings();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host detection — feat/codex-support Phase 1
|
|
3
|
+
*
|
|
4
|
+
* `forgen install` interactive 의 prerequisite — 사용자 환경에 어떤 host (Claude/Codex)
|
|
5
|
+
* 가 가용한지 탐지. spec §10 Phase 1 + interview R3.
|
|
6
|
+
*
|
|
7
|
+
* 탐지 신호 (각 host 별):
|
|
8
|
+
* - binary 가 PATH 에 있음 (`which claude` / `which codex`)
|
|
9
|
+
* - host 디렉토리 존재 (~/.claude/ / ~/.codex/)
|
|
10
|
+
* - (Codex 만) `~/.codex/auth.json` 존재 (로그인 흔적)
|
|
11
|
+
*
|
|
12
|
+
* detect 결과는 *추론* 만. install 강제 안 함.
|
|
13
|
+
*/
|
|
14
|
+
import type { HostId } from './trust-layer-intent.js';
|
|
15
|
+
export interface HostAvailability {
|
|
16
|
+
readonly host: HostId;
|
|
17
|
+
/** binary 가 PATH 에 있음. */
|
|
18
|
+
readonly binaryFound: boolean;
|
|
19
|
+
/** binary 절대경로 (없으면 null). */
|
|
20
|
+
readonly binaryPath: string | null;
|
|
21
|
+
/** host home 디렉토리 존재 (~/.claude/ 또는 ~/.codex/). */
|
|
22
|
+
readonly homeExists: boolean;
|
|
23
|
+
/** host home 절대경로. */
|
|
24
|
+
readonly homePath: string;
|
|
25
|
+
/** Codex 의 경우 auth.json 존재 (로그인 흔적). Claude 는 항상 null. */
|
|
26
|
+
readonly authPresent: boolean | null;
|
|
27
|
+
/**
|
|
28
|
+
* 종합 판단 — *install 후보로 적합한가*.
|
|
29
|
+
* - binaryFound 또는 homeExists 중 하나 이상이면 true.
|
|
30
|
+
* - 둘 다 없으면 false (사용자가 host 를 안 쓸 가능성 높음).
|
|
31
|
+
*/
|
|
32
|
+
readonly available: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface HostDetectionResult {
|
|
35
|
+
readonly claude: HostAvailability;
|
|
36
|
+
readonly codex: HostAvailability;
|
|
37
|
+
/** 둘 다 사용 가능. */
|
|
38
|
+
readonly bothAvailable: boolean;
|
|
39
|
+
/** 하나도 사용 가능하지 않음 (warn). */
|
|
40
|
+
readonly noneAvailable: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function detectAvailableHosts(): HostDetectionResult;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host detection — feat/codex-support Phase 1
|
|
3
|
+
*
|
|
4
|
+
* `forgen install` interactive 의 prerequisite — 사용자 환경에 어떤 host (Claude/Codex)
|
|
5
|
+
* 가 가용한지 탐지. spec §10 Phase 1 + interview R3.
|
|
6
|
+
*
|
|
7
|
+
* 탐지 신호 (각 host 별):
|
|
8
|
+
* - binary 가 PATH 에 있음 (`which claude` / `which codex`)
|
|
9
|
+
* - host 디렉토리 존재 (~/.claude/ / ~/.codex/)
|
|
10
|
+
* - (Codex 만) `~/.codex/auth.json` 존재 (로그인 흔적)
|
|
11
|
+
*
|
|
12
|
+
* detect 결과는 *추론* 만. install 강제 안 함.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
function which(binary) {
|
|
19
|
+
try {
|
|
20
|
+
const out = execFileSync('which', [binary], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
21
|
+
const trimmed = out.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function detectClaude() {
|
|
29
|
+
const binaryPath = which('claude');
|
|
30
|
+
const homePath = path.join(os.homedir(), '.claude');
|
|
31
|
+
const homeExists = fs.existsSync(homePath);
|
|
32
|
+
const binaryFound = binaryPath !== null;
|
|
33
|
+
return {
|
|
34
|
+
host: 'claude',
|
|
35
|
+
binaryFound,
|
|
36
|
+
binaryPath,
|
|
37
|
+
homeExists,
|
|
38
|
+
homePath,
|
|
39
|
+
authPresent: null, // Claude 는 별도 auth.json 패턴이 없음 (subscription 통합)
|
|
40
|
+
available: binaryFound || homeExists,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function detectCodex() {
|
|
44
|
+
const binaryPath = which('codex');
|
|
45
|
+
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
|
|
46
|
+
const homeExists = fs.existsSync(codexHome);
|
|
47
|
+
const binaryFound = binaryPath !== null;
|
|
48
|
+
const authPresent = fs.existsSync(path.join(codexHome, 'auth.json'));
|
|
49
|
+
return {
|
|
50
|
+
host: 'codex',
|
|
51
|
+
binaryFound,
|
|
52
|
+
binaryPath,
|
|
53
|
+
homeExists,
|
|
54
|
+
homePath: codexHome,
|
|
55
|
+
authPresent,
|
|
56
|
+
available: binaryFound || homeExists,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function detectAvailableHosts() {
|
|
60
|
+
const claude = detectClaude();
|
|
61
|
+
const codex = detectCodex();
|
|
62
|
+
return {
|
|
63
|
+
claude,
|
|
64
|
+
codex,
|
|
65
|
+
bothAvailable: claude.available && codex.available,
|
|
66
|
+
noneAvailable: !claude.available && !codex.available,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/core/installer.js
CHANGED
|
@@ -111,7 +111,7 @@ export function installAgents(cwd, pkgRoot) {
|
|
|
111
111
|
const targetDir = path.join(cwd, '.claude', 'agents');
|
|
112
112
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
113
113
|
const hashes = loadAgentHashes();
|
|
114
|
-
const sourceDir = path.join(pkgRoot, 'agents');
|
|
114
|
+
const sourceDir = path.join(pkgRoot, 'assets', 'claude', 'agents');
|
|
115
115
|
try {
|
|
116
116
|
installAgentsFromDir(sourceDir, targetDir, 'ch-', hashes);
|
|
117
117
|
cleanupStaleAgents(sourceDir, targetDir, 'ch-', hashes);
|
|
@@ -159,7 +159,7 @@ function cleanupStaleCommands(commandsDir, validFiles) {
|
|
|
159
159
|
}
|
|
160
160
|
/** 스킬을 Claude Code 슬래시 명령으로 설치 (패키지 내장만) */
|
|
161
161
|
export function installSlashCommands(_cwd, pkgRoot) {
|
|
162
|
-
let skillsDir = path.join(pkgRoot, 'commands');
|
|
162
|
+
let skillsDir = path.join(pkgRoot, 'assets', 'claude', 'commands');
|
|
163
163
|
if (!fs.existsSync(skillsDir)) {
|
|
164
164
|
skillsDir = path.join(pkgRoot, 'skills');
|
|
165
165
|
}
|
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
* - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
|
|
7
7
|
* 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
|
|
8
8
|
* 경우 영구 재기록이 필요.
|
|
9
|
+
* - evidence-host: ~/.forgen/me/behavior/*.json 에 host 필드가 없는 파일 백필.
|
|
9
10
|
*/
|
|
10
11
|
export declare function handleMigrate(args: string[]): Promise<void>;
|
package/dist/core/migrate-cli.js
CHANGED
|
@@ -6,14 +6,19 @@
|
|
|
6
6
|
* - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
|
|
7
7
|
* 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
|
|
8
8
|
* 경우 영구 재기록이 필요.
|
|
9
|
+
* - evidence-host: ~/.forgen/me/behavior/*.json 에 host 필드가 없는 파일 백필.
|
|
9
10
|
*/
|
|
10
11
|
import { migrateImplicitFeedbackLog } from '../store/implicit-feedback-store.js';
|
|
12
|
+
import { migrateEvidenceHost } from './migrate-evidence-host.js';
|
|
11
13
|
const HELP = `
|
|
12
14
|
forgen migrate — one-shot schema migrations
|
|
13
15
|
|
|
14
16
|
Usage:
|
|
15
17
|
forgen migrate implicit-feedback category 필드가 없는 레거시 엔트리 백필 + 재기록
|
|
16
18
|
forgen migrate all (현재는 implicit-feedback 과 동일)
|
|
19
|
+
forgen migrate evidence-host behavior/*.json 에 host 필드 백필
|
|
20
|
+
--dry-run 디스크 미수정, 카운트만 출력
|
|
21
|
+
--default-host <claude|codex> host 기본값 (default: claude)
|
|
17
22
|
forgen migrate --help 이 도움말
|
|
18
23
|
`;
|
|
19
24
|
export async function handleMigrate(args) {
|
|
@@ -28,6 +33,20 @@ export async function handleMigrate(args) {
|
|
|
28
33
|
console.log(`[forgen migrate] 백필 ${migrated}건, 드롭 ${dropped}건 — 재기록 완료.`);
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
36
|
+
if (sub === 'evidence-host') {
|
|
37
|
+
const dryRun = args.includes('--dry-run');
|
|
38
|
+
const hostFlagIdx = args.indexOf('--default-host');
|
|
39
|
+
const rawHost = hostFlagIdx !== -1 ? args[hostFlagIdx + 1] : 'claude';
|
|
40
|
+
if (rawHost !== 'claude' && rawHost !== 'codex') {
|
|
41
|
+
console.error(`[forgen migrate] --default-host 는 'claude' 또는 'codex' 여야 합니다. 받은 값: ${rawHost}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const defaultHost = rawHost;
|
|
45
|
+
const result = migrateEvidenceHost({ defaultHost, dryRun });
|
|
46
|
+
const label = dryRun ? ' (dry-run)' : '';
|
|
47
|
+
console.log(`[forgen] migrated: ${result.migrated} (skipped: ${result.skipped}, total: ${result.total})${label}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
31
50
|
console.error(`[forgen migrate] unknown target: ${sub}`);
|
|
32
51
|
console.error(HELP);
|
|
33
52
|
process.exit(1);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Evidence Host Backfill Migration
|
|
3
|
+
*
|
|
4
|
+
* spec §10 우선순위 5 + §4.2:
|
|
5
|
+
* 사용자가 명시적으로 디스크의 evidence 파일에 host 필드를 박제하고 싶을 때 사용.
|
|
6
|
+
* (마이그레이션 박제, audit 목적)
|
|
7
|
+
*
|
|
8
|
+
* loadEvidence 의 자동 backfill (evidence-store.ts::backfillHost) 과는 독립.
|
|
9
|
+
* 이 모듈은 디스크 파일을 직접 수정하는 명시적 경로.
|
|
10
|
+
*/
|
|
11
|
+
export interface MigrateEvidenceHostOptions {
|
|
12
|
+
/** backfill 할 host 값 (host 필드 없는 파일에만 적용) */
|
|
13
|
+
defaultHost: 'claude' | 'codex';
|
|
14
|
+
/**
|
|
15
|
+
* true 이면 디스크를 수정하지 않고 카운트만 반환.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface MigrateEvidenceHostResult {
|
|
21
|
+
/** host 필드를 새로 추가한 파일 수 */
|
|
22
|
+
migrated: number;
|
|
23
|
+
/** 처리 대상 전체 .json 파일 수 */
|
|
24
|
+
total: number;
|
|
25
|
+
/** 이미 host 필드가 있어 건너뛴 파일 수 */
|
|
26
|
+
skipped: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `~/.forgen/me/behavior/*.json` 을 순회하여 host 필드가 없는 파일에
|
|
30
|
+
* `defaultHost` 를 추가한다.
|
|
31
|
+
*
|
|
32
|
+
* - dryRun=true 이면 디스크 미수정, 카운트만 반환.
|
|
33
|
+
* - 이미 host 필드가 있는 파일은 건너뜀 (idempotent).
|
|
34
|
+
* - 파싱 실패 / host 필드가 아닌 값인 파일도 건너뜀 (안전).
|
|
35
|
+
*/
|
|
36
|
+
export declare function migrateEvidenceHost(options: MigrateEvidenceHostOptions): MigrateEvidenceHostResult;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Evidence Host Backfill Migration
|
|
3
|
+
*
|
|
4
|
+
* spec §10 우선순위 5 + §4.2:
|
|
5
|
+
* 사용자가 명시적으로 디스크의 evidence 파일에 host 필드를 박제하고 싶을 때 사용.
|
|
6
|
+
* (마이그레이션 박제, audit 목적)
|
|
7
|
+
*
|
|
8
|
+
* loadEvidence 의 자동 backfill (evidence-store.ts::backfillHost) 과는 독립.
|
|
9
|
+
* 이 모듈은 디스크 파일을 직접 수정하는 명시적 경로.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { ME_BEHAVIOR } from './paths.js';
|
|
14
|
+
import { atomicWriteJSON, safeReadJSON } from '../hooks/shared/atomic-write.js';
|
|
15
|
+
/**
|
|
16
|
+
* `~/.forgen/me/behavior/*.json` 을 순회하여 host 필드가 없는 파일에
|
|
17
|
+
* `defaultHost` 를 추가한다.
|
|
18
|
+
*
|
|
19
|
+
* - dryRun=true 이면 디스크 미수정, 카운트만 반환.
|
|
20
|
+
* - 이미 host 필드가 있는 파일은 건너뜀 (idempotent).
|
|
21
|
+
* - 파싱 실패 / host 필드가 아닌 값인 파일도 건너뜀 (안전).
|
|
22
|
+
*/
|
|
23
|
+
export function migrateEvidenceHost(options) {
|
|
24
|
+
const { defaultHost, dryRun = false } = options;
|
|
25
|
+
if (!fs.existsSync(ME_BEHAVIOR)) {
|
|
26
|
+
return { migrated: 0, total: 0, skipped: 0 };
|
|
27
|
+
}
|
|
28
|
+
const files = fs.readdirSync(ME_BEHAVIOR).filter((f) => f.endsWith('.json'));
|
|
29
|
+
let migrated = 0;
|
|
30
|
+
let skipped = 0;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const filePath = path.join(ME_BEHAVIOR, file);
|
|
33
|
+
const data = safeReadJSON(filePath, null);
|
|
34
|
+
if (data === null || typeof data !== 'object') {
|
|
35
|
+
skipped++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const host = data['host'];
|
|
39
|
+
if (host === 'claude' || host === 'codex') {
|
|
40
|
+
skipped++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!dryRun) {
|
|
44
|
+
atomicWriteJSON(filePath, { ...data, host: defaultHost }, { pretty: true });
|
|
45
|
+
}
|
|
46
|
+
migrated++;
|
|
47
|
+
}
|
|
48
|
+
return { migrated, total: files.length, skipped };
|
|
49
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { generateHooksJson } from '../hooks/hooks-generator.js';
|
|
10
|
+
import { getHostRuntime } from '../host/host-runtime.js';
|
|
10
11
|
import { ConfigError } from './errors.js';
|
|
11
12
|
import { createLogger } from './logger.js';
|
|
12
13
|
import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, readSettingsSafely, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
|
|
@@ -74,7 +75,8 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
|
|
|
74
75
|
hooksConfig[event] = filtered;
|
|
75
76
|
}
|
|
76
77
|
try {
|
|
77
|
-
|
|
78
|
+
const host = getHostRuntime(runtime);
|
|
79
|
+
if (host.hookInjectionStrategy === 'generate') {
|
|
78
80
|
const generated = generateHooksJson({ cwd, runtime, pluginRoot: path.join(pkgRoot, 'dist') });
|
|
79
81
|
for (const [event, handlers] of Object.entries(generated.hooks)) {
|
|
80
82
|
if (!hooksConfig[event])
|
|
@@ -83,7 +85,7 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
else {
|
|
86
|
-
//
|
|
88
|
+
// 'pre-baked-file': pkgRoot/hooks/hooks.json 읽고 ${CLAUDE_PLUGIN_ROOT} 치환
|
|
87
89
|
const hooksJsonPath = path.join(pkgRoot, 'hooks', 'hooks.json');
|
|
88
90
|
if (fs.existsSync(hooksJsonPath)) {
|
|
89
91
|
const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'));
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { V1HarnessContext } from './harness.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { RuntimeHost } from './types.js';
|
|
3
3
|
/** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
|
|
4
4
|
export declare function spawnClaude(args: string[], context: V1HarnessContext, runtime?: RuntimeHost): Promise<number>;
|
|
5
5
|
/**
|
package/dist/core/spawn.js
CHANGED
|
@@ -7,13 +7,11 @@ import { buildEnv } from './config-injector.js';
|
|
|
7
7
|
import { loadGlobalConfig } from './global-config.js';
|
|
8
8
|
import { createLogger } from './logger.js';
|
|
9
9
|
import { STATE_DIR } from './paths.js';
|
|
10
|
+
import { getHostRuntime } from '../host/host-runtime.js';
|
|
10
11
|
const log = createLogger('spawn');
|
|
11
|
-
/**
|
|
12
|
-
function findClaude() {
|
|
13
|
-
return 'claude';
|
|
14
|
-
}
|
|
12
|
+
/** Phase 2: host-runtime 어댑터 위임. */
|
|
15
13
|
function findRuntimeLauncher(runtime) {
|
|
16
|
-
return runtime
|
|
14
|
+
return getHostRuntime(runtime).launcher;
|
|
17
15
|
}
|
|
18
16
|
function transcriptProjectDir(cwd) {
|
|
19
17
|
// Claude Code는 cwd의 /를 -로 치환하고 선행 -를 유지
|
|
@@ -161,12 +159,7 @@ export async function spawnClaude(args, context, runtime = 'claude') {
|
|
|
161
159
|
});
|
|
162
160
|
child.on('error', (err) => {
|
|
163
161
|
if (err.code === 'ENOENT') {
|
|
164
|
-
|
|
165
|
-
reject(new Error('Codex is not installed.'));
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
reject(new Error('Claude Code is not installed. npm install -g @anthropic-ai/claude-code'));
|
|
169
|
-
}
|
|
162
|
+
reject(new Error(getHostRuntime(runtime).missingInstallMessage));
|
|
170
163
|
}
|
|
171
164
|
else {
|
|
172
165
|
reject(err);
|
package/dist/core/stats-cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as path from 'node:path';
|
|
|
10
10
|
import { loadAllRules } from '../store/rule-store.js';
|
|
11
11
|
import { loadAllEvidence } from '../store/evidence-store.js';
|
|
12
12
|
import { STATE_DIR, ME_DIR } from './paths.js';
|
|
13
|
+
import { computeFixFeatRatio, formatFixRatio } from './git-stats.js';
|
|
13
14
|
// v0.4.1 격리 fix: 이전에는 os.homedir() 직접 사용해서 FORGEN_HOME env 로
|
|
14
15
|
// 홈 격리해도 이 파일의 경로는 여전히 실 홈 가리켰음. paths.ts 상수 import.
|
|
15
16
|
const ENFORCEMENT_DIR = path.join(STATE_DIR, 'enforcement');
|
|
@@ -244,6 +245,17 @@ export function renderStats(s) {
|
|
|
244
245
|
lines.push(` Last reclass ${s.philosophy.lastReclassification ?? 'never'}`);
|
|
245
246
|
lines.push('');
|
|
246
247
|
}
|
|
248
|
+
// P4 셀프 가드 — 최근 30커밋 fix:feat 비율로 회귀 패턴 자가 노출.
|
|
249
|
+
// 30% 초과 시 "이거 고치면 저거 버그난다" 패턴 의심 → forgen doctor 가 경고.
|
|
250
|
+
try {
|
|
251
|
+
const ratio = computeFixFeatRatio();
|
|
252
|
+
if (ratio.available) {
|
|
253
|
+
lines.push(' Repo health (last 30 commits)');
|
|
254
|
+
lines.push(` ${formatFixRatio(ratio)}`);
|
|
255
|
+
lines.push('');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch { /* fail-open: git 없거나 비-repo 환경 */ }
|
|
247
259
|
lines.push(` Last extraction: ${s.lastExtraction}`);
|
|
248
260
|
lines.push('');
|
|
249
261
|
return lines.join('\n');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Layer Intent — Multi-Host Core Design §9.0 산출물 #1
|
|
3
|
+
*
|
|
4
|
+
* forgen 이 host 위에서 보장하는 행동의 enum. spec §9.0 의 7 의도 매트릭스와 1:1.
|
|
5
|
+
* 각 host adapter 는 이 enum 의 모든 항목에 대해 CapabilityDeclaration 을 선언해야 하며,
|
|
6
|
+
* 미선언은 컴파일 타임(`Record<TrustLayerIntent, _>`) + 런타임(`assertCapabilitiesComplete`) 양쪽에서 fail.
|
|
7
|
+
*
|
|
8
|
+
* 1원칙: Claude semantics 가 reference. 본 enum 의 의미는 Claude Hook schema 의 행동을 그대로 사용한다.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TRUST_LAYER_INTENTS: readonly ["block-completion", "block-tool-use", "inject-context", "observe-only", "secret-filter", "forge-loop-state-inject", "self-evidence-record"];
|
|
11
|
+
export type TrustLayerIntent = (typeof TRUST_LAYER_INTENTS)[number];
|
|
12
|
+
export type CapabilityStatus = 'supported' | 'partial' | 'unsupported';
|
|
13
|
+
export interface CapabilityDeclaration {
|
|
14
|
+
readonly status: CapabilityStatus;
|
|
15
|
+
/** host 표면이 이 의도를 표현하는 hook/필드 (예: "Stop + decision:'block' + reason"). */
|
|
16
|
+
readonly expression: string;
|
|
17
|
+
/** partial/unsupported 시 등가성 보존을 위한 mitigation 핸들. supported 면 undefined. */
|
|
18
|
+
readonly mitigation?: string;
|
|
19
|
+
/** source-of-truth (spec 또는 외부 docs/source 인용). */
|
|
20
|
+
readonly source?: string;
|
|
21
|
+
}
|
|
22
|
+
export type HostId = 'claude' | 'codex';
|
|
23
|
+
export interface HostCapabilities {
|
|
24
|
+
readonly hostId: HostId;
|
|
25
|
+
/**
|
|
26
|
+
* 모든 TrustLayerIntent 에 대한 선언. `Record<TrustLayerIntent, _>` 타입이
|
|
27
|
+
* 컴파일 타임에 누락을 차단한다.
|
|
28
|
+
*/
|
|
29
|
+
readonly intents: Record<TrustLayerIntent, CapabilityDeclaration>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 런타임 assertion — host adapter 가 새 의도 추가를 누락한 경우 fail.
|
|
33
|
+
* 컴파일 타임 가드를 우회하는 동적 생성 코드를 위한 안전망.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertCapabilitiesComplete(caps: HostCapabilities): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Layer Intent — Multi-Host Core Design §9.0 산출물 #1
|
|
3
|
+
*
|
|
4
|
+
* forgen 이 host 위에서 보장하는 행동의 enum. spec §9.0 의 7 의도 매트릭스와 1:1.
|
|
5
|
+
* 각 host adapter 는 이 enum 의 모든 항목에 대해 CapabilityDeclaration 을 선언해야 하며,
|
|
6
|
+
* 미선언은 컴파일 타임(`Record<TrustLayerIntent, _>`) + 런타임(`assertCapabilitiesComplete`) 양쪽에서 fail.
|
|
7
|
+
*
|
|
8
|
+
* 1원칙: Claude semantics 가 reference. 본 enum 의 의미는 Claude Hook schema 의 행동을 그대로 사용한다.
|
|
9
|
+
*/
|
|
10
|
+
export const TRUST_LAYER_INTENTS = [
|
|
11
|
+
'block-completion',
|
|
12
|
+
'block-tool-use',
|
|
13
|
+
'inject-context',
|
|
14
|
+
'observe-only',
|
|
15
|
+
'secret-filter',
|
|
16
|
+
'forge-loop-state-inject',
|
|
17
|
+
'self-evidence-record',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* 런타임 assertion — host adapter 가 새 의도 추가를 누락한 경우 fail.
|
|
21
|
+
* 컴파일 타임 가드를 우회하는 동적 생성 코드를 위한 안전망.
|
|
22
|
+
*/
|
|
23
|
+
export function assertCapabilitiesComplete(caps) {
|
|
24
|
+
const declared = new Set(Object.keys(caps.intents));
|
|
25
|
+
const missing = TRUST_LAYER_INTENTS.filter((i) => !declared.has(i));
|
|
26
|
+
if (missing.length > 0) {
|
|
27
|
+
throw new Error(`HostCapabilities for "${caps.hostId}" missing intents: ${missing.join(', ')}. ` +
|
|
28
|
+
`All TrustLayerIntent values must be declared (spec §9.0).`);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ export type RuntimeHost = 'claude' | 'codex';
|
|
|
112
112
|
export interface LaunchContext {
|
|
113
113
|
runtime: RuntimeHost;
|
|
114
114
|
args: string[];
|
|
115
|
-
runtimeSource: 'flag' | 'env' | 'default';
|
|
115
|
+
runtimeSource: 'flag' | 'env' | 'profile' | 'default';
|
|
116
116
|
}
|
|
117
117
|
/** 훅 입력 이벤트 스키마 (버전 간 상위 호환용 최소 스펙) */
|
|
118
118
|
export interface HookEventInput {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import * as fs from 'node:fs';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
|
+
import { execHost } from '../host/exec-host.js';
|
|
21
22
|
import { serializeSolutionV3, DEFAULT_EVIDENCE, extractTags } from './solution-format.js';
|
|
22
23
|
import { createLogger } from '../core/logger.js';
|
|
23
24
|
const log = createLogger('compound-extractor');
|
|
@@ -655,15 +656,12 @@ function enrichSolutionContent(solution, diffSnippet) {
|
|
|
655
656
|
'코드 변경 (일부):',
|
|
656
657
|
diffSnippet.slice(0, 2000),
|
|
657
658
|
].join('\n');
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (enriched.length > 30 && enriched.length < 1000) {
|
|
665
|
-
return enriched;
|
|
666
|
-
}
|
|
659
|
+
// feat/codex-support P2-2 — host-aware exec via profile.default_host.
|
|
660
|
+
// Codex 메인 사용자도 자동 추출 enrichment 가능 (해당 host CLI 호출).
|
|
661
|
+
// fail-open 정책 유지 — LLM enrichment 실패는 추출 자체를 막지 않음.
|
|
662
|
+
const { message } = execHost({ prompt, model: 'haiku', timeout: 15000 });
|
|
663
|
+
if (message.length > 30 && message.length < 1000)
|
|
664
|
+
return message;
|
|
667
665
|
return null;
|
|
668
666
|
}
|
|
669
667
|
catch {
|
package/dist/engine/learn-cli.js
CHANGED
|
@@ -147,10 +147,12 @@ function runEvolve(args) {
|
|
|
147
147
|
const rollbackIdx = args.indexOf('--rollback');
|
|
148
148
|
const promoteIdx = args.indexOf('--promote');
|
|
149
149
|
if (rollbackIdx >= 0 && args[rollbackIdx + 1]) {
|
|
150
|
-
|
|
150
|
+
runEvolveRollback(args[rollbackIdx + 1]);
|
|
151
|
+
return;
|
|
151
152
|
}
|
|
152
153
|
if (promoteIdx >= 0 && args[promoteIdx + 1]) {
|
|
153
|
-
|
|
154
|
+
runEvolvePromote(args[promoteIdx + 1]);
|
|
155
|
+
return;
|
|
154
156
|
}
|
|
155
157
|
// Default: generate + optionally save weakness report, print proposer
|
|
156
158
|
// brief so the user can hand it to the ch-solution-evolver agent.
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* Rule.policy 자연어에서 "피해야 할 패턴" 을 추출하고, Write/Edit/Bash 도구
|
|
5
5
|
* 출력에서 해당 패턴을 찾아 BypassEntry 후보로 반환한다.
|
|
6
6
|
*
|
|
7
|
-
* Heuristic:
|
|
7
|
+
* Heuristic priority (most explicit first):
|
|
8
|
+
* 0) Parenthesized examples (e.g., "(rm -rf, DROP, force-push)") → tokens inside
|
|
8
9
|
* 1) "use X not Y" / "use X instead of Y" / "X over Y" → bypass = Y
|
|
9
10
|
* 2) "avoid X" / "don't use X" / "never use X" / "do not use X" → bypass = X
|
|
10
11
|
* 3) Korean: "X 말라" / "X 금지" / "X 하지 않" → bypass = X
|
|
11
12
|
* 4) 그 외: 빈 배열 (탐지 불가).
|
|
12
13
|
*
|
|
14
|
+
* Stop list filter: generic Korean verbs (실행/사용/선언/...) extracted by Korean
|
|
15
|
+
* heuristic are removed — they cause massive FP (RC5/E9: matched the word "실행"
|
|
16
|
+
* everywhere instead of "rm -rf"). 64 false-positive bypasses observed before fix.
|
|
17
|
+
*
|
|
13
18
|
* 반환된 패턴은 escape 된 정규식 문자열 — caller 가 `new RegExp(p)` 로 사용.
|
|
14
19
|
*/
|
|
15
20
|
import type { Rule } from '../../store/types.js';
|