@wooojin/forgen 0.4.0 → 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 +194 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +74 -9
- package/README.ko.md +77 -12
- package/README.md +127 -25
- package/README.zh.md +43 -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/checks/conclusion-verification-ratio.d.ts +37 -0
- package/dist/checks/conclusion-verification-ratio.js +86 -0
- package/dist/checks/fact-vs-agreement.d.ts +47 -0
- package/dist/checks/fact-vs-agreement.js +92 -0
- package/dist/checks/self-score-deflation.d.ts +38 -0
- package/dist/checks/self-score-deflation.js +108 -0
- package/dist/cli.js +98 -6
- package/dist/core/auto-compound-runner.js +137 -49
- 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 +41 -2
- package/dist/core/doctor.js +118 -5
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -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/init-cli.d.ts +26 -0
- package/dist/core/init-cli.js +104 -0
- package/dist/core/init.js +17 -0
- package/dist/core/inspect-cli.js +1 -2
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +11 -0
- package/dist/core/migrate-cli.js +53 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/paths.d.ts +8 -1
- package/dist/core/paths.js +11 -2
- package/dist/core/recall-cli.d.ts +26 -0
- package/dist/core/recall-cli.js +125 -0
- package/dist/core/recall-reference-detector.d.ts +43 -0
- package/dist/core/recall-reference-detector.js +65 -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.d.ts +21 -0
- package/dist/core/stats-cli.js +133 -10
- 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/core/uninstall.js +2 -1
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +5 -6
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
- package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
- package/dist/engine/lifecycle/orchestrator.js +2 -2
- package/dist/engine/lifecycle/signals.js +6 -6
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
- package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
- package/dist/engine/skill-promoter.js +3 -6
- 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/context-guard.js +1 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +21 -5
- 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/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +2 -2
- package/dist/hooks/notepad-injector.js +1 -1
- package/dist/hooks/permission-handler.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +7 -1
- package/dist/hooks/post-tool-use.js +50 -23
- package/dist/hooks/pre-compact.js +2 -2
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +28 -10
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +1 -1
- package/dist/hooks/session-recovery.js +12 -1
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -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 +30 -2
- package/dist/hooks/shared/hook-response.js +61 -3
- package/dist/hooks/skill-injector.js +2 -2
- package/dist/hooks/slop-detector.js +2 -2
- package/dist/hooks/solution-injector.d.ts +9 -0
- package/dist/hooks/solution-injector.js +48 -5
- package/dist/hooks/stop-guard.js +152 -13
- package/dist/hooks/subagent-tracker.js +1 -1
- 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/i18n/index.js +3 -5
- 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 +45 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/rule-store.js +8 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +7 -5
- 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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install orchestrator — feat/codex-support P1-3
|
|
3
|
+
*
|
|
4
|
+
* `forgen install` CLI 의 분기 처리:
|
|
5
|
+
* - 인자 없음 → interactive 3-choice (claude/codex/both/quit)
|
|
6
|
+
* - 'claude' → planClaudeInstall()
|
|
7
|
+
* - 'codex' → planCodexInstall()
|
|
8
|
+
* - 'both' → 둘 다 실행
|
|
9
|
+
*
|
|
10
|
+
* 사용자 host 선택 권한이 forgen 측에 위임 (1원칙: Claude default 강요 금지).
|
|
11
|
+
* Phase 1 Round 2 의 *마이그레이션 정책 C* (기존 entry 보존) 와 함께 동작.
|
|
12
|
+
*/
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as readline from 'node:readline';
|
|
15
|
+
import { detectAvailableHosts } from '../core/host-detect.js';
|
|
16
|
+
import { planClaudeInstall } from './install-claude.js';
|
|
17
|
+
import { planCodexInstall } from './install-codex.js';
|
|
18
|
+
function askChoice(rl, question, validChoices) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const ask = () => {
|
|
21
|
+
rl.question(question, (answer) => {
|
|
22
|
+
const trimmed = answer.trim().toLowerCase();
|
|
23
|
+
if (validChoices.includes(trimmed))
|
|
24
|
+
resolve(trimmed);
|
|
25
|
+
else {
|
|
26
|
+
console.log(` Please enter one of: ${validChoices.join(', ')}`);
|
|
27
|
+
ask();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
ask();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function renderHostStatus(host) {
|
|
35
|
+
if (!host.available)
|
|
36
|
+
return ` ✗ ${host.host} (not detected — binary 미설치 + ~/.${host.host}/ 부재)`;
|
|
37
|
+
const bits = [];
|
|
38
|
+
if (host.binaryFound)
|
|
39
|
+
bits.push(`binary: ${host.binaryPath}`);
|
|
40
|
+
if (host.homeExists)
|
|
41
|
+
bits.push(`home: ${host.homePath}`);
|
|
42
|
+
if (host.host === 'codex' && host.authPresent)
|
|
43
|
+
bits.push('auth: present');
|
|
44
|
+
return ` ✓ ${host.host} (${bits.join(', ')})`;
|
|
45
|
+
}
|
|
46
|
+
async function chooseTargetInteractively(detection) {
|
|
47
|
+
console.log('\n [forgen] Setup wizard\n');
|
|
48
|
+
console.log(' Detected hosts:');
|
|
49
|
+
console.log(renderHostStatus(detection.claude));
|
|
50
|
+
console.log(renderHostStatus(detection.codex));
|
|
51
|
+
console.log('');
|
|
52
|
+
if (detection.noneAvailable) {
|
|
53
|
+
console.log(' ⚠ Neither Claude nor Codex detected. Install one of:');
|
|
54
|
+
console.log(' - Claude Code: npm install -g @anthropic-ai/claude-code');
|
|
55
|
+
console.log(' - Codex CLI: npm install -g @openai/codex');
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
console.log(' Where to register forgen?');
|
|
59
|
+
console.log(' [1] Claude only');
|
|
60
|
+
console.log(' [2] Codex only');
|
|
61
|
+
console.log(' [3] Both');
|
|
62
|
+
console.log(' [q] Quit');
|
|
63
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
64
|
+
try {
|
|
65
|
+
const choice = await askChoice(rl, ' Choice: ', ['1', '2', '3', 'q']);
|
|
66
|
+
if (choice === 'q')
|
|
67
|
+
return null;
|
|
68
|
+
return choice === '1' ? 'claude' : choice === '2' ? 'codex' : 'both';
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
rl.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function runInstall(opts) {
|
|
75
|
+
const detection = detectAvailableHosts();
|
|
76
|
+
let target;
|
|
77
|
+
if (opts.target === 'claude' || opts.target === 'codex' || opts.target === 'both') {
|
|
78
|
+
target = opts.target;
|
|
79
|
+
}
|
|
80
|
+
else if (opts.target === undefined) {
|
|
81
|
+
const interactive = await chooseTargetInteractively(detection);
|
|
82
|
+
if (interactive === null)
|
|
83
|
+
return null;
|
|
84
|
+
target = interactive;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw new Error(`Unknown install target: ${opts.target}. Use claude|codex|both or omit for interactive.`);
|
|
88
|
+
}
|
|
89
|
+
const result = { target, detection };
|
|
90
|
+
const dryRun = opts.dryRun ?? false;
|
|
91
|
+
const registerMcp = opts.registerMcp ?? true;
|
|
92
|
+
if (target === 'claude' || target === 'both') {
|
|
93
|
+
result.claude = planClaudeInstall({ pkgRoot: opts.pkgRoot, dryRun, registerMcp });
|
|
94
|
+
}
|
|
95
|
+
if (target === 'codex' || target === 'both') {
|
|
96
|
+
result.codex = planCodexInstall({ pkgRoot: opts.pkgRoot, dryRun, registerMcp });
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/** CLI 출력 포맷터 — orchestrator 결과를 사용자에게 표시. */
|
|
101
|
+
export function renderResult(result, dryRun) {
|
|
102
|
+
const lines = [];
|
|
103
|
+
lines.push(`\n [forgen] Install ${dryRun ? '(dry-run)' : 'completed'} — target: ${result.target}`);
|
|
104
|
+
if (result.claude) {
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(' Claude:');
|
|
107
|
+
lines.push(` plugin cache: ${result.claude.pluginCachePath}`);
|
|
108
|
+
lines.push(` slash commands: ${result.claude.slashCommandsCount} → ${result.claude.slashCommandsPath}`);
|
|
109
|
+
lines.push(` settings.json hooks: ${result.claude.hooksInjected}`);
|
|
110
|
+
lines.push(` MCP: ${result.claude.mcpAlreadyPresent ? 'already present' : (result.claude.mcpRegistered ? 'registered' : 'skipped')}`);
|
|
111
|
+
}
|
|
112
|
+
if (result.codex) {
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push(' Codex:');
|
|
115
|
+
lines.push(` CODEX_HOME: ${result.codex.codexHome}`);
|
|
116
|
+
lines.push(` hooks.json: ${result.codex.hooksCount} forgen hooks (preserved user: ${result.codex.preservedUserHookCount})`);
|
|
117
|
+
lines.push(` MCP: ${result.codex.mcpAlreadyPresent ? 'already present' : (result.codex.mcpRegistered ? 'registered' : 'skipped')}`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('');
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
/** pkgRoot resolve from binary location (dist/cli.js → pkgRoot). */
|
|
123
|
+
export function resolvePkgRootFromBinary(metaUrl) {
|
|
124
|
+
const here = path.dirname(new URL(metaUrl).pathname);
|
|
125
|
+
return path.resolve(here, '..');
|
|
126
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* invoke-agent — feat/codex-support P3-4/P3-5
|
|
3
|
+
*
|
|
4
|
+
* forgen 의 sub-agent (assets/claude/agents/<name>.md) 를 host-aware 로 호출.
|
|
5
|
+
* Claude 의 Task tool 동치 — 별도 child process 에서 sub-agent 의 system prompt 를
|
|
6
|
+
* prefix 로 사용자 task 실행 후 결과 반환.
|
|
7
|
+
*
|
|
8
|
+
* Recursion guard: FORGEN_INVOKE_DEPTH env var 로 depth 추적, max 2 (sub-agent 가
|
|
9
|
+
* 또 sub-agent 호출 시도 → 차단).
|
|
10
|
+
*/
|
|
11
|
+
import { type ExecHostResult } from './exec-host.js';
|
|
12
|
+
export interface InvokeAgentOptions {
|
|
13
|
+
agentName: string;
|
|
14
|
+
task: string;
|
|
15
|
+
/** Child process timeout (ms). Default 60s. */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
/** Override host (default: profile.default_host). */
|
|
18
|
+
host?: 'claude' | 'codex';
|
|
19
|
+
}
|
|
20
|
+
export interface InvokeAgentResult {
|
|
21
|
+
agentName: string;
|
|
22
|
+
host: 'claude' | 'codex';
|
|
23
|
+
summary: string;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
usage: ExecHostResult['usage'];
|
|
26
|
+
}
|
|
27
|
+
export declare function invokeAgent(opts: InvokeAgentOptions): Promise<InvokeAgentResult>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* invoke-agent — feat/codex-support P3-4/P3-5
|
|
3
|
+
*
|
|
4
|
+
* forgen 의 sub-agent (assets/claude/agents/<name>.md) 를 host-aware 로 호출.
|
|
5
|
+
* Claude 의 Task tool 동치 — 별도 child process 에서 sub-agent 의 system prompt 를
|
|
6
|
+
* prefix 로 사용자 task 실행 후 결과 반환.
|
|
7
|
+
*
|
|
8
|
+
* Recursion guard: FORGEN_INVOKE_DEPTH env var 로 depth 추적, max 2 (sub-agent 가
|
|
9
|
+
* 또 sub-agent 호출 시도 → 차단).
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { execHost } from './exec-host.js';
|
|
15
|
+
const MAX_DEPTH = 2;
|
|
16
|
+
const MAX_CONCURRENT = 3;
|
|
17
|
+
let activeInvocations = 0;
|
|
18
|
+
function findAgentsRoot() {
|
|
19
|
+
// Phase 3 critic fix: 단순 디렉토리 매치 시 모노레포의 동명 디렉토리 위험.
|
|
20
|
+
// package.json 의 name === '@wooojin/forgen' 검증으로 *정확한 forgen pkg root* 확정.
|
|
21
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
23
|
+
const pkgJson = path.join(dir, 'package.json');
|
|
24
|
+
if (fs.existsSync(pkgJson)) {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'));
|
|
27
|
+
if (pkg.name === '@wooojin/forgen') {
|
|
28
|
+
const candidate = path.join(dir, 'assets', 'claude', 'agents');
|
|
29
|
+
if (fs.existsSync(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { /* fallthrough — 다음 walk-up */ }
|
|
34
|
+
}
|
|
35
|
+
const parent = path.dirname(dir);
|
|
36
|
+
if (parent === dir)
|
|
37
|
+
break;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
40
|
+
throw new Error('invoke-agent: forgen pkg root + assets/claude/agents/ not found');
|
|
41
|
+
}
|
|
42
|
+
function loadAgentDefinition(agentName) {
|
|
43
|
+
const safeName = agentName.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
44
|
+
if (safeName !== agentName || safeName.length === 0) {
|
|
45
|
+
throw new Error(`invoke-agent: invalid agent_name "${agentName}" — use only [a-zA-Z0-9_-]`);
|
|
46
|
+
}
|
|
47
|
+
const root = findAgentsRoot();
|
|
48
|
+
const filePath = path.join(root, `${safeName}.md`);
|
|
49
|
+
if (!fs.existsSync(filePath)) {
|
|
50
|
+
const available = fs.readdirSync(root)
|
|
51
|
+
.filter((f) => f.endsWith('.md'))
|
|
52
|
+
.map((f) => f.replace(/\.md$/, ''))
|
|
53
|
+
.sort();
|
|
54
|
+
throw new Error(`invoke-agent: agent "${agentName}" not found. Available: ${available.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
57
|
+
// Phase 3 critic fix: BOM + CRLF 정규화 (Windows / Notion 파일 호환)
|
|
58
|
+
const normalized = raw.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
59
|
+
const fmMatch = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
60
|
+
const description = fmMatch?.[1].match(/description:\s*(.+)/)?.[1].trim() ?? safeName;
|
|
61
|
+
const body = fmMatch?.[2]?.trim() ?? normalized;
|
|
62
|
+
return { systemPrompt: body, description };
|
|
63
|
+
}
|
|
64
|
+
function buildAgentPrompt(opts) {
|
|
65
|
+
return [
|
|
66
|
+
`You are the "${opts.agentName}" sub-agent. ${opts.description}`,
|
|
67
|
+
'',
|
|
68
|
+
'<system-prompt>',
|
|
69
|
+
opts.systemPrompt,
|
|
70
|
+
'</system-prompt>',
|
|
71
|
+
'',
|
|
72
|
+
'TASK:',
|
|
73
|
+
opts.task,
|
|
74
|
+
'',
|
|
75
|
+
'Respond with the deliverable — concise, focused on the task. No preamble.',
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
export async function invokeAgent(opts) {
|
|
79
|
+
// Phase 3 critic fix: depth 외에 fan-out 도 제한.
|
|
80
|
+
// depth 2 에서 N 개 sibling invoke 가 동시 시작되면 N² child spawn 가능 →
|
|
81
|
+
// 비용/timeout cascading. process-level concurrency limit MAX_CONCURRENT 로 제한.
|
|
82
|
+
const currentDepth = parseInt(process.env.FORGEN_INVOKE_DEPTH ?? '0', 10);
|
|
83
|
+
if (currentDepth >= MAX_DEPTH) {
|
|
84
|
+
throw new Error(`invoke-agent: max recursion depth ${MAX_DEPTH} exceeded (current=${currentDepth})`);
|
|
85
|
+
}
|
|
86
|
+
if (activeInvocations >= MAX_CONCURRENT) {
|
|
87
|
+
throw new Error(`invoke-agent: max concurrent invocations ${MAX_CONCURRENT} reached (active=${activeInvocations}). ` +
|
|
88
|
+
'Sibling sub-agents must run sequentially.');
|
|
89
|
+
}
|
|
90
|
+
const { systemPrompt, description } = loadAgentDefinition(opts.agentName);
|
|
91
|
+
const prompt = buildAgentPrompt({ agentName: opts.agentName, description, systemPrompt, task: opts.task });
|
|
92
|
+
const startedAt = Date.now();
|
|
93
|
+
activeInvocations += 1;
|
|
94
|
+
try {
|
|
95
|
+
// Phase 3 critic fix: default timeout 60s → 90s (codex sandbox startup +
|
|
96
|
+
// 인증 + LLM 응답까지 60s 부족할 수 있음. tail latency 안전마진).
|
|
97
|
+
const result = execHost({
|
|
98
|
+
prompt,
|
|
99
|
+
timeout: opts.timeoutMs ?? 90000,
|
|
100
|
+
host: opts.host,
|
|
101
|
+
env: { FORGEN_INVOKE_DEPTH: String(currentDepth + 1) },
|
|
102
|
+
});
|
|
103
|
+
const durationMs = Date.now() - startedAt;
|
|
104
|
+
return {
|
|
105
|
+
agentName: opts.agentName,
|
|
106
|
+
host: result.host,
|
|
107
|
+
summary: result.message,
|
|
108
|
+
durationMs,
|
|
109
|
+
usage: result.usage,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
activeInvocations -= 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BehavioralParityScenario harness — Multi-Host Core Design §10 우선순위 4
|
|
3
|
+
*
|
|
4
|
+
* "Claude 와 Codex 양쪽에서 같은 입력을 흘려보냈을 때 evidence 가 의미적으로 같다" 를
|
|
5
|
+
* 검증하는 골격. P4 단계에서는 *projection 사영 후 등가성* 만 검증한다 — 실 모델 호출은
|
|
6
|
+
* P6 (실 Codex CLI) 트랙.
|
|
7
|
+
*
|
|
8
|
+
* 본 harness 가 verify 하는 것:
|
|
9
|
+
* 1. 같은 forgen hook 입력에 대해 양쪽 host 의 raw 출력을 사영하면 의미 동치한 객체가 된다.
|
|
10
|
+
* 2. 사영 결과가 1원칙 (Claude reference) 의 행동 의도와 일치한다.
|
|
11
|
+
*
|
|
12
|
+
* verify 하지 않는 것 (P6 별도 트랙):
|
|
13
|
+
* - 실제 Codex 모델이 같은 prompt 에 같은 행동을 보이는지
|
|
14
|
+
* - 실제 Claude 모델과의 동작 동등성
|
|
15
|
+
*/
|
|
16
|
+
import type { HookEventInput, HookEventOutput } from '../core/types.js';
|
|
17
|
+
import type { HostId, TrustLayerIntent } from '../core/trust-layer-intent.js';
|
|
18
|
+
export interface BehavioralParityScenario {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
/** 검증하려는 Trust Layer 의도. */
|
|
21
|
+
readonly intent: TrustLayerIntent;
|
|
22
|
+
readonly description: string;
|
|
23
|
+
/** hook 입력 (HookEventInput 동치). */
|
|
24
|
+
readonly input: HookEventInput;
|
|
25
|
+
/**
|
|
26
|
+
* 각 host 가 *내보낼 것으로 가정* 하는 raw 출력. P4 단계에서는 spec §18 source schema
|
|
27
|
+
* 기반 직접 작성. P6 단계에서는 실 Codex CLI 출력으로 대체.
|
|
28
|
+
*/
|
|
29
|
+
readonly hostRaw: Record<HostId, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* 사영 후 의미 동치성을 검증할 키들.
|
|
32
|
+
* 예: ['continue', 'hookSpecificOutput.permissionDecision'].
|
|
33
|
+
* 각 key 는 . 으로 nested path 표현.
|
|
34
|
+
*/
|
|
35
|
+
readonly compareKeys: ReadonlyArray<string>;
|
|
36
|
+
}
|
|
37
|
+
export interface ParityCheckResult {
|
|
38
|
+
readonly scenarioId: string;
|
|
39
|
+
readonly intent: TrustLayerIntent;
|
|
40
|
+
readonly passed: boolean;
|
|
41
|
+
readonly diffs: ReadonlyArray<{
|
|
42
|
+
key: string;
|
|
43
|
+
claude: unknown;
|
|
44
|
+
codex: unknown;
|
|
45
|
+
}>;
|
|
46
|
+
/** 사영 결과 자체 (디버깅용). */
|
|
47
|
+
readonly projected: Readonly<Record<HostId, HookEventOutput>>;
|
|
48
|
+
}
|
|
49
|
+
export declare function runScenario(scenario: BehavioralParityScenario): ParityCheckResult;
|
|
50
|
+
/**
|
|
51
|
+
* P4 1차 시나리오 corpus — Trust Layer 7 의도 중 hook 출력으로 직접 관측 가능한 5종.
|
|
52
|
+
* (`forge-loop-state-inject` 는 inject-context 의 특수 케이스, `self-evidence-record` 는
|
|
53
|
+
* 파일 시스템 사이드이펙트라 본 corpus 가 아닌 별도 e2e 트랙.)
|
|
54
|
+
*
|
|
55
|
+
* P4 2차 추가 시나리오 (spec §10 우선순위 4 산출물, 2026-04-27):
|
|
56
|
+
* - forge-loop-m1-inject-stale : §17.1 fact 1 / M1 hook 1KB cap + stale tag
|
|
57
|
+
* - block-tool-use-pretool-ask : §9.0 block-tool-use row — Codex ask 값 동치
|
|
58
|
+
* - stop-hook-reentry-guard : §15 stop_hook_active=true 즉시 approve 경로
|
|
59
|
+
* - posttooluse-general-block-only : §9.0 secret-filter row partial — block decision 만 등가
|
|
60
|
+
* - suppress-output-equivalence : suppressOutput 동치성
|
|
61
|
+
*/
|
|
62
|
+
export declare const SCENARIO_CORPUS: ReadonlyArray<BehavioralParityScenario>;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BehavioralParityScenario harness — Multi-Host Core Design §10 우선순위 4
|
|
3
|
+
*
|
|
4
|
+
* "Claude 와 Codex 양쪽에서 같은 입력을 흘려보냈을 때 evidence 가 의미적으로 같다" 를
|
|
5
|
+
* 검증하는 골격. P4 단계에서는 *projection 사영 후 등가성* 만 검증한다 — 실 모델 호출은
|
|
6
|
+
* P6 (실 Codex CLI) 트랙.
|
|
7
|
+
*
|
|
8
|
+
* 본 harness 가 verify 하는 것:
|
|
9
|
+
* 1. 같은 forgen hook 입력에 대해 양쪽 host 의 raw 출력을 사영하면 의미 동치한 객체가 된다.
|
|
10
|
+
* 2. 사영 결과가 1원칙 (Claude reference) 의 행동 의도와 일치한다.
|
|
11
|
+
*
|
|
12
|
+
* verify 하지 않는 것 (P6 별도 트랙):
|
|
13
|
+
* - 실제 Codex 모델이 같은 prompt 에 같은 행동을 보이는지
|
|
14
|
+
* - 실제 Claude 모델과의 동작 동등성
|
|
15
|
+
*/
|
|
16
|
+
import { equal as deepEqual } from 'node:assert/strict';
|
|
17
|
+
import { getProjection } from './projection.js';
|
|
18
|
+
function pickPath(obj, dotted) {
|
|
19
|
+
const parts = dotted.split('.');
|
|
20
|
+
let cur = obj;
|
|
21
|
+
for (const p of parts) {
|
|
22
|
+
if (cur && typeof cur === 'object' && p in cur) {
|
|
23
|
+
cur = cur[p];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return cur;
|
|
30
|
+
}
|
|
31
|
+
function valuesSemanticEqual(a, b) {
|
|
32
|
+
try {
|
|
33
|
+
deepEqual(a, b);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function runScenario(scenario) {
|
|
41
|
+
const projected = {
|
|
42
|
+
claude: getProjection('claude')(scenario.hostRaw.claude, scenario.input),
|
|
43
|
+
codex: getProjection('codex')(scenario.hostRaw.codex, scenario.input),
|
|
44
|
+
};
|
|
45
|
+
const diffs = [];
|
|
46
|
+
for (const key of scenario.compareKeys) {
|
|
47
|
+
const cv = pickPath(projected.claude, key);
|
|
48
|
+
const xv = pickPath(projected.codex, key);
|
|
49
|
+
if (!valuesSemanticEqual(cv, xv)) {
|
|
50
|
+
diffs.push({ key, claude: cv, codex: xv });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
scenarioId: scenario.id,
|
|
55
|
+
intent: scenario.intent,
|
|
56
|
+
passed: diffs.length === 0,
|
|
57
|
+
diffs,
|
|
58
|
+
projected,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* P4 1차 시나리오 corpus — Trust Layer 7 의도 중 hook 출력으로 직접 관측 가능한 5종.
|
|
63
|
+
* (`forge-loop-state-inject` 는 inject-context 의 특수 케이스, `self-evidence-record` 는
|
|
64
|
+
* 파일 시스템 사이드이펙트라 본 corpus 가 아닌 별도 e2e 트랙.)
|
|
65
|
+
*
|
|
66
|
+
* P4 2차 추가 시나리오 (spec §10 우선순위 4 산출물, 2026-04-27):
|
|
67
|
+
* - forge-loop-m1-inject-stale : §17.1 fact 1 / M1 hook 1KB cap + stale tag
|
|
68
|
+
* - block-tool-use-pretool-ask : §9.0 block-tool-use row — Codex ask 값 동치
|
|
69
|
+
* - stop-hook-reentry-guard : §15 stop_hook_active=true 즉시 approve 경로
|
|
70
|
+
* - posttooluse-general-block-only : §9.0 secret-filter row partial — block decision 만 등가
|
|
71
|
+
* - suppress-output-equivalence : suppressOutput 동치성
|
|
72
|
+
*/
|
|
73
|
+
export const SCENARIO_CORPUS = [
|
|
74
|
+
{
|
|
75
|
+
id: 'block-completion-stop',
|
|
76
|
+
intent: 'block-completion',
|
|
77
|
+
description: 'Stop hook 이 block + reason 으로 자동 continuation 트리거',
|
|
78
|
+
input: { hookEventName: 'Stop', stop_hook_active: false },
|
|
79
|
+
hostRaw: {
|
|
80
|
+
claude: { decision: 'block', reason: 'tests not yet executed' },
|
|
81
|
+
codex: { decision: 'block', reason: 'tests not yet executed' },
|
|
82
|
+
},
|
|
83
|
+
compareKeys: [
|
|
84
|
+
'continue',
|
|
85
|
+
'hookSpecificOutput.permissionDecision',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'block-tool-use-pretool-deny',
|
|
90
|
+
intent: 'block-tool-use',
|
|
91
|
+
description: 'PreToolUse 가 permissionDecision:deny + reason 으로 도구 차단',
|
|
92
|
+
input: { hookEventName: 'PreToolUse', tool_name: 'Bash' },
|
|
93
|
+
hostRaw: {
|
|
94
|
+
claude: {
|
|
95
|
+
hookSpecificOutput: {
|
|
96
|
+
hookEventName: 'PreToolUse',
|
|
97
|
+
permissionDecision: 'deny',
|
|
98
|
+
permissionDecisionReason: 'rm -rf / matched',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
codex: {
|
|
102
|
+
hookSpecificOutput: {
|
|
103
|
+
hookEventName: 'PreToolUse',
|
|
104
|
+
permissionDecision: 'deny',
|
|
105
|
+
permissionDecisionReason: 'rm -rf / matched',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
compareKeys: [
|
|
110
|
+
'hookSpecificOutput.permissionDecision',
|
|
111
|
+
'hookSpecificOutput.permissionDecisionReason',
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'inject-context-session-start',
|
|
116
|
+
intent: 'inject-context',
|
|
117
|
+
description: 'SessionStart 가 additionalContext 로 forge-loop state 주입',
|
|
118
|
+
input: { hookEventName: 'SessionStart' },
|
|
119
|
+
hostRaw: {
|
|
120
|
+
claude: {
|
|
121
|
+
hookSpecificOutput: {
|
|
122
|
+
hookEventName: 'SessionStart',
|
|
123
|
+
additionalContext: '<forge-loop-state>...</forge-loop-state>',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
codex: {
|
|
127
|
+
hookSpecificOutput: {
|
|
128
|
+
hookEventName: 'SessionStart',
|
|
129
|
+
additionalContext: '<forge-loop-state>...</forge-loop-state>',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
compareKeys: [
|
|
134
|
+
'continue',
|
|
135
|
+
'hookSpecificOutput.additionalContext',
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'observe-only-non-allowlist',
|
|
140
|
+
intent: 'observe-only',
|
|
141
|
+
description: 'ALLOW-LIST 외 hook 이 deny 시도 시 approve 강등',
|
|
142
|
+
input: { hookEventName: 'PreToolUse' },
|
|
143
|
+
hostRaw: {
|
|
144
|
+
claude: { continue: true },
|
|
145
|
+
codex: { continue: true },
|
|
146
|
+
},
|
|
147
|
+
compareKeys: ['continue'],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'secret-filter-pretooluse-block',
|
|
151
|
+
intent: 'secret-filter',
|
|
152
|
+
description: 'API 키 노출 차단 — PreToolUse 가드 (양쪽 동일 경로)',
|
|
153
|
+
input: { hookEventName: 'PreToolUse', tool_name: 'Bash' },
|
|
154
|
+
hostRaw: {
|
|
155
|
+
claude: {
|
|
156
|
+
hookSpecificOutput: {
|
|
157
|
+
hookEventName: 'PreToolUse',
|
|
158
|
+
permissionDecision: 'deny',
|
|
159
|
+
permissionDecisionReason: 'API_KEY=... matched',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
codex: {
|
|
163
|
+
hookSpecificOutput: {
|
|
164
|
+
hookEventName: 'PreToolUse',
|
|
165
|
+
permissionDecision: 'deny',
|
|
166
|
+
permissionDecisionReason: 'API_KEY=... matched',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
compareKeys: [
|
|
171
|
+
'hookSpecificOutput.permissionDecision',
|
|
172
|
+
'hookSpecificOutput.permissionDecisionReason',
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
// ── P4 2차 추가 시나리오 ────────────────────────────────────────────────────
|
|
176
|
+
{
|
|
177
|
+
id: 'forge-loop-m1-inject-stale',
|
|
178
|
+
intent: 'forge-loop-state-inject',
|
|
179
|
+
description: 'SessionStart 가 stale="true" 태그 포함 forge-loop-state 를 1KB cap 내에서 inject (spec §17.1 fact 1 / M1 hook)',
|
|
180
|
+
input: { hookEventName: 'SessionStart' },
|
|
181
|
+
hostRaw: {
|
|
182
|
+
claude: {
|
|
183
|
+
hookSpecificOutput: {
|
|
184
|
+
hookEventName: 'SessionStart',
|
|
185
|
+
additionalContext: '<forge-loop-state stale="true">{"phase":"M1","lastCompletedAt":"2026-04-26T22:00:00Z"}</forge-loop-state>',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
codex: {
|
|
189
|
+
hookSpecificOutput: {
|
|
190
|
+
hookEventName: 'SessionStart',
|
|
191
|
+
additionalContext: '<forge-loop-state stale="true">{"phase":"M1","lastCompletedAt":"2026-04-26T22:00:00Z"}</forge-loop-state>',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
compareKeys: [
|
|
196
|
+
'continue',
|
|
197
|
+
'hookSpecificOutput.additionalContext',
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'block-tool-use-pretool-ask',
|
|
202
|
+
intent: 'block-tool-use',
|
|
203
|
+
description: 'Codex PreToolUse permissionDecision:"ask" 가 Claude ask 값과 의미 동치 — forgen denyOrObserve 의 ask 의도 (spec §9.0 block-tool-use row 메모)',
|
|
204
|
+
input: { hookEventName: 'PreToolUse', tool_name: 'Bash' },
|
|
205
|
+
hostRaw: {
|
|
206
|
+
claude: {
|
|
207
|
+
hookSpecificOutput: {
|
|
208
|
+
hookEventName: 'PreToolUse',
|
|
209
|
+
permissionDecision: 'ask',
|
|
210
|
+
permissionDecisionReason: 'requires human confirmation',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
codex: {
|
|
214
|
+
hookSpecificOutput: {
|
|
215
|
+
hookEventName: 'PreToolUse',
|
|
216
|
+
permissionDecision: 'ask',
|
|
217
|
+
permissionDecisionReason: 'requires human confirmation',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
compareKeys: [
|
|
222
|
+
'hookSpecificOutput.permissionDecision',
|
|
223
|
+
'hookSpecificOutput.permissionDecisionReason',
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'stop-hook-reentry-guard',
|
|
228
|
+
intent: 'block-completion',
|
|
229
|
+
description: 'stop_hook_active=true 시 Stop hook 이 즉시 approve(continue:true) 로 빠지는 재진입 가드 (spec §15)',
|
|
230
|
+
input: { hookEventName: 'Stop', stop_hook_active: true },
|
|
231
|
+
hostRaw: {
|
|
232
|
+
claude: { continue: true },
|
|
233
|
+
codex: { continue: true },
|
|
234
|
+
},
|
|
235
|
+
compareKeys: ['continue'],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'posttooluse-general-block-only',
|
|
239
|
+
intent: 'secret-filter',
|
|
240
|
+
description: 'PostToolUse 일반 tool — Codex 는 redact 미보장이므로 block decision 만 양쪽 등가. updatedMCPToolOutput 없음 (spec §9.0 secret-filter row partial / §18.2 fact 4)',
|
|
241
|
+
input: { hookEventName: 'PostToolUse', tool_name: 'Bash' },
|
|
242
|
+
hostRaw: {
|
|
243
|
+
claude: {
|
|
244
|
+
hookSpecificOutput: {
|
|
245
|
+
hookEventName: 'PostToolUse',
|
|
246
|
+
permissionDecision: 'deny',
|
|
247
|
+
permissionDecisionReason: 'SECRET=... detected in tool output',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
codex: {
|
|
251
|
+
hookSpecificOutput: {
|
|
252
|
+
hookEventName: 'PostToolUse',
|
|
253
|
+
permissionDecision: 'deny',
|
|
254
|
+
permissionDecisionReason: 'SECRET=... detected in tool output',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
compareKeys: [
|
|
259
|
+
'hookSpecificOutput.permissionDecision',
|
|
260
|
+
'hookSpecificOutput.permissionDecisionReason',
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'suppress-output-equivalence',
|
|
265
|
+
intent: 'observe-only',
|
|
266
|
+
description: 'PostToolUse suppressOutput=true 가 양쪽 host 에서 동치로 사영됨',
|
|
267
|
+
input: { hookEventName: 'PostToolUse', tool_name: 'Read' },
|
|
268
|
+
hostRaw: {
|
|
269
|
+
claude: {
|
|
270
|
+
continue: true,
|
|
271
|
+
suppressOutput: true,
|
|
272
|
+
},
|
|
273
|
+
codex: {
|
|
274
|
+
continue: true,
|
|
275
|
+
suppressOutput: true,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
compareKeys: [
|
|
279
|
+
'continue',
|
|
280
|
+
'suppressOutput',
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectToClaudeEvent — Multi-Host Core Design §5.2 / §10 우선순위 2
|
|
3
|
+
*
|
|
4
|
+
* Codex (또는 미래의 다른 host) 의 hook 출력을 Claude Hook schema 로 사영하는
|
|
5
|
+
* 정식 계약. spec §17.4 / §18.4 에서 검증되었듯 schema-level 에서 거의 identity 이므로
|
|
6
|
+
* 본 함수는 *형식 정규화* 만 책임진다.
|
|
7
|
+
*
|
|
8
|
+
* - 입력: host-native 출력(JSON object, plaintext, exit-code 등은 별도 layer 에서 처리)
|
|
9
|
+
* - 출력: Claude HookEventOutput 동치 — `continue`, `hookSpecificOutput.permissionDecision`, etc.
|
|
10
|
+
* - 실패 정책: parse 실패 / 알 수 없는 형식 → fail-open (`{ continue: true }`)
|
|
11
|
+
*
|
|
12
|
+
* 본 모듈은 host 측 표면을 *모르고*, 받은 raw 의 형태만으로 동작한다 (1원칙: core 는 Claude
|
|
13
|
+
* semantics 알아도 됨, Codex 표면 모름). 즉 Codex CLI 의 stdout 을 받아 코어가 학습 가능한
|
|
14
|
+
* Claude 형 객체로 변환만 한다.
|
|
15
|
+
*/
|
|
16
|
+
import type { HookEventInput, HookEventOutput } from '../core/types.js';
|
|
17
|
+
import type { HostId } from '../core/trust-layer-intent.js';
|
|
18
|
+
export type ProjectToClaudeEvent = (raw: unknown, input: HookEventInput) => HookEventOutput;
|
|
19
|
+
/**
|
|
20
|
+
* Codex 출력 → Claude HookEventOutput 정식 사영.
|
|
21
|
+
*
|
|
22
|
+
* spec §18.2 fact #3 에 따라 PreToolUse 의 *이중* decision 필드 중 어댑터는
|
|
23
|
+
* `hookSpecificOutput.permissionDecision` 을 우선한다. 본 함수가 그 규약을 강제.
|
|
24
|
+
*/
|
|
25
|
+
export declare const projectCodexToClaude: ProjectToClaudeEvent;
|
|
26
|
+
/**
|
|
27
|
+
* Claude 어댑터의 사영. 1원칙(Claude reference) + spec §18.4 (Codex hooks.json schema 동일성)
|
|
28
|
+
* 에 따라 본 함수는 `projectCodexToClaude` 와 *같은 normalize 로직* 을 공유한다.
|
|
29
|
+
* 둘 다 같은 canonical Claude HookEventOutput 형식을 만든다.
|
|
30
|
+
*
|
|
31
|
+
* (왜 두 함수를 별도 export 하는가: 향후 schema 가 다른 host 가 추가될 때 본 binding 만
|
|
32
|
+
* 교체하면 되도록 — `getProjection(host)` 가 단일 진입점.)
|
|
33
|
+
*/
|
|
34
|
+
export declare const projectClaudeToClaude: ProjectToClaudeEvent;
|
|
35
|
+
export declare function getProjection(host: HostId): ProjectToClaudeEvent;
|