@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,283 @@
1
+ /**
2
+ * BehavioralParityScenario harness — Multi-Host Core Design §10 우선순위 4
3
+ *
4
+ * "Claude 와 Codex 양쪽에서 같은 입력을 흘려보냈을 때 evidence 가 의미적으로 같다" 를
5
+ * 검증하는 골격. P4 단계에서는 *projection 사영 후 등가성* 만 검증한다 — 실 모델 호출은
6
+ * P6 (실 Codex CLI) 트랙.
7
+ *
8
+ * 본 harness 가 verify 하는 것:
9
+ * 1. 같은 forgen hook 입력에 대해 양쪽 host 의 raw 출력을 사영하면 의미 동치한 객체가 된다.
10
+ * 2. 사영 결과가 1원칙 (Claude reference) 의 행동 의도와 일치한다.
11
+ *
12
+ * verify 하지 않는 것 (P6 별도 트랙):
13
+ * - 실제 Codex 모델이 같은 prompt 에 같은 행동을 보이는지
14
+ * - 실제 Claude 모델과의 동작 동등성
15
+ */
16
+ import { equal as deepEqual } from 'node:assert/strict';
17
+ import { getProjection } from './projection.js';
18
+ function pickPath(obj, dotted) {
19
+ const parts = dotted.split('.');
20
+ let cur = obj;
21
+ for (const p of parts) {
22
+ if (cur && typeof cur === 'object' && p in cur) {
23
+ cur = cur[p];
24
+ }
25
+ else {
26
+ return undefined;
27
+ }
28
+ }
29
+ return cur;
30
+ }
31
+ function valuesSemanticEqual(a, b) {
32
+ try {
33
+ deepEqual(a, b);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ export function runScenario(scenario) {
41
+ const projected = {
42
+ claude: getProjection('claude')(scenario.hostRaw.claude, scenario.input),
43
+ codex: getProjection('codex')(scenario.hostRaw.codex, scenario.input),
44
+ };
45
+ const diffs = [];
46
+ for (const key of scenario.compareKeys) {
47
+ const cv = pickPath(projected.claude, key);
48
+ const xv = pickPath(projected.codex, key);
49
+ if (!valuesSemanticEqual(cv, xv)) {
50
+ diffs.push({ key, claude: cv, codex: xv });
51
+ }
52
+ }
53
+ return {
54
+ scenarioId: scenario.id,
55
+ intent: scenario.intent,
56
+ passed: diffs.length === 0,
57
+ diffs,
58
+ projected,
59
+ };
60
+ }
61
+ /**
62
+ * P4 1차 시나리오 corpus — Trust Layer 7 의도 중 hook 출력으로 직접 관측 가능한 5종.
63
+ * (`forge-loop-state-inject` 는 inject-context 의 특수 케이스, `self-evidence-record` 는
64
+ * 파일 시스템 사이드이펙트라 본 corpus 가 아닌 별도 e2e 트랙.)
65
+ *
66
+ * P4 2차 추가 시나리오 (spec §10 우선순위 4 산출물, 2026-04-27):
67
+ * - forge-loop-m1-inject-stale : §17.1 fact 1 / M1 hook 1KB cap + stale tag
68
+ * - block-tool-use-pretool-ask : §9.0 block-tool-use row — Codex ask 값 동치
69
+ * - stop-hook-reentry-guard : §15 stop_hook_active=true 즉시 approve 경로
70
+ * - posttooluse-general-block-only : §9.0 secret-filter row partial — block decision 만 등가
71
+ * - suppress-output-equivalence : suppressOutput 동치성
72
+ */
73
+ export const SCENARIO_CORPUS = [
74
+ {
75
+ id: 'block-completion-stop',
76
+ intent: 'block-completion',
77
+ description: 'Stop hook 이 block + reason 으로 자동 continuation 트리거',
78
+ input: { hookEventName: 'Stop', stop_hook_active: false },
79
+ hostRaw: {
80
+ claude: { decision: 'block', reason: 'tests not yet executed' },
81
+ codex: { decision: 'block', reason: 'tests not yet executed' },
82
+ },
83
+ compareKeys: [
84
+ 'continue',
85
+ 'hookSpecificOutput.permissionDecision',
86
+ ],
87
+ },
88
+ {
89
+ id: 'block-tool-use-pretool-deny',
90
+ intent: 'block-tool-use',
91
+ description: 'PreToolUse 가 permissionDecision:deny + reason 으로 도구 차단',
92
+ input: { hookEventName: 'PreToolUse', tool_name: 'Bash' },
93
+ hostRaw: {
94
+ claude: {
95
+ hookSpecificOutput: {
96
+ hookEventName: 'PreToolUse',
97
+ permissionDecision: 'deny',
98
+ permissionDecisionReason: 'rm -rf / matched',
99
+ },
100
+ },
101
+ codex: {
102
+ hookSpecificOutput: {
103
+ hookEventName: 'PreToolUse',
104
+ permissionDecision: 'deny',
105
+ permissionDecisionReason: 'rm -rf / matched',
106
+ },
107
+ },
108
+ },
109
+ compareKeys: [
110
+ 'hookSpecificOutput.permissionDecision',
111
+ 'hookSpecificOutput.permissionDecisionReason',
112
+ ],
113
+ },
114
+ {
115
+ id: 'inject-context-session-start',
116
+ intent: 'inject-context',
117
+ description: 'SessionStart 가 additionalContext 로 forge-loop state 주입',
118
+ input: { hookEventName: 'SessionStart' },
119
+ hostRaw: {
120
+ claude: {
121
+ hookSpecificOutput: {
122
+ hookEventName: 'SessionStart',
123
+ additionalContext: '<forge-loop-state>...</forge-loop-state>',
124
+ },
125
+ },
126
+ codex: {
127
+ hookSpecificOutput: {
128
+ hookEventName: 'SessionStart',
129
+ additionalContext: '<forge-loop-state>...</forge-loop-state>',
130
+ },
131
+ },
132
+ },
133
+ compareKeys: [
134
+ 'continue',
135
+ 'hookSpecificOutput.additionalContext',
136
+ ],
137
+ },
138
+ {
139
+ id: 'observe-only-non-allowlist',
140
+ intent: 'observe-only',
141
+ description: 'ALLOW-LIST 외 hook 이 deny 시도 시 approve 강등',
142
+ input: { hookEventName: 'PreToolUse' },
143
+ hostRaw: {
144
+ claude: { continue: true },
145
+ codex: { continue: true },
146
+ },
147
+ compareKeys: ['continue'],
148
+ },
149
+ {
150
+ id: 'secret-filter-pretooluse-block',
151
+ intent: 'secret-filter',
152
+ description: 'API 키 노출 차단 — PreToolUse 가드 (양쪽 동일 경로)',
153
+ input: { hookEventName: 'PreToolUse', tool_name: 'Bash' },
154
+ hostRaw: {
155
+ claude: {
156
+ hookSpecificOutput: {
157
+ hookEventName: 'PreToolUse',
158
+ permissionDecision: 'deny',
159
+ permissionDecisionReason: 'API_KEY=... matched',
160
+ },
161
+ },
162
+ codex: {
163
+ hookSpecificOutput: {
164
+ hookEventName: 'PreToolUse',
165
+ permissionDecision: 'deny',
166
+ permissionDecisionReason: 'API_KEY=... matched',
167
+ },
168
+ },
169
+ },
170
+ compareKeys: [
171
+ 'hookSpecificOutput.permissionDecision',
172
+ 'hookSpecificOutput.permissionDecisionReason',
173
+ ],
174
+ },
175
+ // ── P4 2차 추가 시나리오 ────────────────────────────────────────────────────
176
+ {
177
+ id: 'forge-loop-m1-inject-stale',
178
+ intent: 'forge-loop-state-inject',
179
+ description: 'SessionStart 가 stale="true" 태그 포함 forge-loop-state 를 1KB cap 내에서 inject (spec §17.1 fact 1 / M1 hook)',
180
+ input: { hookEventName: 'SessionStart' },
181
+ hostRaw: {
182
+ claude: {
183
+ hookSpecificOutput: {
184
+ hookEventName: 'SessionStart',
185
+ additionalContext: '<forge-loop-state stale="true">{"phase":"M1","lastCompletedAt":"2026-04-26T22:00:00Z"}</forge-loop-state>',
186
+ },
187
+ },
188
+ codex: {
189
+ hookSpecificOutput: {
190
+ hookEventName: 'SessionStart',
191
+ additionalContext: '<forge-loop-state stale="true">{"phase":"M1","lastCompletedAt":"2026-04-26T22:00:00Z"}</forge-loop-state>',
192
+ },
193
+ },
194
+ },
195
+ compareKeys: [
196
+ 'continue',
197
+ 'hookSpecificOutput.additionalContext',
198
+ ],
199
+ },
200
+ {
201
+ id: 'block-tool-use-pretool-ask',
202
+ intent: 'block-tool-use',
203
+ description: 'Codex PreToolUse permissionDecision:"ask" 가 Claude ask 값과 의미 동치 — forgen denyOrObserve 의 ask 의도 (spec §9.0 block-tool-use row 메모)',
204
+ input: { hookEventName: 'PreToolUse', tool_name: 'Bash' },
205
+ hostRaw: {
206
+ claude: {
207
+ hookSpecificOutput: {
208
+ hookEventName: 'PreToolUse',
209
+ permissionDecision: 'ask',
210
+ permissionDecisionReason: 'requires human confirmation',
211
+ },
212
+ },
213
+ codex: {
214
+ hookSpecificOutput: {
215
+ hookEventName: 'PreToolUse',
216
+ permissionDecision: 'ask',
217
+ permissionDecisionReason: 'requires human confirmation',
218
+ },
219
+ },
220
+ },
221
+ compareKeys: [
222
+ 'hookSpecificOutput.permissionDecision',
223
+ 'hookSpecificOutput.permissionDecisionReason',
224
+ ],
225
+ },
226
+ {
227
+ id: 'stop-hook-reentry-guard',
228
+ intent: 'block-completion',
229
+ description: 'stop_hook_active=true 시 Stop hook 이 즉시 approve(continue:true) 로 빠지는 재진입 가드 (spec §15)',
230
+ input: { hookEventName: 'Stop', stop_hook_active: true },
231
+ hostRaw: {
232
+ claude: { continue: true },
233
+ codex: { continue: true },
234
+ },
235
+ compareKeys: ['continue'],
236
+ },
237
+ {
238
+ id: 'posttooluse-general-block-only',
239
+ intent: 'secret-filter',
240
+ description: 'PostToolUse 일반 tool — Codex 는 redact 미보장이므로 block decision 만 양쪽 등가. updatedMCPToolOutput 없음 (spec §9.0 secret-filter row partial / §18.2 fact 4)',
241
+ input: { hookEventName: 'PostToolUse', tool_name: 'Bash' },
242
+ hostRaw: {
243
+ claude: {
244
+ hookSpecificOutput: {
245
+ hookEventName: 'PostToolUse',
246
+ permissionDecision: 'deny',
247
+ permissionDecisionReason: 'SECRET=... detected in tool output',
248
+ },
249
+ },
250
+ codex: {
251
+ hookSpecificOutput: {
252
+ hookEventName: 'PostToolUse',
253
+ permissionDecision: 'deny',
254
+ permissionDecisionReason: 'SECRET=... detected in tool output',
255
+ },
256
+ },
257
+ },
258
+ compareKeys: [
259
+ 'hookSpecificOutput.permissionDecision',
260
+ 'hookSpecificOutput.permissionDecisionReason',
261
+ ],
262
+ },
263
+ {
264
+ id: 'suppress-output-equivalence',
265
+ intent: 'observe-only',
266
+ description: 'PostToolUse suppressOutput=true 가 양쪽 host 에서 동치로 사영됨',
267
+ input: { hookEventName: 'PostToolUse', tool_name: 'Read' },
268
+ hostRaw: {
269
+ claude: {
270
+ continue: true,
271
+ suppressOutput: true,
272
+ },
273
+ codex: {
274
+ continue: true,
275
+ suppressOutput: true,
276
+ },
277
+ },
278
+ compareKeys: [
279
+ 'continue',
280
+ 'suppressOutput',
281
+ ],
282
+ },
283
+ ];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ProjectToClaudeEvent — Multi-Host Core Design §5.2 / §10 우선순위 2
3
+ *
4
+ * Codex (또는 미래의 다른 host) 의 hook 출력을 Claude Hook schema 로 사영하는
5
+ * 정식 계약. spec §17.4 / §18.4 에서 검증되었듯 schema-level 에서 거의 identity 이므로
6
+ * 본 함수는 *형식 정규화* 만 책임진다.
7
+ *
8
+ * - 입력: host-native 출력(JSON object, plaintext, exit-code 등은 별도 layer 에서 처리)
9
+ * - 출력: Claude HookEventOutput 동치 — `continue`, `hookSpecificOutput.permissionDecision`, etc.
10
+ * - 실패 정책: parse 실패 / 알 수 없는 형식 → fail-open (`{ continue: true }`)
11
+ *
12
+ * 본 모듈은 host 측 표면을 *모르고*, 받은 raw 의 형태만으로 동작한다 (1원칙: core 는 Claude
13
+ * semantics 알아도 됨, Codex 표면 모름). 즉 Codex CLI 의 stdout 을 받아 코어가 학습 가능한
14
+ * Claude 형 객체로 변환만 한다.
15
+ */
16
+ import type { HookEventInput, HookEventOutput } from '../core/types.js';
17
+ import type { HostId } from '../core/trust-layer-intent.js';
18
+ export type ProjectToClaudeEvent = (raw: unknown, input: HookEventInput) => HookEventOutput;
19
+ /**
20
+ * Codex 출력 → Claude HookEventOutput 정식 사영.
21
+ *
22
+ * spec §18.2 fact #3 에 따라 PreToolUse 의 *이중* decision 필드 중 어댑터는
23
+ * `hookSpecificOutput.permissionDecision` 을 우선한다. 본 함수가 그 규약을 강제.
24
+ */
25
+ export declare const projectCodexToClaude: ProjectToClaudeEvent;
26
+ /**
27
+ * Claude 어댑터의 사영. 1원칙(Claude reference) + spec §18.4 (Codex hooks.json schema 동일성)
28
+ * 에 따라 본 함수는 `projectCodexToClaude` 와 *같은 normalize 로직* 을 공유한다.
29
+ * 둘 다 같은 canonical Claude HookEventOutput 형식을 만든다.
30
+ *
31
+ * (왜 두 함수를 별도 export 하는가: 향후 schema 가 다른 host 가 추가될 때 본 binding 만
32
+ * 교체하면 되도록 — `getProjection(host)` 가 단일 진입점.)
33
+ */
34
+ export declare const projectClaudeToClaude: ProjectToClaudeEvent;
35
+ export declare function getProjection(host: HostId): ProjectToClaudeEvent;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * ProjectToClaudeEvent — Multi-Host Core Design §5.2 / §10 우선순위 2
3
+ *
4
+ * Codex (또는 미래의 다른 host) 의 hook 출력을 Claude Hook schema 로 사영하는
5
+ * 정식 계약. spec §17.4 / §18.4 에서 검증되었듯 schema-level 에서 거의 identity 이므로
6
+ * 본 함수는 *형식 정규화* 만 책임진다.
7
+ *
8
+ * - 입력: host-native 출력(JSON object, plaintext, exit-code 등은 별도 layer 에서 처리)
9
+ * - 출력: Claude HookEventOutput 동치 — `continue`, `hookSpecificOutput.permissionDecision`, etc.
10
+ * - 실패 정책: parse 실패 / 알 수 없는 형식 → fail-open (`{ continue: true }`)
11
+ *
12
+ * 본 모듈은 host 측 표면을 *모르고*, 받은 raw 의 형태만으로 동작한다 (1원칙: core 는 Claude
13
+ * semantics 알아도 됨, Codex 표면 모름). 즉 Codex CLI 의 stdout 을 받아 코어가 학습 가능한
14
+ * Claude 형 객체로 변환만 한다.
15
+ */
16
+ function parseDecision(raw) {
17
+ if (typeof raw === 'boolean')
18
+ return { continueFlag: raw };
19
+ if (typeof raw === 'string') {
20
+ const normalized = raw.toLowerCase();
21
+ if (normalized === 'continue')
22
+ return { continueFlag: true };
23
+ if (normalized === 'stop' ||
24
+ normalized === 'deny' ||
25
+ normalized === 'reject' ||
26
+ normalized === 'block') {
27
+ return { continueFlag: false, permissionDecision: normalized };
28
+ }
29
+ return { continueFlag: true };
30
+ }
31
+ if (typeof raw !== 'object' || raw === null)
32
+ return { continueFlag: true };
33
+ const decision = raw.decision;
34
+ if (typeof decision === 'string') {
35
+ const normalized = decision.toLowerCase();
36
+ if (normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
37
+ return { continueFlag: false, permissionDecision: normalized };
38
+ }
39
+ if (normalized === 'ask' || normalized === 'prompt' || normalized === 'confirm') {
40
+ return { continueFlag: true, permissionDecision: normalized };
41
+ }
42
+ }
43
+ if (typeof raw.approved === 'boolean') {
44
+ const approved = raw.approved;
45
+ return approved
46
+ ? { continueFlag: true, permissionDecision: raw.decision || 'approve' }
47
+ : { continueFlag: false, permissionDecision: 'deny' };
48
+ }
49
+ if (typeof raw.continue === 'boolean') {
50
+ return { continueFlag: raw.continue };
51
+ }
52
+ return { continueFlag: true };
53
+ }
54
+ /**
55
+ * Codex 출력 → Claude HookEventOutput 정식 사영.
56
+ *
57
+ * spec §18.2 fact #3 에 따라 PreToolUse 의 *이중* decision 필드 중 어댑터는
58
+ * `hookSpecificOutput.permissionDecision` 을 우선한다. 본 함수가 그 규약을 강제.
59
+ */
60
+ export const projectCodexToClaude = (raw, input) => {
61
+ const result = { continue: true };
62
+ const decision = parseDecision(raw);
63
+ result.continue = decision.continueFlag;
64
+ if (typeof raw === 'object' && raw !== null) {
65
+ const payload = raw;
66
+ if (typeof payload.continue === 'boolean')
67
+ result.continue = payload.continue;
68
+ if (typeof payload.systemMessage === 'string')
69
+ result.systemMessage = payload.systemMessage;
70
+ if (typeof payload.suppressOutput === 'boolean')
71
+ result.suppressOutput = payload.suppressOutput;
72
+ if (typeof payload.hookSpecificOutput === 'object' && payload.hookSpecificOutput !== null) {
73
+ result.hookSpecificOutput = { ...payload.hookSpecificOutput };
74
+ }
75
+ // top-level decision (Codex 의 PreToolUse 이중 decision 중 legacy 측 또는 Stop/Post 의 단일 측)
76
+ // 이 있고, hookSpecificOutput.permissionDecision 이 비었을 때만 보존.
77
+ if (typeof payload.decision === 'string' &&
78
+ !(result.hookSpecificOutput && 'permissionDecision' in result.hookSpecificOutput)) {
79
+ result.hookSpecificOutput = {
80
+ ...(result.hookSpecificOutput ?? {}),
81
+ permissionDecision: payload.decision,
82
+ };
83
+ }
84
+ }
85
+ const eventName = result.hookSpecificOutput?.hookEventName ?? input.hookEventName ?? input.event;
86
+ if (eventName) {
87
+ result.hookSpecificOutput = {
88
+ hookEventName: eventName,
89
+ ...(result.hookSpecificOutput ?? {}),
90
+ };
91
+ }
92
+ if (!result.continue && !result.hookSpecificOutput?.permissionDecision) {
93
+ if (decision.permissionDecision) {
94
+ result.hookSpecificOutput = {
95
+ ...(result.hookSpecificOutput ?? {}),
96
+ permissionDecision: decision.permissionDecision,
97
+ };
98
+ }
99
+ else {
100
+ result.hookSpecificOutput = {
101
+ ...(result.hookSpecificOutput ?? {}),
102
+ permissionDecision: 'deny',
103
+ };
104
+ }
105
+ }
106
+ return result;
107
+ };
108
+ /**
109
+ * Claude 어댑터의 사영. 1원칙(Claude reference) + spec §18.4 (Codex hooks.json schema 동일성)
110
+ * 에 따라 본 함수는 `projectCodexToClaude` 와 *같은 normalize 로직* 을 공유한다.
111
+ * 둘 다 같은 canonical Claude HookEventOutput 형식을 만든다.
112
+ *
113
+ * (왜 두 함수를 별도 export 하는가: 향후 schema 가 다른 host 가 추가될 때 본 binding 만
114
+ * 교체하면 되도록 — `getProjection(host)` 가 단일 진입점.)
115
+ */
116
+ export const projectClaudeToClaude = (raw, input) => projectCodexToClaude(raw, input);
117
+ const PROJECTIONS = {
118
+ claude: projectClaudeToClaude,
119
+ codex: projectCodexToClaude,
120
+ };
121
+ export function getProjection(host) {
122
+ const fn = PROJECTIONS[host];
123
+ if (!fn)
124
+ throw new Error(`No ProjectToClaudeEvent registered for host: ${host}`);
125
+ return fn;
126
+ }
@@ -12,6 +12,17 @@ if (!process.env.FORGEN_CWD && !process.env.COMPOUND_CWD) {
12
12
  process.env.FORGEN_CWD = process.cwd();
13
13
  process.env.COMPOUND_CWD = process.cwd(); // legacy compat
14
14
  }
15
+ // Multi-host evidence attribution (spec §10-5):
16
+ // 호출 host 가 spawn 시 `--host=<claude|codex>` 인자를 넘기면 본 process 의
17
+ // FORGEN_HOST env 를 set 한다. evidence-store 의 detectHost() 가 이 env 를 읽어
18
+ // correction-record 가 정확한 host 로 박제되게 한다. 미지정 시 기존 fallback
19
+ // (FORGEN_HOST > CODEX_HOME 추론 > 'claude') 그대로.
20
+ const hostArg = process.argv
21
+ .find((a) => a === '--host=claude' || a === '--host=codex')
22
+ ?.split('=')[1];
23
+ if (hostArg === 'claude' || hostArg === 'codex') {
24
+ process.env.FORGEN_HOST = hostArg;
25
+ }
15
26
  const INSTRUCTIONS = [
16
27
  'Forgen compound knowledge — accumulated patterns and solutions from past sessions.',
17
28
  '',
package/dist/mcp/tools.js CHANGED
@@ -441,4 +441,51 @@ export function registerTools(server) {
441
441
  }],
442
442
  };
443
443
  });
