@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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude HostCapabilities — Multi-Host Core Design §9.0
|
|
3
|
+
*
|
|
4
|
+
* Claude 는 reference host. 모든 TrustLayerIntent 가 supported (identity binding).
|
|
5
|
+
* 본 선언은 *spec 정의 그 자체* 의 코드 표현 — 변경 시 spec §9.0 도 같이 갱신해야 한다.
|
|
6
|
+
*/
|
|
7
|
+
export const claudeCapabilities = {
|
|
8
|
+
hostId: 'claude',
|
|
9
|
+
intents: {
|
|
10
|
+
'block-completion': {
|
|
11
|
+
status: 'supported',
|
|
12
|
+
expression: 'Stop hook + `decision:"block"` + `reason`',
|
|
13
|
+
source: 'forgen v0.4.0 stop-guard, src/hooks/stop-guard.ts',
|
|
14
|
+
},
|
|
15
|
+
'block-tool-use': {
|
|
16
|
+
status: 'supported',
|
|
17
|
+
expression: 'PreToolUse + `hookSpecificOutput.permissionDecision:"deny"` + `permissionDecisionReason`',
|
|
18
|
+
source: 'forgen v0.4.0 pre-tool-use, src/hooks/pre-tool-use.ts',
|
|
19
|
+
},
|
|
20
|
+
'inject-context': {
|
|
21
|
+
status: 'supported',
|
|
22
|
+
expression: 'SessionStart/UserPromptSubmit + `hookSpecificOutput.additionalContext`',
|
|
23
|
+
source: 'forgen v0.4.2 M1, src/hooks/session-recovery.ts + forge-loop-progress.ts',
|
|
24
|
+
},
|
|
25
|
+
'observe-only': {
|
|
26
|
+
status: 'supported',
|
|
27
|
+
expression: 'non-allowlist hook approve + observer log (denyOrObserve)',
|
|
28
|
+
source: 'forgen v0.4.2 P3\', src/hooks/shared/blocking-allowlist.ts + hook-response.ts',
|
|
29
|
+
},
|
|
30
|
+
'secret-filter': {
|
|
31
|
+
status: 'supported',
|
|
32
|
+
expression: 'PreToolUse 가드 + (선택) PostToolUse 차단/redact',
|
|
33
|
+
source: 'forgen v0.4.0 secret-filter, src/hooks/secret-filter.ts',
|
|
34
|
+
},
|
|
35
|
+
'forge-loop-state-inject': {
|
|
36
|
+
status: 'supported',
|
|
37
|
+
expression: 'SessionStart/UserPromptSubmit + `<forge-loop-state>` ≤1KB additionalContext',
|
|
38
|
+
source: 'forgen v0.4.2 M1, src/hooks/shared/forge-loop-state.ts',
|
|
39
|
+
},
|
|
40
|
+
'self-evidence-record': {
|
|
41
|
+
status: 'supported',
|
|
42
|
+
expression: 'hook 결과 → ~/.forgen/state/*.json (host 무관)',
|
|
43
|
+
source: 'forgen v0.4.2, ~/.forgen/state/e2e-result.json 외',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex HostCapabilities — Multi-Host Core Design §9.0 + §18 (source-level verified)
|
|
3
|
+
*
|
|
4
|
+
* Codex 는 1원칙 (Claude reference) 의 등가 확장 host. schema-level 에서 7/7 supported.
|
|
5
|
+
* 단, secret-filter 는 PostToolUse `hookSpecificOutput.updatedMCPToolOutput` 이 MCP tool 한정이므로
|
|
6
|
+
* partial. 일반 shell/edit tool 의 결과 redact 는 미보장 — PreToolUse 가드 유지로 mitigation.
|
|
7
|
+
*
|
|
8
|
+
* source-of-truth: codex-rs/hooks/schema/generated/* (Apache-2.0). spec §17/§18 이 박제한 검증 결과.
|
|
9
|
+
*/
|
|
10
|
+
import type { HostCapabilities } from '../core/trust-layer-intent.js';
|
|
11
|
+
export declare const codexCapabilities: HostCapabilities;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex HostCapabilities — Multi-Host Core Design §9.0 + §18 (source-level verified)
|
|
3
|
+
*
|
|
4
|
+
* Codex 는 1원칙 (Claude reference) 의 등가 확장 host. schema-level 에서 7/7 supported.
|
|
5
|
+
* 단, secret-filter 는 PostToolUse `hookSpecificOutput.updatedMCPToolOutput` 이 MCP tool 한정이므로
|
|
6
|
+
* partial. 일반 shell/edit tool 의 결과 redact 는 미보장 — PreToolUse 가드 유지로 mitigation.
|
|
7
|
+
*
|
|
8
|
+
* source-of-truth: codex-rs/hooks/schema/generated/* (Apache-2.0). spec §17/§18 이 박제한 검증 결과.
|
|
9
|
+
*/
|
|
10
|
+
export const codexCapabilities = {
|
|
11
|
+
hostId: 'codex',
|
|
12
|
+
intents: {
|
|
13
|
+
'block-completion': {
|
|
14
|
+
status: 'supported',
|
|
15
|
+
expression: 'Stop + `decision:"block"` + `reason` (Codex 가 reason 을 다음 turn prompt 로 자동 주입)',
|
|
16
|
+
source: 'codex-rs/hooks/schema/generated/stop.command.output.schema.json — description 에 "Claude requires `reason` when `decision` is `block`" 명시',
|
|
17
|
+
},
|
|
18
|
+
'block-tool-use': {
|
|
19
|
+
status: 'supported',
|
|
20
|
+
expression: 'PreToolUse + `hookSpecificOutput.permissionDecision:"deny"` + `permissionDecisionReason`',
|
|
21
|
+
source: 'codex-rs/hooks/schema/generated/pre-tool-use.command.output.schema.json — PreToolUsePermissionDecisionWire enum ["allow","deny","ask"]',
|
|
22
|
+
},
|
|
23
|
+
'inject-context': {
|
|
24
|
+
status: 'supported',
|
|
25
|
+
expression: 'SessionStart/UserPromptSubmit + `hookSpecificOutput.additionalContext`',
|
|
26
|
+
source: 'codex-rs/hooks/schema/generated/{session-start,user-prompt-submit}.command.output.schema.json — additionalContext: string',
|
|
27
|
+
},
|
|
28
|
+
'observe-only': {
|
|
29
|
+
status: 'supported',
|
|
30
|
+
expression: 'non-allowlist hook approve + observer log (denyOrObserve 그대로)',
|
|
31
|
+
source: 'forgen denyOrObserve 가 stdout JSON 만 다루므로 host 무관 — spec §17.2 확인',
|
|
32
|
+
},
|
|
33
|
+
'secret-filter': {
|
|
34
|
+
status: 'partial',
|
|
35
|
+
expression: 'MCP tool 한정: PostToolUse + `hookSpecificOutput.updatedMCPToolOutput`. 일반 shell/edit tool 결과 redact 계약 부재.',
|
|
36
|
+
mitigation: '1차는 PreToolUse 단계의 secret-filter 가드 유지 (Claude 와 동일 경로). 일반 tool 결과 redact 는 향후 PostToolUse 도입 시 MCP tool 에 한해 강화.',
|
|
37
|
+
source: 'codex-rs/hooks/schema/generated/post-tool-use.command.output.schema.json — updatedMCPToolOutput 만 정의',
|
|
38
|
+
},
|
|
39
|
+
'forge-loop-state-inject': {
|
|
40
|
+
status: 'supported',
|
|
41
|
+
expression: 'SessionStart/UserPromptSubmit + `<forge-loop-state>` ≤1KB additionalContext',
|
|
42
|
+
source: 'spec §9.0 row 6 — schema 가 Claude 와 동치하므로 1KB cap 정책 그대로 적용',
|
|
43
|
+
},
|
|
44
|
+
'self-evidence-record': {
|
|
45
|
+
status: 'supported',
|
|
46
|
+
expression: 'hook 결과 → ~/.forgen/state/*.json (host 무관). evidence 에 host:"codex" 태그 추가만 필요.',
|
|
47
|
+
source: 'spec §4.2 host-tagged evidence',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Capabilities Registry — Multi-Host Core Design §10 우선순위 1
|
|
3
|
+
*
|
|
4
|
+
* 등록된 모든 host 의 HostCapabilities 를 모듈 로드 시점에 검증한다.
|
|
5
|
+
* 새 TrustLayerIntent 추가 시 두 host 어댑터가 모두 선언을 추가하지 않으면 컴파일 fail.
|
|
6
|
+
* (TypeScript `Record<TrustLayerIntent, _>` 타입 + 이 모듈의 runtime assert 이중 가드.)
|
|
7
|
+
*/
|
|
8
|
+
import { type HostCapabilities, type HostId, type TrustLayerIntent } from '../core/trust-layer-intent.js';
|
|
9
|
+
export declare function getHostCapabilities(host: HostId): HostCapabilities;
|
|
10
|
+
export declare function listRegisteredHosts(): readonly HostId[];
|
|
11
|
+
export declare function intentSupported(host: HostId, intent: TrustLayerIntent): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Capabilities Registry — Multi-Host Core Design §10 우선순위 1
|
|
3
|
+
*
|
|
4
|
+
* 등록된 모든 host 의 HostCapabilities 를 모듈 로드 시점에 검증한다.
|
|
5
|
+
* 새 TrustLayerIntent 추가 시 두 host 어댑터가 모두 선언을 추가하지 않으면 컴파일 fail.
|
|
6
|
+
* (TypeScript `Record<TrustLayerIntent, _>` 타입 + 이 모듈의 runtime assert 이중 가드.)
|
|
7
|
+
*/
|
|
8
|
+
import { assertCapabilitiesComplete, } from '../core/trust-layer-intent.js';
|
|
9
|
+
import { claudeCapabilities } from './capabilities-claude.js';
|
|
10
|
+
import { codexCapabilities } from './capabilities-codex.js';
|
|
11
|
+
const REGISTRY = new Map([
|
|
12
|
+
[claudeCapabilities.hostId, claudeCapabilities],
|
|
13
|
+
[codexCapabilities.hostId, codexCapabilities],
|
|
14
|
+
]);
|
|
15
|
+
// 모듈 로드 시점 자기 검증 — 하나라도 미선언이면 즉시 throw.
|
|
16
|
+
for (const caps of REGISTRY.values()) {
|
|
17
|
+
assertCapabilitiesComplete(caps);
|
|
18
|
+
}
|
|
19
|
+
export function getHostCapabilities(host) {
|
|
20
|
+
const caps = REGISTRY.get(host);
|
|
21
|
+
if (!caps)
|
|
22
|
+
throw new Error(`Unknown host: ${host}`);
|
|
23
|
+
return caps;
|
|
24
|
+
}
|
|
25
|
+
export function listRegisteredHosts() {
|
|
26
|
+
return Array.from(REGISTRY.keys());
|
|
27
|
+
}
|
|
28
|
+
export function intentSupported(host, intent) {
|
|
29
|
+
return getHostCapabilities(host).intents[intent].status === 'supported';
|
|
30
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Codex 훅 어댑터
|
|
3
|
+
* Codex 훅 어댑터 — Multi-Host Core Design §10 우선순위 2 (승격)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 본 binary 는 codex 런타임에서 실행되는 훅 스크립트 출력을 Claude Hook schema 로
|
|
6
|
+
* 사영(projection)한다. 사영 로직은 정식 계약 `ProjectToClaudeEvent` (src/host/projection.ts)
|
|
7
|
+
* 에서 제공하며, 본 파일은 그 계약의 *binary 진입점* 역할만 수행한다.
|
|
8
|
+
*
|
|
9
|
+
* - 입력: 사용자 hook 스크립트(stdin JSON, argv 의 첫 인자가 delegate path)
|
|
10
|
+
* - 출력: Claude HookEventOutput 동치 JSON (stdout 1줄)
|
|
11
|
+
* - 실패 정책: parse/실행 실패 → fail-open (`{ continue: true }`)
|
|
9
12
|
*/
|
|
10
13
|
export {};
|
|
@@ -1,49 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Codex 훅 어댑터
|
|
3
|
+
* Codex 훅 어댑터 — Multi-Host Core Design §10 우선순위 2 (승격)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 본 binary 는 codex 런타임에서 실행되는 훅 스크립트 출력을 Claude Hook schema 로
|
|
6
|
+
* 사영(projection)한다. 사영 로직은 정식 계약 `ProjectToClaudeEvent` (src/host/projection.ts)
|
|
7
|
+
* 에서 제공하며, 본 파일은 그 계약의 *binary 진입점* 역할만 수행한다.
|
|
8
|
+
*
|
|
9
|
+
* - 입력: 사용자 hook 스크립트(stdin JSON, argv 의 첫 인자가 delegate path)
|
|
10
|
+
* - 출력: Claude HookEventOutput 동치 JSON (stdout 1줄)
|
|
11
|
+
* - 실패 정책: parse/실행 실패 → fail-open (`{ continue: true }`)
|
|
9
12
|
*/
|
|
10
13
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
|
|
12
|
-
if (typeof raw === 'boolean') {
|
|
13
|
-
return { continueFlag: raw };
|
|
14
|
-
}
|
|
15
|
-
if (typeof raw === 'string') {
|
|
16
|
-
const normalized = raw.toLowerCase();
|
|
17
|
-
if (normalized === 'continue')
|
|
18
|
-
return { continueFlag: true };
|
|
19
|
-
if (normalized === 'stop' || normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
|
|
20
|
-
return { continueFlag: false, permissionDecision: normalized };
|
|
21
|
-
}
|
|
22
|
-
return { continueFlag: true };
|
|
23
|
-
}
|
|
24
|
-
if (typeof raw !== 'object' || raw === null)
|
|
25
|
-
return { continueFlag: true };
|
|
26
|
-
const value = raw.decision;
|
|
27
|
-
if (typeof value === 'string') {
|
|
28
|
-
const normalized = value.toLowerCase();
|
|
29
|
-
if (normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
|
|
30
|
-
return { continueFlag: false, permissionDecision: normalized };
|
|
31
|
-
}
|
|
32
|
-
if (normalized === 'ask' || normalized === 'prompt' || normalized === 'confirm') {
|
|
33
|
-
return { continueFlag: true, permissionDecision: normalized };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (typeof raw.approved === 'boolean') {
|
|
37
|
-
const approved = raw.approved;
|
|
38
|
-
return approved
|
|
39
|
-
? { continueFlag: true, permissionDecision: raw.decision || 'approve' }
|
|
40
|
-
: { continueFlag: false, permissionDecision: 'deny' };
|
|
41
|
-
}
|
|
42
|
-
if (typeof raw.continue === 'boolean') {
|
|
43
|
-
return { continueFlag: raw.continue };
|
|
44
|
-
}
|
|
45
|
-
return { continueFlag: true };
|
|
46
|
-
}
|
|
14
|
+
import { projectCodexToClaude } from './projection.js';
|
|
47
15
|
function lastJSONObjectFromText(raw) {
|
|
48
16
|
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
49
17
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
@@ -61,46 +29,6 @@ function lastJSONObjectFromText(raw) {
|
|
|
61
29
|
return null;
|
|
62
30
|
}
|
|
63
31
|
}
|
|
64
|
-
function normalizeOutput(raw, input) {
|
|
65
|
-
const result = { continue: true };
|
|
66
|
-
const decision = parseDecision(raw);
|
|
67
|
-
result.continue = decision.continueFlag;
|
|
68
|
-
if (typeof raw === 'object' && raw !== null) {
|
|
69
|
-
const payload = raw;
|
|
70
|
-
if (typeof payload.continue === 'boolean')
|
|
71
|
-
result.continue = payload.continue;
|
|
72
|
-
if (typeof payload.systemMessage === 'string')
|
|
73
|
-
result.systemMessage = payload.systemMessage;
|
|
74
|
-
if (typeof payload.suppressOutput === 'boolean')
|
|
75
|
-
result.suppressOutput = payload.suppressOutput;
|
|
76
|
-
if (typeof payload.hookSpecificOutput === 'object' && payload.hookSpecificOutput !== null) {
|
|
77
|
-
result.hookSpecificOutput = { ...payload.hookSpecificOutput };
|
|
78
|
-
}
|
|
79
|
-
if (typeof payload.decision === 'string') {
|
|
80
|
-
result.hookSpecificOutput = {
|
|
81
|
-
...(result.hookSpecificOutput ?? {}),
|
|
82
|
-
permissionDecision: payload.decision,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const eventName = result.hookSpecificOutput?.hookEventName ?? input.hookEventName ?? input.event;
|
|
87
|
-
if (eventName) {
|
|
88
|
-
result.hookSpecificOutput = {
|
|
89
|
-
hookEventName: eventName,
|
|
90
|
-
...(result.hookSpecificOutput ?? {}),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (!result.continue && !result.hookSpecificOutput?.permissionDecision) {
|
|
94
|
-
if (decision.permissionDecision)
|
|
95
|
-
result.hookSpecificOutput = {
|
|
96
|
-
...(result.hookSpecificOutput ?? {}),
|
|
97
|
-
permissionDecision: decision.permissionDecision,
|
|
98
|
-
};
|
|
99
|
-
else
|
|
100
|
-
result.hookSpecificOutput = { ...(result.hookSpecificOutput ?? {}), permissionDecision: 'deny' };
|
|
101
|
-
}
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
32
|
async function main() {
|
|
105
33
|
const [delegatePath, ...restArgs] = process.argv.slice(2);
|
|
106
34
|
if (!delegatePath) {
|
|
@@ -142,7 +70,7 @@ async function main() {
|
|
|
142
70
|
console.log(JSON.stringify({ continue: true }));
|
|
143
71
|
return;
|
|
144
72
|
}
|
|
145
|
-
const output =
|
|
73
|
+
const output = projectCodexToClaude(parsed, input);
|
|
146
74
|
console.log(JSON.stringify(output));
|
|
147
75
|
}
|
|
148
76
|
catch {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex exec --json 출력 파서 — feat/codex-support Phase 2 (P2-1)
|
|
3
|
+
*
|
|
4
|
+
* codex exec --json 의 stdout 은 JSONL — 한 줄에 하나씩 이벤트.
|
|
5
|
+
* 본 파서는 agent_message 만 추출하여 문자열로 반환.
|
|
6
|
+
*
|
|
7
|
+
* 출력 형식 (실측 2026-04-27, Codex 0.125.0):
|
|
8
|
+
* {"type":"thread.started","thread_id":"..."}
|
|
9
|
+
* {"type":"turn.started"}
|
|
10
|
+
* {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"..."}}
|
|
11
|
+
* {"type":"turn.completed","usage":{...}}
|
|
12
|
+
*
|
|
13
|
+
* spec §10 P2-1 산출물 — Phase 2 의 compound-extractor 가 host-aware 분기 시 사용.
|
|
14
|
+
*/
|
|
15
|
+
export interface CodexUsage {
|
|
16
|
+
input_tokens?: number;
|
|
17
|
+
cached_input_tokens?: number;
|
|
18
|
+
output_tokens?: number;
|
|
19
|
+
reasoning_output_tokens?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CodexExecResult {
|
|
22
|
+
/** 모든 agent_message text 를 join. 보통 1개. */
|
|
23
|
+
readonly message: string;
|
|
24
|
+
/** 모든 agent_message segment (디버깅/multi-turn 용). */
|
|
25
|
+
readonly segments: ReadonlyArray<string>;
|
|
26
|
+
/** turn.completed 의 usage (없으면 null). */
|
|
27
|
+
readonly usage: CodexUsage | null;
|
|
28
|
+
/** thread.started 의 thread_id. */
|
|
29
|
+
readonly threadId: string | null;
|
|
30
|
+
/** parse 실패한 line 수 (의미 있는 신호 — 0 이 아니면 형식 변경 신호). */
|
|
31
|
+
readonly parseFailures: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* codex exec --json 의 stdout 을 받아 agent message + 메타 추출.
|
|
35
|
+
* stderr 의 hook 발화 noise 는 *별도* — 본 함수는 stdout 만 처리.
|
|
36
|
+
*
|
|
37
|
+
* fail-open: parse 실패 line 은 무시하되 카운터로 보고.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseCodexJsonlOutput(stdout: string): CodexExecResult;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex exec --json 출력 파서 — feat/codex-support Phase 2 (P2-1)
|
|
3
|
+
*
|
|
4
|
+
* codex exec --json 의 stdout 은 JSONL — 한 줄에 하나씩 이벤트.
|
|
5
|
+
* 본 파서는 agent_message 만 추출하여 문자열로 반환.
|
|
6
|
+
*
|
|
7
|
+
* 출력 형식 (실측 2026-04-27, Codex 0.125.0):
|
|
8
|
+
* {"type":"thread.started","thread_id":"..."}
|
|
9
|
+
* {"type":"turn.started"}
|
|
10
|
+
* {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"..."}}
|
|
11
|
+
* {"type":"turn.completed","usage":{...}}
|
|
12
|
+
*
|
|
13
|
+
* spec §10 P2-1 산출물 — Phase 2 의 compound-extractor 가 host-aware 분기 시 사용.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* codex exec --json 의 stdout 을 받아 agent message + 메타 추출.
|
|
17
|
+
* stderr 의 hook 발화 noise 는 *별도* — 본 함수는 stdout 만 처리.
|
|
18
|
+
*
|
|
19
|
+
* fail-open: parse 실패 line 은 무시하되 카운터로 보고.
|
|
20
|
+
*/
|
|
21
|
+
export function parseCodexJsonlOutput(stdout) {
|
|
22
|
+
const segments = [];
|
|
23
|
+
let usage = null;
|
|
24
|
+
let threadId = null;
|
|
25
|
+
let parseFailures = 0;
|
|
26
|
+
for (const line of stdout.split('\n')) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
continue;
|
|
30
|
+
if (!trimmed.startsWith('{'))
|
|
31
|
+
continue; // ANSI / status line skip
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(trimmed);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
parseFailures += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
41
|
+
continue;
|
|
42
|
+
const event = parsed;
|
|
43
|
+
const type = event.type;
|
|
44
|
+
if (type === 'thread.started') {
|
|
45
|
+
const tid = event.thread_id;
|
|
46
|
+
if (typeof tid === 'string')
|
|
47
|
+
threadId = tid;
|
|
48
|
+
}
|
|
49
|
+
else if (type === 'item.completed') {
|
|
50
|
+
const item = event.item;
|
|
51
|
+
if (item?.type === 'agent_message') {
|
|
52
|
+
// Phase 2 critic fix: text 가 string 아니면 schema drift 신호 → parseFailures 증가.
|
|
53
|
+
// (Codex 가 향후 array/object content 형식 도입 시 silent miss 방지)
|
|
54
|
+
if (typeof item.text === 'string') {
|
|
55
|
+
segments.push(item.text);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
parseFailures += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (type === 'turn.completed') {
|
|
63
|
+
const u = event.usage;
|
|
64
|
+
if (u && typeof u === 'object')
|
|
65
|
+
usage = u;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
message: segments.join('\n'),
|
|
70
|
+
segments,
|
|
71
|
+
usage,
|
|
72
|
+
threadId,
|
|
73
|
+
parseFailures,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-aware exec — feat/codex-support Phase 2 (P2-2/P2-3 공통)
|
|
3
|
+
*
|
|
4
|
+
* compound-extractor + auto-compound-runner 가 *어느 host CLI 로 LLM 호출* 할지
|
|
5
|
+
* 결정. profile.default_host 우선 + override 가능.
|
|
6
|
+
*
|
|
7
|
+
* 출력은 단일 string (agent message) 으로 통일 — caller 가 stdout 파싱 안 해도 됨.
|
|
8
|
+
*
|
|
9
|
+
* 호환성: 기존 'claude -p prompt --model haiku' 호출은 default_host 가 'claude' 인
|
|
10
|
+
* 경우 동일 동작. Codex 메인 사용자는 자동으로 codex exec --json 호출.
|
|
11
|
+
*/
|
|
12
|
+
export interface ExecHostOptions {
|
|
13
|
+
/** prompt — `-p`/`exec` 의 본문 */
|
|
14
|
+
prompt: string;
|
|
15
|
+
/** model 힌트 (claude: --model haiku, codex: 무시 — codex CLI 가 default 사용) */
|
|
16
|
+
model?: string;
|
|
17
|
+
/** child process timeout (ms). 미지정 시 host 별 기본값: claude 30s, codex 90s. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/** working directory */
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** explicit host override (default: profile.default_host). */
|
|
22
|
+
host?: 'claude' | 'codex';
|
|
23
|
+
/** ENV vars 추가 (기존 process.env 위에 머지) */
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Host 별 기본 timeout. Phase 3 deferred fix:
|
|
28
|
+
* - claude -p 는 보통 1~5s 응답이라 30s 충분.
|
|
29
|
+
* - codex exec --json 은 cold start + reasoning 모드로 60~90s 정상이라
|
|
30
|
+
* 30s default 가 false-positive ETIMEDOUT 발생. 90s 마진 필요.
|
|
31
|
+
* - 명시 timeout 옵션은 그대로 우선.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DEFAULT_TIMEOUT_BY_HOST: Record<'claude' | 'codex', number>;
|
|
34
|
+
export interface ExecHostResult {
|
|
35
|
+
message: string;
|
|
36
|
+
host: 'claude' | 'codex';
|
|
37
|
+
/** 토큰 사용량 (codex 만 노출. claude 는 null). */
|
|
38
|
+
usage: {
|
|
39
|
+
input_tokens?: number;
|
|
40
|
+
output_tokens?: number;
|
|
41
|
+
} | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 실 host CLI 를 호출하여 prompt 응답 받기.
|
|
45
|
+
* - claude: `claude -p <prompt> --model <model>`
|
|
46
|
+
* - codex: `codex exec --json -s read-only -c approval_policy="never" --ephemeral --skip-git-repo-check <prompt>`
|
|
47
|
+
*
|
|
48
|
+
* Codex 호출은 sandbox read-only + approval never + ephemeral 로 *자동 추출 안전성*
|
|
49
|
+
* 보장 (사용자 환경 미오염). compound-extractor / auto-compound-runner 같은
|
|
50
|
+
* 백그라운드 학습 호출에 적합.
|
|
51
|
+
*/
|
|
52
|
+
export declare function execHost(opts: ExecHostOptions): ExecHostResult;
|
|
53
|
+
/** 1회 retry — transient 에러(ETIMEDOUT 등) 대응. */
|
|
54
|
+
export declare function execHostRetry(opts: ExecHostOptions): ExecHostResult;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-aware exec — feat/codex-support Phase 2 (P2-2/P2-3 공통)
|
|
3
|
+
*
|
|
4
|
+
* compound-extractor + auto-compound-runner 가 *어느 host CLI 로 LLM 호출* 할지
|
|
5
|
+
* 결정. profile.default_host 우선 + override 가능.
|
|
6
|
+
*
|
|
7
|
+
* 출력은 단일 string (agent message) 으로 통일 — caller 가 stdout 파싱 안 해도 됨.
|
|
8
|
+
*
|
|
9
|
+
* 호환성: 기존 'claude -p prompt --model haiku' 호출은 default_host 가 'claude' 인
|
|
10
|
+
* 경우 동일 동작. Codex 메인 사용자는 자동으로 codex exec --json 호출.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { resolveDefaultHost } from '../store/profile-store.js';
|
|
14
|
+
import { parseCodexJsonlOutput } from './codex-output-parser.js';
|
|
15
|
+
/**
|
|
16
|
+
* Host 별 기본 timeout. Phase 3 deferred fix:
|
|
17
|
+
* - claude -p 는 보통 1~5s 응답이라 30s 충분.
|
|
18
|
+
* - codex exec --json 은 cold start + reasoning 모드로 60~90s 정상이라
|
|
19
|
+
* 30s default 가 false-positive ETIMEDOUT 발생. 90s 마진 필요.
|
|
20
|
+
* - 명시 timeout 옵션은 그대로 우선.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_TIMEOUT_BY_HOST = {
|
|
23
|
+
claude: 30_000,
|
|
24
|
+
codex: 90_000,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 실 host CLI 를 호출하여 prompt 응답 받기.
|
|
28
|
+
* - claude: `claude -p <prompt> --model <model>`
|
|
29
|
+
* - codex: `codex exec --json -s read-only -c approval_policy="never" --ephemeral --skip-git-repo-check <prompt>`
|
|
30
|
+
*
|
|
31
|
+
* Codex 호출은 sandbox read-only + approval never + ephemeral 로 *자동 추출 안전성*
|
|
32
|
+
* 보장 (사용자 환경 미오염). compound-extractor / auto-compound-runner 같은
|
|
33
|
+
* 백그라운드 학습 호출에 적합.
|
|
34
|
+
*/
|
|
35
|
+
export function execHost(opts) {
|
|
36
|
+
const resolved = resolveDefaultHost(opts.host);
|
|
37
|
+
// 'ask' 는 자동 호출 컨텍스트라 명시 fallback. 그러나 Codex-only 사용자가 'ask'
|
|
38
|
+
// 설정 후 claude 가 PATH 에 없으면 ENOENT 발생 → 명시 안내. (Phase 2 critic fix)
|
|
39
|
+
const host = resolved === 'codex' ? 'codex' : 'claude';
|
|
40
|
+
if (resolved === 'ask' && opts.host === undefined) {
|
|
41
|
+
// 자동 호출에서 'ask' 도달 — caller 가 명시 host 안 줬으므로 default fallback 안내.
|
|
42
|
+
process.stderr.write('[forgen exec-host] default_host="ask" — auto-call falling back to claude. ' +
|
|
43
|
+
'If claude CLI is missing, set: forgen config default-host {claude|codex}\n');
|
|
44
|
+
}
|
|
45
|
+
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_BY_HOST[host];
|
|
46
|
+
const baseOpts = {
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
timeout,
|
|
49
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
50
|
+
cwd: opts.cwd,
|
|
51
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
52
|
+
};
|
|
53
|
+
if (host === 'claude') {
|
|
54
|
+
const args = ['-p', opts.prompt];
|
|
55
|
+
if (opts.model)
|
|
56
|
+
args.push('--model', opts.model);
|
|
57
|
+
const stdout = execFileSync('claude', args, baseOpts);
|
|
58
|
+
return { message: stdout.toString().trim(), host: 'claude', usage: null };
|
|
59
|
+
}
|
|
60
|
+
// host === 'codex'
|
|
61
|
+
// Phase 2 critic fix: -c approval_policy="never" 의 인용부호는 shell 처리 없이
|
|
62
|
+
// execFileSync 인자라 codex 가 literal `"never"` 로 받을 위험. quote 제거 + 실측 검증.
|
|
63
|
+
const args = [
|
|
64
|
+
'exec',
|
|
65
|
+
'--json',
|
|
66
|
+
'-s', 'read-only',
|
|
67
|
+
'-c', 'approval_policy=never',
|
|
68
|
+
'--ephemeral',
|
|
69
|
+
'--skip-git-repo-check',
|
|
70
|
+
opts.prompt,
|
|
71
|
+
];
|
|
72
|
+
const stdout = execFileSync('codex', args, baseOpts);
|
|
73
|
+
const parsed = parseCodexJsonlOutput(stdout.toString());
|
|
74
|
+
return {
|
|
75
|
+
message: parsed.message,
|
|
76
|
+
host: 'codex',
|
|
77
|
+
usage: parsed.usage ? { input_tokens: parsed.usage.input_tokens, output_tokens: parsed.usage.output_tokens } : null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** 1회 retry — transient 에러(ETIMEDOUT 등) 대응. */
|
|
81
|
+
export function execHostRetry(opts) {
|
|
82
|
+
try {
|
|
83
|
+
return execHost(opts);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
const code = e?.code;
|
|
87
|
+
if (code === 'ETIMEDOUT' || code === 'ECONNRESET') {
|
|
88
|
+
return execHost(opts);
|
|
89
|
+
}
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HostRuntime — Multi-Host Core Design Phase 2
|
|
3
|
+
*
|
|
4
|
+
* `runtime === 'codex'` 분기를 core 에서 제거하기 위한 host-specific 표면 모듈.
|
|
5
|
+
* spec §3.3 / §5.3 의 비대칭 경계: core 는 Claude semantics 알아도 됨, Codex 표면만 모름.
|
|
6
|
+
*
|
|
7
|
+
* 본 모듈이 노출하는 host-specific 표면:
|
|
8
|
+
* - launcher binary 이름 (codex / claude)
|
|
9
|
+
* - 사용자 표시 라벨 (Codex / Claude)
|
|
10
|
+
* - hook command wrapping (Codex 는 codex-adapter 경유)
|
|
11
|
+
* - 미설치 시 에러 메시지 (host 별 안내)
|
|
12
|
+
*
|
|
13
|
+
* core 측 코드는 본 모듈의 `getHostRuntime(runtime)` 만 호출하여 동작 분기를 위임.
|
|
14
|
+
*/
|
|
15
|
+
import type { RuntimeHost } from '../core/types.js';
|
|
16
|
+
export interface HostRuntime {
|
|
17
|
+
readonly id: RuntimeHost;
|
|
18
|
+
/** 사용자에게 노출되는 표시명 (UI 라벨, 로그). */
|
|
19
|
+
readonly displayName: string;
|
|
20
|
+
/** 실 실행 binary 이름 또는 절대경로. PATH 에서 찾으면 됨. */
|
|
21
|
+
readonly launcher: string;
|
|
22
|
+
/** 미설치 ENOENT 시 사용자에게 노출할 안내. */
|
|
23
|
+
readonly missingInstallMessage: string;
|
|
24
|
+
/**
|
|
25
|
+
* Hook command 래핑.
|
|
26
|
+
* Claude: `node "${pluginRoot}/${script}" ${args}`
|
|
27
|
+
* Codex: `node "${pluginRoot}/host/codex-adapter.js" "${pluginRoot}/${script}" ${args}` (sandbox 호환 + projection)
|
|
28
|
+
*/
|
|
29
|
+
wrapHookCommand(pluginRoot: string, scriptPath: string, args: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* settings hook injection strategy.
|
|
32
|
+
* - 'generate': generateHooksJson({runtime}) 호출 (Codex 등, host-aware wrapping 필요)
|
|
33
|
+
* - 'pre-baked-file': pkgRoot/hooks/hooks.json 읽고 ${CLAUDE_PLUGIN_ROOT} 치환 (Claude — 빌드 산출물 재사용)
|
|
34
|
+
*/
|
|
35
|
+
readonly hookInjectionStrategy: 'generate' | 'pre-baked-file';
|
|
36
|
+
}
|
|
37
|
+
export declare function getHostRuntime(runtime: RuntimeHost): HostRuntime;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HostRuntime — Multi-Host Core Design Phase 2
|
|
3
|
+
*
|
|
4
|
+
* `runtime === 'codex'` 분기를 core 에서 제거하기 위한 host-specific 표면 모듈.
|
|
5
|
+
* spec §3.3 / §5.3 의 비대칭 경계: core 는 Claude semantics 알아도 됨, Codex 표면만 모름.
|
|
6
|
+
*
|
|
7
|
+
* 본 모듈이 노출하는 host-specific 표면:
|
|
8
|
+
* - launcher binary 이름 (codex / claude)
|
|
9
|
+
* - 사용자 표시 라벨 (Codex / Claude)
|
|
10
|
+
* - hook command wrapping (Codex 는 codex-adapter 경유)
|
|
11
|
+
* - 미설치 시 에러 메시지 (host 별 안내)
|
|
12
|
+
*
|
|
13
|
+
* core 측 코드는 본 모듈의 `getHostRuntime(runtime)` 만 호출하여 동작 분기를 위임.
|
|
14
|
+
*/
|
|
15
|
+
function quoteArg(raw) {
|
|
16
|
+
return `"${raw.replace(/"/g, '\\"')}"`;
|
|
17
|
+
}
|
|
18
|
+
const claudeRuntime = {
|
|
19
|
+
id: 'claude',
|
|
20
|
+
displayName: 'Claude',
|
|
21
|
+
launcher: 'claude',
|
|
22
|
+
missingInstallMessage: 'Claude Code is not installed. npm install -g @anthropic-ai/claude-code',
|
|
23
|
+
wrapHookCommand(pluginRoot, scriptPath, args) {
|
|
24
|
+
const fullScript = `${pluginRoot}/${scriptPath}`;
|
|
25
|
+
return args ? `node ${quoteArg(fullScript)} ${args}` : `node ${quoteArg(fullScript)}`;
|
|
26
|
+
},
|
|
27
|
+
hookInjectionStrategy: 'pre-baked-file',
|
|
28
|
+
};
|
|
29
|
+
const codexRuntime = {
|
|
30
|
+
id: 'codex',
|
|
31
|
+
displayName: 'Codex',
|
|
32
|
+
launcher: 'codex',
|
|
33
|
+
missingInstallMessage: 'Codex is not installed.',
|
|
34
|
+
wrapHookCommand(pluginRoot, scriptPath, args) {
|
|
35
|
+
const adapterPath = `${pluginRoot}/host/codex-adapter.js`;
|
|
36
|
+
const fullScript = `${pluginRoot}/${scriptPath}`;
|
|
37
|
+
const base = `node ${quoteArg(adapterPath)} ${quoteArg(fullScript)}`;
|
|
38
|
+
return args ? `${base} ${args}` : base;
|
|
39
|
+
},
|
|
40
|
+
hookInjectionStrategy: 'generate',
|
|
41
|
+
};
|
|
42
|
+
const RUNTIMES = {
|
|
43
|
+
claude: claudeRuntime,
|
|
44
|
+
codex: codexRuntime,
|
|
45
|
+
};
|
|
46
|
+
export function getHostRuntime(runtime) {
|
|
47
|
+
const r = RUNTIMES[runtime];
|
|
48
|
+
if (!r)
|
|
49
|
+
throw new Error(`Unknown runtime host: ${runtime}`);
|
|
50
|
+
return r;
|
|
51
|
+
}
|