@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.
Files changed (151) hide show
  1. package/.claude-plugin/plugin.json +5 -5
  2. package/CHANGELOG.md +267 -15
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.ja.md +17 -9
  5. package/README.ko.md +34 -12
  6. package/README.md +65 -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/{commands → assets/claude/commands}/calibrate.md +4 -3
  13. package/{commands → assets/claude/commands}/retro.md +2 -2
  14. package/assets/demo/01-install.gif +0 -0
  15. package/assets/demo/01-install.tape +54 -0
  16. package/assets/demo/02-compound-learning.gif +0 -0
  17. package/assets/demo/02-compound-learning.tape +50 -0
  18. package/assets/demo/03-forge-personalization.gif +0 -0
  19. package/assets/demo/03-forge-personalization.tape +64 -0
  20. package/assets/demo/before-after.gif +0 -0
  21. package/assets/demo/before-after.tape +98 -0
  22. package/assets/demo-preview.svg +96 -0
  23. package/assets/icon.png +0 -0
  24. package/{hooks → assets/shared}/hook-registry.json +2 -1
  25. package/dist/checks/_shared/text-sanitizer.d.ts +21 -0
  26. package/dist/checks/_shared/text-sanitizer.js +60 -0
  27. package/dist/checks/dangerous-response-pattern.d.ts +32 -0
  28. package/dist/checks/dangerous-response-pattern.js +65 -0
  29. package/dist/checks/fact-vs-agreement.js +25 -1
  30. package/dist/cli.js +78 -6
  31. package/dist/core/auto-compound-runner.js +90 -39
  32. package/dist/core/behavior-classifier.d.ts +28 -0
  33. package/dist/core/behavior-classifier.js +46 -0
  34. package/dist/core/dashboard.d.ts +7 -0
  35. package/dist/core/dashboard.js +32 -0
  36. package/dist/core/doctor.js +92 -0
  37. package/dist/core/git-stats.d.ts +36 -0
  38. package/dist/core/git-stats.js +79 -0
  39. package/dist/core/harness.d.ts +1 -1
  40. package/dist/core/harness.js +27 -20
  41. package/dist/core/host-detect.d.ts +42 -0
  42. package/dist/core/host-detect.js +68 -0
  43. package/dist/core/installer.js +2 -2
  44. package/dist/core/migrate-cli.d.ts +1 -0
  45. package/dist/core/migrate-cli.js +19 -0
  46. package/dist/core/migrate-evidence-host.d.ts +36 -0
  47. package/dist/core/migrate-evidence-host.js +49 -0
  48. package/dist/core/settings-injector.js +4 -2
  49. package/dist/core/spawn.d.ts +1 -1
  50. package/dist/core/spawn.js +4 -11
  51. package/dist/core/stats-cli.js +12 -0
  52. package/dist/core/trust-layer-intent.d.ts +35 -0
  53. package/dist/core/trust-layer-intent.js +30 -0
  54. package/dist/core/types.d.ts +1 -1
  55. package/dist/engine/compound-extractor.js +7 -9
  56. package/dist/engine/learn-cli.js +4 -2
  57. package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
  58. package/dist/engine/lifecycle/bypass-detector.js +57 -5
  59. package/dist/fgx.js +2 -1
  60. package/dist/forge/evidence-processor.js +12 -0
  61. package/dist/forge/onboarding.d.ts +3 -2
  62. package/dist/forge/onboarding.js +3 -2
  63. package/dist/hooks/db-guard.js +3 -3
  64. package/dist/hooks/forge-loop-progress.d.ts +9 -0
  65. package/dist/hooks/forge-loop-progress.js +38 -0
  66. package/dist/hooks/hook-registry.js +1 -1
  67. package/dist/hooks/hooks-generator.d.ts +15 -1
  68. package/dist/hooks/hooks-generator.js +18 -16
  69. package/dist/hooks/keyword-detector.js +1 -1
  70. package/dist/hooks/post-tool-use.d.ts +1 -1
  71. package/dist/hooks/post-tool-use.js +13 -4
  72. package/dist/hooks/pre-compact.js +1 -1
  73. package/dist/hooks/pre-tool-use.js +4 -4
  74. package/dist/hooks/rate-limiter.js +2 -2
  75. package/dist/hooks/session-recovery.js +11 -0
  76. package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
  77. package/dist/hooks/shared/blocking-allowlist.js +38 -0
  78. package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
  79. package/dist/hooks/shared/forge-loop-state.js +116 -0
  80. package/dist/hooks/shared/hook-response.d.ts +18 -0
  81. package/dist/hooks/shared/hook-response.js +31 -0
  82. package/dist/hooks/skill-injector.js +1 -1
  83. package/dist/hooks/stop-guard.js +57 -25
  84. package/dist/host/capabilities-claude.d.ts +8 -0
  85. package/dist/host/capabilities-claude.js +46 -0
  86. package/dist/host/capabilities-codex.d.ts +11 -0
  87. package/dist/host/capabilities-codex.js +50 -0
  88. package/dist/host/capabilities-registry.d.ts +11 -0
  89. package/dist/host/capabilities-registry.js +30 -0
  90. package/dist/host/codex-adapter.d.ts +8 -5
  91. package/dist/host/codex-adapter.js +10 -82
  92. package/dist/host/codex-output-parser.d.ts +39 -0
  93. package/dist/host/codex-output-parser.js +75 -0
  94. package/dist/host/exec-host.d.ts +54 -0
  95. package/dist/host/exec-host.js +92 -0
  96. package/dist/host/host-runtime.d.ts +37 -0
  97. package/dist/host/host-runtime.js +51 -0
  98. package/dist/host/install-claude.d.ts +35 -0
  99. package/dist/host/install-claude.js +238 -0
  100. package/dist/host/install-codex.d.ts +44 -0
  101. package/dist/host/install-codex.js +276 -0
  102. package/dist/host/install-orchestrator.d.ts +34 -0
  103. package/dist/host/install-orchestrator.js +126 -0
  104. package/dist/host/invoke-agent.d.ts +27 -0
  105. package/dist/host/invoke-agent.js +115 -0
  106. package/dist/host/parity-harness.d.ts +62 -0
  107. package/dist/host/parity-harness.js +283 -0
  108. package/dist/host/projection.d.ts +35 -0
  109. package/dist/host/projection.js +126 -0
  110. package/dist/mcp/server.js +11 -0
  111. package/dist/mcp/tools.js +51 -0
  112. package/dist/renderer/rule-renderer.d.ts +1 -1
  113. package/dist/renderer/rule-renderer.js +73 -1
  114. package/dist/services/session.d.ts +6 -3
  115. package/dist/services/session.js +33 -4
  116. package/dist/store/compound-usage-store.d.ts +28 -0
  117. package/dist/store/compound-usage-store.js +59 -0
  118. package/dist/store/evidence-store.d.ts +1 -0
  119. package/dist/store/evidence-store.js +34 -3
  120. package/dist/store/host-mismatch.d.ts +42 -0
  121. package/dist/store/host-mismatch.js +65 -0
  122. package/dist/store/profile-store.d.ts +29 -0
  123. package/dist/store/profile-store.js +53 -0
  124. package/dist/store/types.d.ts +13 -0
  125. package/hooks/hooks.json +6 -1
  126. package/package.json +6 -4
  127. package/plugin.json +4 -4
  128. package/scripts/postinstall.js +100 -25
  129. package/skills/calibrate/SKILL.md +4 -3
  130. package/skills/retro/SKILL.md +2 -2
  131. /package/{agents → assets/claude/agents}/analyst.md +0 -0
  132. /package/{agents → assets/claude/agents}/architect.md +0 -0
  133. /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
  134. /package/{agents → assets/claude/agents}/critic.md +0 -0
  135. /package/{agents → assets/claude/agents}/debugger.md +0 -0
  136. /package/{agents → assets/claude/agents}/designer.md +0 -0
  137. /package/{agents → assets/claude/agents}/executor.md +0 -0
  138. /package/{agents → assets/claude/agents}/explore.md +0 -0
  139. /package/{agents → assets/claude/agents}/git-master.md +0 -0
  140. /package/{agents → assets/claude/agents}/planner.md +0 -0
  141. /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
  142. /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
  143. /package/{agents → assets/claude/agents}/verifier.md +0 -0
  144. /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
  145. /package/{commands → assets/claude/commands}/code-review.md +0 -0
  146. /package/{commands → assets/claude/commands}/compound.md +0 -0
  147. /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
  148. /package/{commands → assets/claude/commands}/docker.md +0 -0
  149. /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
  150. /package/{commands → assets/claude/commands}/learn.md +0 -0
  151. /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