444
+ // ── invoke-agent (P3-4/P3-5) ─────────────────────────────────────────
445
+ //
446
+ // forgen 의 13 sub-agents (assets/claude/agents/*.md) 를 MCP 도구로 노출.
447
+ // Claude 의 Task tool 동치를 host 무관 형태로 제공:
448
+ // - Claude main agent: Task tool 우선 사용 (native, faster)
449
+ // - Codex main agent: invoke-agent MCP 도구가 codex exec --json 으로 child spawn
450
+ // 실 작업: assets/claude/agents/<name>.md 의 system prompt 를 prefix 로,
451
+ // 사용자 task 를 prompt 로 child host CLI 호출 → 응답 반환.
452
+ server.registerTool('invoke-agent', {
453
+ description: [
454
+ 'Invoke a forgen sub-agent (specialized prompt) on a task and return its summary.',
455
+ 'Use when delegation is helpful — e.g., focused exploration (ch-explore), implementation (ch-executor),',
456
+ 'critical review (ch-critic), test writing (ch-test-engineer).',
457
+ 'The sub-agent runs in a separate child process with isolated context and returns just the result.',
458
+ 'Available agent names match files under assets/claude/agents/ (e.g., ch-explore, ch-executor, ch-critic).',
459
+ ].join('\n'),
460
+ inputSchema: {
461
+ agent_name: z.string().describe('Sub-agent identifier (e.g., "ch-explore", "ch-executor")'),
462
+ task: z.string().describe('What you want the sub-agent to do — natural language task description'),
463
+ timeout_ms: z.number().int().positive().max(300000).optional()
464
+ .describe('Child timeout in ms (default 60s, max 5m)'),
465
+ },
466
+ annotations: { readOnlyHint: false },
467
+ }, async ({ agent_name, task, timeout_ms }) => {
468
+ try {
469
+ const { invokeAgent } = await import('../host/invoke-agent.js');
470
+ const result = await invokeAgent({ agentName: agent_name, task, timeoutMs: timeout_ms });
471
+ const lines = [
472
+ `[invoke-agent] ${agent_name} (${result.host}, ${result.durationMs}ms)`,
473
+ '',
474
+ result.summary,
475
+ ];
476
+ if (result.usage) {
477
+ lines.push('', `Usage: input=${result.usage.input_tokens ?? '?'} output=${result.usage.output_tokens ?? '?'}`);
478
+ }
479
+ return {
480
+ content: [{ type: 'text', text: lines.join('\n') }],
481
+ };
482
+ }
483
+ catch (e) {
484
+ const msg = e instanceof Error ? e.message : String(e);
485
+ return {
486
+ content: [{ type: 'text', text: `[invoke-agent] error: ${msg}` }],
487
+ isError: true,
488
+ };
489
+ }
490
+ });
444
491
  }
