@wooojin/forgen 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/.claude-plugin/plugin.json +5 -5
  2. package/CHANGELOG.md +164 -15
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.ja.md +17 -9
  5. package/README.ko.md +20 -12
  6. package/README.md +46 -12
  7. package/README.zh.md +17 -9
  8. package/assets/README.md +86 -0
  9. package/assets/architecture.svg +100 -0
  10. package/assets/banner.png +0 -0
  11. package/assets/banner.svg +53 -0
  12. package/assets/demo/01-install.gif +0 -0
  13. package/assets/demo/01-install.tape +54 -0
  14. package/assets/demo/02-compound-learning.gif +0 -0
  15. package/assets/demo/02-compound-learning.tape +50 -0
  16. package/assets/demo/03-forge-personalization.gif +0 -0
  17. package/assets/demo/03-forge-personalization.tape +64 -0
  18. package/assets/demo/before-after.gif +0 -0
  19. package/assets/demo/before-after.tape +98 -0
  20. package/assets/demo-preview.svg +96 -0
  21. package/assets/icon.png +0 -0
  22. package/{hooks → assets/shared}/hook-registry.json +2 -1
  23. package/dist/cli.js +78 -6
  24. package/dist/core/auto-compound-runner.js +62 -38
  25. package/dist/core/behavior-classifier.d.ts +28 -0
  26. package/dist/core/behavior-classifier.js +46 -0
  27. package/dist/core/dashboard.d.ts +7 -0
  28. package/dist/core/dashboard.js +32 -0
  29. package/dist/core/doctor.js +92 -0
  30. package/dist/core/git-stats.d.ts +36 -0
  31. package/dist/core/git-stats.js +79 -0
  32. package/dist/core/harness.d.ts +1 -1
  33. package/dist/core/harness.js +27 -20
  34. package/dist/core/host-detect.d.ts +42 -0
  35. package/dist/core/host-detect.js +68 -0
  36. package/dist/core/installer.js +2 -2
  37. package/dist/core/migrate-cli.d.ts +1 -0
  38. package/dist/core/migrate-cli.js +19 -0
  39. package/dist/core/migrate-evidence-host.d.ts +36 -0
  40. package/dist/core/migrate-evidence-host.js +49 -0
  41. package/dist/core/settings-injector.js +4 -2
  42. package/dist/core/spawn.d.ts +1 -1
  43. package/dist/core/spawn.js +4 -11
  44. package/dist/core/stats-cli.js +12 -0
  45. package/dist/core/trust-layer-intent.d.ts +35 -0
  46. package/dist/core/trust-layer-intent.js +30 -0
  47. package/dist/core/types.d.ts +1 -1
  48. package/dist/engine/compound-extractor.js +7 -9
  49. package/dist/engine/learn-cli.js +4 -2
  50. package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
  51. package/dist/engine/lifecycle/bypass-detector.js +57 -5
  52. package/dist/fgx.js +2 -1
  53. package/dist/forge/evidence-processor.js +12 -0
  54. package/dist/forge/onboarding.d.ts +3 -2
  55. package/dist/forge/onboarding.js +3 -2
  56. package/dist/hooks/db-guard.js +3 -3
  57. package/dist/hooks/forge-loop-progress.d.ts +9 -0
  58. package/dist/hooks/forge-loop-progress.js +38 -0
  59. package/dist/hooks/hook-registry.js +1 -1
  60. package/dist/hooks/hooks-generator.d.ts +15 -1
  61. package/dist/hooks/hooks-generator.js +18 -16
  62. package/dist/hooks/keyword-detector.js +1 -1
  63. package/dist/hooks/post-tool-use.d.ts +1 -1
  64. package/dist/hooks/post-tool-use.js +13 -4
  65. package/dist/hooks/pre-compact.js +1 -1
  66. package/dist/hooks/pre-tool-use.js +4 -4
  67. package/dist/hooks/rate-limiter.js +2 -2
  68. package/dist/hooks/session-recovery.js +11 -0
  69. package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
  70. package/dist/hooks/shared/blocking-allowlist.js +38 -0
  71. package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
  72. package/dist/hooks/shared/forge-loop-state.js +116 -0
  73. package/dist/hooks/shared/hook-response.d.ts +18 -0
  74. package/dist/hooks/shared/hook-response.js +31 -0
  75. package/dist/hooks/skill-injector.js +1 -1
  76. package/dist/hooks/stop-guard.js +15 -0
  77. package/dist/host/capabilities-claude.d.ts +8 -0
  78. package/dist/host/capabilities-claude.js +46 -0
  79. package/dist/host/capabilities-codex.d.ts +11 -0
  80. package/dist/host/capabilities-codex.js +50 -0
  81. package/dist/host/capabilities-registry.d.ts +11 -0
  82. package/dist/host/capabilities-registry.js +30 -0
  83. package/dist/host/codex-adapter.d.ts +8 -5
  84. package/dist/host/codex-adapter.js +10 -82
  85. package/dist/host/codex-output-parser.d.ts +39 -0
  86. package/dist/host/codex-output-parser.js +75 -0
  87. package/dist/host/exec-host.d.ts +54 -0
  88. package/dist/host/exec-host.js +92 -0
  89. package/dist/host/host-runtime.d.ts +37 -0
  90. package/dist/host/host-runtime.js +51 -0
  91. package/dist/host/install-claude.d.ts +35 -0
  92. package/dist/host/install-claude.js +238 -0
  93. package/dist/host/install-codex.d.ts +44 -0
  94. package/dist/host/install-codex.js +276 -0
  95. package/dist/host/install-orchestrator.d.ts +34 -0
  96. package/dist/host/install-orchestrator.js +126 -0
  97. package/dist/host/invoke-agent.d.ts +27 -0
  98. package/dist/host/invoke-agent.js +115 -0
  99. package/dist/host/parity-harness.d.ts +62 -0
  100. package/dist/host/parity-harness.js +283 -0
  101. package/dist/host/projection.d.ts +35 -0
  102. package/dist/host/projection.js +126 -0
  103. package/dist/mcp/server.js +11 -0
  104. package/dist/mcp/tools.js +47 -0
  105. package/dist/services/session.d.ts +6 -3
  106. package/dist/services/session.js +33 -4
  107. package/dist/store/evidence-store.d.ts +1 -0
  108. package/dist/store/evidence-store.js +34 -3
  109. package/dist/store/host-mismatch.d.ts +42 -0
  110. package/dist/store/host-mismatch.js +65 -0
  111. package/dist/store/profile-store.d.ts +29 -0
  112. package/dist/store/profile-store.js +53 -0
  113. package/dist/store/types.d.ts +13 -0
  114. package/hooks/hooks.json +6 -1
  115. package/package.json +6 -4
  116. package/plugin.json +4 -4
  117. package/scripts/postinstall.js +100 -25
  118. /package/{agents → assets/claude/agents}/analyst.md +0 -0
  119. /package/{agents → assets/claude/agents}/architect.md +0 -0
  120. /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
  121. /package/{agents → assets/claude/agents}/critic.md +0 -0
  122. /package/{agents → assets/claude/agents}/debugger.md +0 -0
  123. /package/{agents → assets/claude/agents}/designer.md +0 -0
  124. /package/{agents → assets/claude/agents}/executor.md +0 -0
  125. /package/{agents → assets/claude/agents}/explore.md +0 -0
  126. /package/{agents → assets/claude/agents}/git-master.md +0 -0
  127. /package/{agents → assets/claude/agents}/planner.md +0 -0
  128. /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
  129. /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
  130. /package/{agents → assets/claude/agents}/verifier.md +0 -0
  131. /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
  132. /package/{commands → assets/claude/commands}/calibrate.md +0 -0
  133. /package/{commands → assets/claude/commands}/code-review.md +0 -0
  134. /package/{commands → assets/claude/commands}/compound.md +0 -0
  135. /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
  136. /package/{commands → assets/claude/commands}/docker.md +0 -0
  137. /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
  138. /package/{commands → assets/claude/commands}/learn.md +0 -0
  139. /package/{commands → assets/claude/commands}/retro.md +0 -0
  140. /package/{commands → assets/claude/commands}/ship.md +0 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Forge Loop State — RC6 가드 (US-M1)