@@ -136,6 +136,10 @@ export function registerTools(server) {
136
136
  }],
137
137
  };
138
138
  }
139
+ // D11 — compound usage signal: 사용자 reuse 신호를 한 줄 기록.
140
+ // mature 승격 정책의 입력 데이터 (정책은 별도 사이클).
141
+ const usageMod = await import('../store/compound-usage-store.js');
142
+ usageMod.recordUsage(result.name, 'mcp');
139
143
  const header = `# ${result.name}\n` +
140
144
  `Status: ${result.status} | Confidence: ${result.confidence.toFixed(2)} | Type: ${result.type} | Scope: ${result.scope}\n` +
141
145
  `Tags: ${result.tags.join(', ')}\n` +
@@ -441,4 +445,51 @@ export function registerTools(server) {
441
445
  }],
442
446
  };
443
447
  });
448
+ // ── invoke-agent (P3-4/P3-5) ─────────────────────────────────────────
449
+ //
450
+ // forgen 의 13 sub-agents (assets/claude/agents/*.md) 를 MCP 도구로 노출.
451
+ // Claude 의 Task tool 동치를 host 무관 형태로 제공:
452
+ // - Claude main agent: Task tool 우선 사용 (native, faster)
453
+ // - Codex main agent: invoke-agent MCP 도구가 codex exec --json 으로 child spawn
454
+ // 실 작업: assets/claude/agents/<name>.md 의 system prompt 를 prefix 로,
455
+ // 사용자 task 를 prompt 로 child host CLI 호출 → 응답 반환.
456
+ server.registerTool('invoke-agent', {
457
+ description: [
458
+ 'Invoke a forgen sub-agent (specialized prompt) on a task and return its summary.',
459
+ 'Use when delegation is helpful — e.g., focused exploration (ch-explore), implementation (ch-executor),',
460
+ 'critical review (ch-critic), test writing (ch-test-engineer).',
461
+ 'The sub-agent runs in a separate child process with isolated context and returns just the result.',
462
+ 'Available agent names match files under assets/claude/agents/ (e.g., ch-explore, ch-executor, ch-critic).',
463
+ ].join('\n'),
464
+ inputSchema: {
465
+ agent_name: z.string().describe('Sub-agent identifier (e.g., "ch-explore", "ch-executor")'),
466
+ task: z.string().describe('What you want the sub-agent to do — natural language task description'),
467
+ timeout_ms: z.number().int().positive().max(300000).optional()
468
+ .describe('Child timeout in ms (default 60s, max 5m)'),
469
+ },
470
+ annotations: { readOnlyHint: false },
471
+ }, async ({ agent_name, task, timeout_ms }) => {
472
+ try {
473
+ const { invokeAgent } = await import('../host/invoke-agent.js');
474
+ const result = await invokeAgent({ agentName: agent_name, task, timeoutMs: timeout_ms });
475
+ const lines = [
476
+ `[invoke-agent] ${agent_name} (${result.host}, ${result.durationMs}ms)`,
477
+ '',
478
+ result.summary,
479
+ ];
480
+ if (result.usage) {
481
+ lines.push('', `Usage: input=${result.usage.input_tokens ?? '?'} output=${result.usage.output_tokens ?? '?'}`);
482
+ }
483
+ return {
484
+ content: [{ type: 'text', text: lines.join('\n') }],
485
+ };
486
+ }
487
+ catch (e) {
488
+ const msg = e instanceof Error ? e.message : String(e);
489
+ return {
490
+ content: [{ type: 'text', text: `[invoke-agent] error: ${msg}` }],
491
+ isError: true,
492
+ };
493
+ }
494
+ });
444
495
  }