@@ -10,10 +10,13 @@
10
10
  * - launch context(런타임 + 정제된 args)를 단일 타입으로 통일
11
11
  * - CLI/fgx에서 수집한 런타임 값을 Harness, Spawn, Hook Generator에 일관되게 전달
12
12
  */
13
- import { type LaunchContext } from '../core/types.js';
13
+ import type { LaunchContext } from '../core/types.js';
14
14
  /**
15
15
  * CLI 인자를 파싱해 런타임 결정 + 런타임 플래그 제거
16
- * - --runtime codex
17
- * - --runtime=codex
16
+ * 우선순위 (높음→낮음):
17
+ * 1. --runtime <claude|codex> flag
18
+ * 2. FORGEN_RUNTIME env
19
+ * 3. profile.default_host (P1-4)
20
+ * 4. 'claude' fallback (legacy 호환)
18
21
  */
19
22
  export declare function resolveLaunchContext(args: string[]): LaunchContext;
@@ -10,6 +10,8 @@
10
10
  * - launch context(런타임 + 정제된 args)를 단일 타입으로 통일
11
11
  * - CLI/fgx에서 수집한 런타임 값을 Harness, Spawn, Hook Generator에 일관되게 전달
12
12
  */
13
+ import { createRequire } from 'node:module';
14
+ const localRequire = createRequire(import.meta.url);
13
15
  /** 런타임 정규화: 외부 문자열을 내부 enum으로 변환 */