3
+ *
4
+ * 직전 forge-loop 의 findings 또는 진행 중 stories 를 ≤1KB 요약으로 렌더한다.
5
+ * SessionStart 와 UserPromptSubmit 두 hook 이 공유하는 단일 진입점.
6
+ *
7
+ * RC6 자기증거: 본 세션 R1 에서 head -80 으로 forge-loop.json 을 읽어 findings
8
+ * 8줄(line 92~99)이 잘렸음. 결과적으로 직전 결론을 컨텍스트에 못 가져 같은
9
+ * 가설을 재발. 이 모듈은 그 회귀를 시스템 레벨에서 차단한다.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { STATE_DIR } from '../../core/paths.js';
14
+ const FORGE_LOOP_PATH = path.join(STATE_DIR, 'forge-loop.json');
15
+ const SOFT_STALE_MS = 24 * 60 * 60 * 1000;
16
+ const HARD_STALE_MS = 7 * 24 * 60 * 60 * 1000;
17
+ const MAX_INJECT_BYTES = 1024;
18
+ const MAX_TASK_CHARS = 240;
19
+ const MAX_FINDING_CHARS = 240;
20
+ const MAX_PENDING = 5;
21
+ export function readForgeLoopState(filePath = FORGE_LOOP_PATH) {
22
+ try {
23
+ if (!fs.existsSync(filePath))
24
+ return null;
25
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
26
+ if (typeof raw !== 'object' || raw === null)
27
+ return null;
28
+ return raw;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function ageMs(state, now = Date.now()) {
35
+ const ts = state.completedAt ?? state.startedAt;
36
+ if (!ts)
37
+ return Number.POSITIVE_INFINITY;
38
+ const t = new Date(ts).getTime();
39
+ if (Number.isNaN(t))
40
+ return Number.POSITIVE_INFINITY;
41
+ return now - t;
42
+ }
43
+ function clipBlock(block) {
44
+ if (block.length <= MAX_INJECT_BYTES)
45
+ return block;
46
+ return `${block.slice(0, MAX_INJECT_BYTES - 3)}...`;
47
+ }
48
+ function escXml(s) {
49
+ return s.replace(/[<>&"]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' })[c] ?? c);
50
+ }
51
+ /** SessionStart 용 — 완료된 forge-loop 의 findings 또는 진행 중 stories 요약. */
52
+ export function renderForgeLoopForSession(state, now = Date.now()) {
53
+ if (!state)
54
+ return null;
55
+ const age = ageMs(state, now);
56
+ if (age > HARD_STALE_MS)
57
+ return null;
58
+ const stale = age > SOFT_STALE_MS;
59
+ const lines = [];
60
+ const task = String(state.task ?? '').trim();
61
+ if (task)
62
+ lines.push(`Task: ${escXml(task.slice(0, MAX_TASK_CHARS))}`);
63
+ if (state.active && Array.isArray(state.stories)) {
64
+ const total = state.stories.length;
65
+ const done = state.stories.filter(s => s?.passes).length;
66
+ lines.push(`Status: in-progress ${done}/${total}${stale ? ' (stale)' : ''}`);
67
+ const pending = state.stories
68
+ .filter(s => !s?.passes)
69
+ .slice(0, MAX_PENDING)
70
+ .map(s => `- ${escXml(String(s.id))}: ${escXml(String(s.title))}`);
71
+ if (pending.length) {
72
+ lines.push('Pending:');
73
+ lines.push(...pending);
74
+ }
75
+ }
76
+ else if (state.findings && typeof state.findings === 'object') {
77
+ lines.push(`Status: completed${stale ? ' (stale)' : ''}`);
78
+ for (const [id, val] of Object.entries(state.findings)) {
79
+ const text = String(val ?? '').slice(0, MAX_FINDING_CHARS);
80
+ if (text)
81
+ lines.push(`- ${escXml(id)}: ${escXml(text)}`);
82
+ }
83
+ }
84
+ else {
85
+ return null;
86
+ }
87
+ if (lines.length === 0)
88
+ return null;
89
+ const tag = stale ? '<forge-loop-state stale="true">' : '<forge-loop-state>';
90
+ const body = lines.join('\n');
91
+ return clipBlock(`${tag}\n${body}\n</forge-loop-state>`);
92
+ }
93
+ /** UserPromptSubmit 용 — active=true 시에만 짧은 진행 상황 1~2줄. */
94
+ export function renderForgeLoopForPrompt(state, now = Date.now()) {
95
+ if (!state || !state.active || !Array.isArray(state.stories))
96
+ return null;
97
+ const age = ageMs(state, now);
98
+ if (age > HARD_STALE_MS)
99
+ return null;
100
+ const total = state.stories.length;
101
+ const done = state.stories.filter(s => s?.passes).length;
102
+ const next = state.stories.find(s => !s?.passes);
103
+ if (!next)
104
+ return null;
105
+ const stale = age > SOFT_STALE_MS;
106
+ const tag = stale ? '<forge-loop-active stale="true">' : '<forge-loop-active>';
107
+ const body = `Progress: ${done}/${total} | next: ${escXml(String(next.id))} ${escXml(String(next.title))}`;
108
+ return clipBlock(`${tag}\n${body}\n</forge-loop-active>`);
109
+ }
110
+ /** 테스트 노출용 상수 — 회귀 시 임계값 변경 즉시 감지. */
111
+ export const FORGE_LOOP_LIMITS = {
112
+ SOFT_STALE_MS,
113
+ HARD_STALE_MS,
114
+ MAX_INJECT_BYTES,
115
+ MAX_PENDING,
116
+ };
@@ -34,6 +34,24 @@ export declare function approveWithWarning(warning: string): string;
34
34
  export declare function deny(reason: string): string;
35
35
  /** 사용자 확인 요청 (PreToolUse 전용) */
36
36
  export declare function ask(reason: string): string;
37
+ /**
38
+ * P3' enforcement helper (2026-04-27)
39
+ *
40
+ * ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
41
+ *
42
+ * 사용:
43
+ * ```ts
44
+ * import { canBlock } from './blocking-allowlist.js';
45
+ * if (someCondition) {
46
+ * console.log(blockOrObserve(hookName, 'reason', logCallback));
47
+ * return;
48
+ * }
49
+ * ```
50
+ *
51
+ * 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
52
+ * 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
53
+ */
54
+ export declare function denyOrObserve(hookName: string, reason: string, observer?: (msg: string) => void): string;
37
55
  /**
38
56
  * Stop hook only — block the agent from stopping and feed a self-check
39
57
  * question back to Claude so the current session resumes with new guidance.
@@ -16,6 +16,7 @@
16
16
  import * as fs from 'node:fs';
17
17
  import * as path from 'node:path';
18
18
  import { STATE_DIR } from '../../core/paths.js';
19
+ import { canBlock } from './blocking-allowlist.js';
19
20
  /** 통과 응답 (컨텍스트 없음, 모든 이벤트 공통) */
20
21
  export function approve() {
21
22
  return JSON.stringify({ continue: true });
@@ -65,6 +66,36 @@ export function ask(reason) {
65
66
  },
66
67
  });
67
68
  }
69
+ /**
70
+ * P3' enforcement helper (2026-04-27)
71
+ *
72
+ * ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
73
+ *
74
+ * 사용:
75
+ * ```ts
76
+ * import { canBlock } from './blocking-allowlist.js';
77
+ * if (someCondition) {
78
+ * console.log(blockOrObserve(hookName, 'reason', logCallback));
79
+ * return;
80
+ * }
81
+ * ```
82
+ *
83
+ * 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
84
+ * 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
85
+ */
86
+ export function denyOrObserve(hookName, reason, observer) {
87
+ if (canBlock(hookName)) {
88
+ return deny(reason);
89
+ }
90
+ // ALLOW-LIST 외 — log only, approve
91
+ if (observer) {
92
+ try {
93
+ observer(`[${hookName}] would-deny (observe-only): ${reason}`);
94
+ }
95
+ catch { /* fail-open */ }
96
+ }
97
+ return approve();
98
+ }
68
99
  /**
69
100
  * Stop hook only — block the agent from stopping and feed a self-check
70
101
  * question back to Claude so the current session resumes with new guidance.
@@ -181,7 +181,7 @@ function collectSkills() {
181
181
  const skills = [];
182
182
  const seen = new Map(); // name → source dir
183
183
  // 패키지 내장 스킬 경로 (dist/../skills/)
184
- const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
184
+ const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'claude', 'commands');
185
185
  // 프로젝트 .forgen > 프로젝트 .compound > 개인 > 글로벌 > 패키지 내장
186
186
  const dirs = [
187
187
  path.join(process.cwd(), '.forgen', 'skills'),
@@ -26,6 +26,7 @@ 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';
29
30
  import { STATE_DIR } from '../core/paths.js';
30
31
  import { sanitizeId } from './shared/sanitize-id.js';
31
32
  import { detectRecallReferences } from '../core/recall-reference-detector.js';
@@ -533,6 +534,20 @@ export async function main() {
533
534
  console.log(blockStop(reasonText, 'rule:TEST-3 — conclusion/verification ratio'));
534
535
  return;
535
536
  }
537
+ // TEST-1: 사실 vs 합의 — fact assertion 키워드가 있으나 측정 도구 호출 0건.
538
+ // 원 design intent (per fact-vs-agreement.ts): alert level only — block 은 TEST-2/3.
539
+ // 여기서는 measurement 신호를 violations.jsonl 에 'alert' kind 로 기록만 (block 안 함).
540
+ // wiring gap 발견 (forgen-eval introspect) → 측정 가능하게 wired up.
541
+ const fva = checkFactVsAgreement({ text: lastMessage, recentTools, minMeasurements: 1 });
542
+ if (fva.alert) {
543
+ recordViolation({
544
+ rule_id: 'builtin:fact-vs-agreement',
545
+ session_id: sessionId,
546
+ source: 'stop-guard',
547
+ kind: 'correction', // alert-level signal (not block) per fact-vs-agreement.ts design
548
+ message_preview: lastMessage.slice(0, 120),
549
+ });
550
+ }
536
551
  }
537
552
  const rules = loadStopRules();
538
553
  if (rules.length === 0) {
@@ -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
- * - codex 런타임에서 실행되는 스크립트 출력을 Claude Hook schema로 정규화
7
- * - continue 누락 또는 codex 특화 판정 필드(approved/decision) 대응
8
- * - 파싱 실패/실행 실패 시 fail-open(continue: true)
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
- * - codex 런타임에서 실행되는 스크립트 출력을 Claude Hook schema로 정규화
7
- * - continue 누락 또는 codex 특화 판정 필드(approved/decision) 대응
8
- * - 파싱 실패/실행 실패 시 fail-open(continue: true)
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
- function parseDecision(raw) {
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 = normalizeOutput(parsed, input);
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
+ }