@@ -15,4 +15,4 @@ export interface RenderContext {
15
15
  include_pack_summary: boolean;
16
16
  }
17
17
  export declare const DEFAULT_CONTEXT: RenderContext;
18
- export declare function renderRules(rules: Rule[], state: SessionEffectiveState, _profile: Profile, ctx?: RenderContext): string;
18
+ export declare function renderRules(rules: Rule[], state: SessionEffectiveState, profile: Profile, ctx?: RenderContext): string;
@@ -91,8 +91,74 @@ function communicationPackRules(pack) {
91
91
  case '균형형': return s.commBalanced;
92
92
  }
93
93
  }
94
+ // ── Facet-driven rules ──
95
+ // 4축 facet 값의 양 극단(≤0.15, ≥0.85)에서만 추가 규칙을 emit. 중간 값은
96
+ // pack 기본 규칙으로 충분하다고 본다 — 12-bucket pack lookup 위에 *연속 값
97
+ // 차별화*를 얇게 얹는 설계.
98
+ //
99
+ // Threshold 정당화: 0.5 가 default 이고 자동 갱신은 ±0.1 단위 (auto-compound-runner).
100
+ // 0.15/0.85 = 3 단계 강한 신호 누적 후에야 발화 → 노이즈에 둔감.
101
+ const FACET_HIGH = 0.85;
102
+ const FACET_LOW = 0.15;
103
+ function facetDrivenRules(profile) {
104
+ const out = [];
105
+ const q = profile.axes.quality_safety.facets;
106
+ const a = profile.axes.autonomy.facets;
107
+ const j = profile.axes.judgment_philosophy.facets;
108
+ const c = profile.axes.communication_style.facets;
109
+ // Quality
110
+ if (q.verification_depth >= FACET_HIGH) {
111
+ out.push({ section: 'How To Validate', rule: '완료 선언 전 실행 로그 / 테스트 결과 / e2e 증거를 응답에 첨부' });
112
+ }
113
+ if (q.stop_threshold >= FACET_HIGH) {
114
+ out.push({ section: 'Working Defaults', rule: '첫 실패 시 즉시 멈추고 진단 — 무진단 재시도 금지' });
115
+ }
116
+ if (q.change_conservatism >= FACET_HIGH) {
117
+ out.push({ section: 'Working Defaults', rule: '의뢰 범위 외 리팩토링/일반화 금지 — 최소 diff 우선' });
118
+ }
119
+ else if (q.change_conservatism <= FACET_LOW) {
120
+ out.push({ section: 'Working Defaults', rule: '명확성을 위한 리팩토링은 허용 — 디버그 중 발견된 인접 결함도 같은 PR 가능' });
121
+ }
122
+ // Autonomy
123
+ if (a.confirmation_independence >= FACET_HIGH) {
124
+ out.push({ section: 'When To Ask', rule: '목표 합의 후 단계별 재확인 생략 — 마무리 시점 한 번만 보고' });
125
+ }
126
+ else if (a.confirmation_independence <= FACET_LOW) {
127
+ out.push({ section: 'When To Ask', rule: '비-사소한 단계마다 사용자 확인을 받고 진행' });
128
+ }
129
+ if (a.approval_threshold >= FACET_HIGH) {
130
+ out.push({ section: 'When To Ask', rule: '비가역 작업(force push, 데이터 삭제, 외부 broadcast) 전 명시 승인 필수' });
131
+ }
132
+ // Judgment
133
+ if (j.minimal_change_bias >= FACET_HIGH) {
134
+ out.push({ section: 'Working Defaults', rule: '직면한 문제만 해결 — 인접 개선/추상화 제안은 별도 보고' });
135
+ }
136
+ if (j.abstraction_bias >= FACET_HIGH) {
137
+ out.push({ section: 'Working Defaults', rule: '반복 패턴 발견 시 재사용 가능한 추상화로 통합 제안' });
138
+ }
139
+ else if (j.abstraction_bias <= FACET_LOW) {
140
+ out.push({ section: 'Working Defaults', rule: '명확성을 해치는 추상화 금지 — 인라인 중복 허용' });
141
+ }
142
+ if (j.evidence_first_bias >= FACET_HIGH) {
143
+ out.push({ section: 'How To Report', rule: '결론·가설 명시 전 근거(파일:라인, 로그, 측정값)를 먼저 표면화' });
144
+ }
145
+ // Communication
146
+ if (c.verbosity <= FACET_LOW) {
147
+ out.push({ section: 'How To Report', rule: '코드 표시가 필요하지 않으면 답변은 3문장 이내' });
148
+ }
149
+ else if (c.verbosity >= FACET_HIGH) {
150
+ out.push({ section: 'How To Report', rule: '결정 배경·대안·tradeoff 를 답변에 포함' });
151
+ }
152
+ if (c.structure >= FACET_HIGH) {
153
+ out.push({ section: 'How To Report', rule: '다중 포인트 답변은 헤더/표/리스트로 구조화' });
154
+ }
155
+ if (c.teaching_bias >= FACET_HIGH) {
156
+ out.push({ section: 'How To Report', rule: 'why/how 를 함께 설명 — what 만 답하지 말 것' });
157
+ }
158
+ return out;
159
+ }
94
160
  // ── Main Render ──
95
- export function renderRules(rules, state, _profile, ctx = DEFAULT_CONTEXT) {
161
+ export function renderRules(rules, state, profile, ctx = DEFAULT_CONTEXT) {
96
162
  // 1. active만 수집
97
163
  const active = rules.filter(r => r.status === 'active');
98
164
  // 2. dedupe by render_key
@@ -122,6 +188,12 @@ export function renderRules(rules, state, _profile, ctx = DEFAULT_CONTEXT) {
122
188
  for (const rule of communicationPackRules(state.communication_pack)) {
123
189
  sections.get('How To Report').push(rule);
124
190
  }
191
+ // 4축 facet 극단값 → 추가 규칙 (12-bucket pack 위에 연속 값 차별화).
192
+ // 각 facet 0.5 default 이고 자동 갱신 ±0.1 단위이므로, 0.85/0.15 임계값은
193
+ // 여러 세션에 걸친 강한 신호 누적 후에만 발화한다.
194
+ for (const fr of facetDrivenRules(profile)) {
195
+ sections.get(fr.section).push(fr.rule);
196
+ }
125
197
  }
126
198
  // 6. 섹션 조립 (AI-optimized: 간결한 태그 형식)
127
199
  const parts = [];
@@ -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;