14
16
  function parseRuntime(raw) {
15
17
  if (!raw)
@@ -24,17 +26,44 @@ function parseRuntime(raw) {
24
26
  }
25
27
  }
26
28
  const DEFAULT_RUNTIME = 'claude';
29
+ /**
30
+ * profile.default_host 를 읽어 runtime 결정.
31
+ * 'ask' 면 별도 prompt 책임 — 본 함수는 default 'claude' 로 fallback (caller 가 --ask 처리).
32
+ * profile-store import 가 cycle 위험이라 require 로 lazy.
33
+ */
34
+ function readProfileDefaultRuntime() {
35
+ try {
36
+ const mod = localRequire('../store/profile-store.js');
37
+ const stored = mod.getDefaultHost?.();
38
+ if (stored === 'claude' || stored === 'codex')
39
+ return stored;
40
+ return null; // 'ask' 또는 미설정
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
27
46
  /**
28
47
  * CLI 인자를 파싱해 런타임 결정 + 런타임 플래그 제거
29
- * - --runtime codex
30
- * - --runtime=codex
48
+ * 우선순위 (높음→낮음):
49
+ * 1. --runtime <claude|codex> flag
50
+ * 2. FORGEN_RUNTIME env
51
+ * 3. profile.default_host (P1-4)
52
+ * 4. 'claude' fallback (legacy 호환)
31
53
  */
32
54
  export function resolveLaunchContext(args) {
33
55
  const runtimeFromEnv = parseRuntime(process.env.FORGEN_RUNTIME);
56
+ const runtimeFromProfile = runtimeFromEnv ? null : readProfileDefaultRuntime();
57
+ const initial = runtimeFromEnv ?? runtimeFromProfile ?? DEFAULT_RUNTIME;
58
+ const initialSource = runtimeFromEnv
59
+ ? 'env'
60
+ : runtimeFromProfile
61
+ ? 'profile'
62
+ : 'default';
34
63
  const result = {
35
- runtime: runtimeFromEnv ?? DEFAULT_RUNTIME,
64
+ runtime: initial,
36
65
  args: [],
37
- runtimeSource: runtimeFromEnv ? 'env' : 'default',
66
+ runtimeSource: initialSource,
38
67
  };
39
68
  for (let i = 0; i < args.length; i += 1) {
40
69
  const arg = args[i];
@@ -14,6 +14,7 @@ export declare function createEvidence(params: {
14
14
  candidate_rule_refs?: string[];
15
15
  confidence: number;
16
16
  raw_payload?: Record<string, unknown>;
17
+ host?: 'claude' | 'codex';
17
18
  }): Evidence;
18
19
  export declare function saveEvidence(evidence: Evidence): void;
19
20
  /**
@@ -17,6 +17,24 @@ import { appendLifecycleEvents } from '../engine/lifecycle/meta-reclassifier.js'
17
17
  function evidencePath(evidenceId) {
18
18
  return path.join(ME_BEHAVIOR, `${evidenceId}.json`);
19
19
  }
20
+ /**
21
+ * 현재 세션이 어느 host 에서 실행되는지 추론 (Multi-Host §4.2).
22
+ * 1) explicit `params.host`
23
+ * 2) env var `FORGEN_HOST` (e2e 격리용)
24
+ * 3) `runtime` env hint
25
+ * 4) Codex CLI 흔적 (`CODEX_HOME` 또는 `CODEX_SANDBOX_NETWORK_DISABLED`)
26
+ * 5) default 'claude' (1원칙)
27
+ */
28
+ function detectHost(explicit) {
29
+ if (explicit)
30
+ return explicit;
31
+ const fromEnv = process.env.FORGEN_HOST;
32
+ if (fromEnv === 'claude' || fromEnv === 'codex')
33
+ return fromEnv;
34
+ if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX_NETWORK_DISABLED)
35
+ return 'codex';
36
+ return 'claude';
37
+ }
20
38
  export function createEvidence(params) {
21
39
  return {
22
40
  evidence_id: crypto.randomUUID(),
@@ -29,6 +47,7 @@ export function createEvidence(params) {
29
47
  candidate_rule_refs: params.candidate_rule_refs ?? [],
30
48
  confidence: params.confidence,
31
49
  raw_payload: params.raw_payload ?? {},
50
+ host: detectHost(params.host),
32
51
  };
33
52
  }
34
53
  /** TEST-4 / RC4: behavior_observation 의 summary 가 의미있는 내용을 담아야 분석 가능. */
@@ -84,8 +103,19 @@ export function appendEvidence(evidence) {
84
103
  return { saved: true, t1_events: 0 };
85
104
  }
86
105
  }
106
+ /**
107
+ * 기존 evidence 에 host 필드가 없으면 'claude' 로 backfill (Multi-Host §4.2 마이그레이션 정책).
108
+ * 새 multi-host 도입 이전 데이터는 모두 Claude 에서 발생했음 — 이 backfill 은 무손실.
109
+ */
110
+ function backfillHost(ev) {
111
+ if (!ev)
112
+ return ev;
113
+ if (ev.host === 'claude' || ev.host === 'codex')
114
+ return ev;
115
+ return { ...ev, host: 'claude' };
116
+ }
87
117
  export function loadEvidence(evidenceId) {
88
- return safeReadJSON(evidencePath(evidenceId), null);
118
+ return backfillHost(safeReadJSON(evidencePath(evidenceId), null));
89
119
  }
90
120
  export function loadAllEvidence() {
91
121
  if (!fs.existsSync(ME_BEHAVIOR))
@@ -95,8 +125,9 @@ export function loadAllEvidence() {
95
125
  if (!file.endsWith('.json'))
96
126
  continue;
97
127
  const ev = safeReadJSON(path.join(ME_BEHAVIOR, file), null);
98
- if (ev)
99
- items.push(ev);
128
+ const filled = backfillHost(ev);
129
+ if (filled)
130
+ items.push(filled);
100
131
  }
101
132
  return items;
102
133
  }