@wooojin/forgen 0.4.1 → 0.4.4
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 +267 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +17 -9
- package/README.ko.md +34 -12
- package/README.md +65 -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/{commands → assets/claude/commands}/calibrate.md +4 -3
- package/{commands → assets/claude/commands}/retro.md +2 -2
- 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/_shared/text-sanitizer.d.ts +21 -0
- package/dist/checks/_shared/text-sanitizer.js +60 -0
- package/dist/checks/dangerous-response-pattern.d.ts +32 -0
- package/dist/checks/dangerous-response-pattern.js +65 -0
- package/dist/checks/fact-vs-agreement.js +25 -1
- package/dist/cli.js +78 -6
- package/dist/core/auto-compound-runner.js +90 -39
- 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 +57 -25
- 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 +51 -0
- package/dist/renderer/rule-renderer.d.ts +1 -1
- package/dist/renderer/rule-renderer.js +73 -1
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/compound-usage-store.d.ts +28 -0
- package/dist/store/compound-usage-store.js +59 -0
- 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/skills/calibrate/SKILL.md +4 -3
- package/skills/retro/SKILL.md +2 -2
- /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}/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}/ship.md +0 -0
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -26,6 +26,9 @@ import { approve, approveWithWarning, blockStop, failOpenWithTracking } from './
|
|
|
26
26
|
import { takeLastExtractionNotice } from '../core/extraction-notice.js';
|
|
27
27
|
import { checkConclusionVerificationRatio } from '../checks/conclusion-verification-ratio.js';
|
|
28
28
|
import { checkSelfScoreInflation } from '../checks/self-score-deflation.js';
|
|
29
|
+
import { checkFactVsAgreement } from '../checks/fact-vs-agreement.js';
|
|
30
|
+
import { checkDangerousResponsePattern } from '../checks/dangerous-response-pattern.js';
|
|
31
|
+
import { sanitizeForGuard } from '../checks/_shared/text-sanitizer.js';
|
|
29
32
|
import { STATE_DIR } from '../core/paths.js';
|
|
30
33
|
import { sanitizeId } from './shared/sanitize-id.js';
|
|
31
34
|
import { detectRecallReferences } from '../core/recall-reference-detector.js';
|
|
@@ -495,42 +498,71 @@ export async function main() {
|
|
|
495
498
|
// block/approve 어느 경로이든 동일하게 기록 (참조는 응답 내용이 결정).
|
|
496
499
|
const sessionIdForRef = input?.session_id ?? 'unknown';
|
|
497
500
|
emitRecallReferencesFailOpen(sessionIdForRef, lastMessage);
|
|
498
|
-
// TEST-2/3: rule-free meta guards — FORGEN_USER_CONFIRMED=1 우회 공통.
|
|
501
|
+
// TEST-1/2/3: rule-free meta guards — FORGEN_USER_CONFIRMED=1 우회 공통.
|
|
502
|
+
// Pathfinder D7: 3중 보일러플레이트를 CHECKS 배열 + for-loop 디스패처로 통합.
|
|
503
|
+
// Pathfinder D4/D5: sanitizeForGuard 가 모든 체크 입력 단계에 일괄 적용.
|
|
499
504
|
if (process.env.FORGEN_USER_CONFIRMED !== '1') {
|
|
500
505
|
const sessionId = input?.session_id ?? 'unknown';
|
|
501
|
-
// TEST-2 (자가 점수 인플레이션): 숫자 점수 상승 선언 + 측정 도구 0회 → block.
|
|
502
|
-
// TEST-3 보다 강한 신호라 먼저 평가.
|
|
503
506
|
const recentTools = loadRecentToolNames(sessionId);
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
507
|
+
const sanitized = sanitizeForGuard(lastMessage);
|
|
508
|
+
// 평가 순서: DANGEROUS-RESPONSE (즉시 차단 — 안전 우선) → TEST-2 (강한 신호) → TEST-3 (텍스트 비율) → TEST-1 (alert-only).
|
|
509
|
+
const checks = [
|
|
510
|
+
{
|
|
511
|
+
shortId: 'dangerous-response-pattern',
|
|
512
|
+
ruleSlug: 'rule:DANGEROUS-RESPONSE — destructive command suggestion',
|
|
510
513
|
kind: 'block',
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
514
|
+
// 주의: sanitizer 가 백틱/코드블록을 제거하므로 raw lastMessage 를 전달.
|
|
515
|
+
// 위험 명령은 코드 fence 안에 있어도 동등하게 위험함.
|
|
516
|
+
run: () => {
|
|
517
|
+
const r = checkDangerousResponsePattern({ text: lastMessage });
|
|
518
|
+
return { triggered: r.block, reason: r.reason };
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
shortId: 'self-score-inflation',
|
|
523
|
+
ruleSlug: 'rule:TEST-2 — self-score inflation',
|
|
524
|
+
kind: 'block',
|
|
525
|
+
run: () => {
|
|
526
|
+
const r = checkSelfScoreInflation({ text: sanitized, recentTools });
|
|
527
|
+
return { triggered: r.block, reason: r.reason };
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
shortId: 'conclusion-ratio',
|
|
532
|
+
ruleSlug: 'rule:TEST-3 — conclusion/verification ratio',
|
|
533
|
+
kind: 'block',
|
|
534
|
+
run: () => {
|
|
535
|
+
const r = checkConclusionVerificationRatio({ text: sanitized });
|
|
536
|
+
return { triggered: r.block, reason: r.reason };
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
shortId: 'fact-vs-agreement',
|
|
541
|
+
ruleSlug: 'rule:TEST-1 — fact vs agreement',
|
|
542
|
+
kind: 'correction', // alert-level only per fact-vs-agreement.ts design
|
|
543
|
+
run: () => {
|
|
544
|
+
const r = checkFactVsAgreement({ text: sanitized, recentTools, minMeasurements: 1 });
|
|
545
|
+
return { triggered: r.alert, reason: r.reason };
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
for (const c of checks) {
|
|
550
|
+
const out = c.run();
|
|
551
|
+
if (!out.triggered)
|
|
552
|
+
continue;
|
|
523
553
|
recordViolation({
|
|
524
|
-
rule_id:
|
|
554
|
+
rule_id: `builtin:${c.shortId}`,
|
|
525
555
|
session_id: sessionId,
|
|
526
556
|
source: 'stop-guard',
|
|
527
|
-
kind:
|
|
557
|
+
kind: c.kind,
|
|
528
558
|
message_preview: lastMessage.slice(0, 120),
|
|
529
559
|
});
|
|
530
|
-
|
|
560
|
+
if (c.kind !== 'block')
|
|
561
|
+
continue;
|
|
562
|
+
const reasonText = `[forgen:stop-guard/${c.shortId}] ${out.reason}
|
|
531
563
|
|
|
532
564
|
(Override this turn: set FORGEN_USER_CONFIRMED=1 (audited).)`;
|
|
533
|
-
console.log(blockStop(reasonText,
|
|
565
|
+
console.log(blockStop(reasonText, c.ruleSlug));
|
|
534
566
|
return;
|
|
535
567
|
}
|
|
536
568
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
import type { HostCapabilities } from '../core/trust-layer-intent.js';
|
|
8
|
+
export declare const claudeCapabilities: HostCapabilities;
|
|
@@ -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;
|