@wooojin/forgen 0.4.13 → 0.5.5

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 (166) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +261 -0
  3. package/README.ja.md +16 -12
  4. package/README.ko.md +12 -8
  5. package/README.md +90 -27
  6. package/README.zh.md +16 -12
  7. package/assets/claude/commands/forge-loop.md +7 -1
  8. package/assets/claude/commands/ship.md +18 -1
  9. package/assets/opencode/forgen.ts +52 -0
  10. package/assets/shared/hook-registry.json +210 -22
  11. package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
  12. package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
  13. package/dist/checks/_shared/model-profile.d.ts +25 -0
  14. package/dist/checks/_shared/model-profile.js +63 -0
  15. package/dist/cli.js +99 -153
  16. package/dist/core/auto-compound-runner.d.ts +0 -11
  17. package/dist/core/auto-compound-runner.js +199 -92
  18. package/dist/core/changelog-cli.d.ts +4 -1
  19. package/dist/core/changelog-cli.js +8 -7
  20. package/dist/core/compound-consent.d.ts +15 -0
  21. package/dist/core/compound-consent.js +48 -0
  22. package/dist/core/compound-sweep-cli.d.ts +60 -0
  23. package/dist/core/compound-sweep-cli.js +374 -0
  24. package/dist/core/config-injector.d.ts +16 -1
  25. package/dist/core/config-injector.js +47 -28
  26. package/dist/core/dashboard-cli.js +40 -16
  27. package/dist/core/dashboard.d.ts +3 -4
  28. package/dist/core/dashboard.js +15 -34
  29. package/dist/core/dev-cli.d.ts +13 -0
  30. package/dist/core/dev-cli.js +70 -0
  31. package/dist/core/doctor.d.ts +16 -5
  32. package/dist/core/doctor.js +190 -176
  33. package/dist/core/drift-score.d.ts +2 -0
  34. package/dist/core/drift-score.js +9 -1
  35. package/dist/core/harness.js +115 -43
  36. package/dist/core/health-cli.d.ts +2 -0
  37. package/dist/core/health-cli.js +6 -1
  38. package/dist/core/host-detect.d.ts +3 -1
  39. package/dist/core/host-detect.js +26 -1
  40. package/dist/core/migrate-cli.js +15 -0
  41. package/dist/core/migrate-evidence-host.d.ts +2 -1
  42. package/dist/core/migrate-tenetx.d.ts +50 -0
  43. package/dist/core/migrate-tenetx.js +262 -0
  44. package/dist/core/probe-workflow-cli.d.ts +3 -3
  45. package/dist/core/probe-workflow-cli.js +13 -13
  46. package/dist/core/recall-cli.js +1 -1
  47. package/dist/core/regress-map-cli.js +1 -1
  48. package/dist/core/rendered-rules-manifest.d.ts +29 -0
  49. package/dist/core/rendered-rules-manifest.js +60 -0
  50. package/dist/core/session-store.js +14 -3
  51. package/dist/core/settings-injector.d.ts +3 -0
  52. package/dist/core/settings-injector.js +12 -16
  53. package/dist/core/spawn.d.ts +37 -1
  54. package/dist/core/spawn.js +116 -9
  55. package/dist/core/state-gc.js +1 -0
  56. package/dist/core/status-cli.d.ts +20 -0
  57. package/dist/core/status-cli.js +100 -0
  58. package/dist/core/statusline-cli.d.ts +7 -0
  59. package/dist/core/statusline-cli.js +63 -19
  60. package/dist/core/transcript-summary.d.ts +18 -0
  61. package/dist/core/transcript-summary.js +81 -0
  62. package/dist/core/trust-layer-intent.d.ts +21 -1
  63. package/dist/core/trust-layer-intent.js +7 -0
  64. package/dist/core/types.d.ts +3 -2
  65. package/dist/core/uninstall.js +12 -0
  66. package/dist/core/usage-telemetry.d.ts +7 -1
  67. package/dist/core/usage-telemetry.js +7 -9
  68. package/dist/core/v1-bootstrap.js +1 -1
  69. package/dist/core/watch-cli.js +1 -1
  70. package/dist/engine/compound-extractor.js +10 -0
  71. package/dist/engine/compound-loop.js +55 -7
  72. package/dist/engine/compound-share.d.ts +85 -0
  73. package/dist/engine/compound-share.js +606 -0
  74. package/dist/engine/correction-cluster-runner.d.ts +38 -0
  75. package/dist/engine/correction-cluster-runner.js +188 -0
  76. package/dist/engine/correction-clustering.d.ts +80 -0
  77. package/dist/engine/correction-clustering.js +167 -0
  78. package/dist/engine/enforce-classifier.d.ts +10 -1
  79. package/dist/engine/enforce-classifier.js +111 -22
  80. package/dist/engine/extraction-session.js +10 -3
  81. package/dist/engine/private-filter.d.ts +36 -0
  82. package/dist/engine/private-filter.js +100 -0
  83. package/dist/engine/ranking-pipeline.js +4 -2
  84. package/dist/engine/relevance-gate.d.ts +12 -0
  85. package/dist/engine/relevance-gate.js +12 -0
  86. package/dist/engine/roi-demotion.d.ts +79 -0
  87. package/dist/engine/roi-demotion.js +159 -0
  88. package/dist/engine/solution-format.d.ts +1 -0
  89. package/dist/engine/solution-format.js +26 -0
  90. package/dist/engine/solution-matcher.js +10 -1
  91. package/dist/fgx.js +7 -6
  92. package/dist/forge/cli.js +8 -2
  93. package/dist/hooks/compound-reflection.js +6 -1
  94. package/dist/hooks/context-guard.d.ts +16 -1
  95. package/dist/hooks/context-guard.js +99 -49
  96. package/dist/hooks/hook-config.js +5 -0
  97. package/dist/hooks/hook-registry.d.ts +6 -1
  98. package/dist/hooks/hooks-generator.js +3 -0
  99. package/dist/hooks/post-tool-use.js +2 -3
  100. package/dist/hooks/pre-compact.js +14 -0
  101. package/dist/hooks/pre-tool-use.js +5 -1
  102. package/dist/hooks/session-end.d.ts +34 -0
  103. package/dist/hooks/session-end.js +100 -0
  104. package/dist/hooks/session-recovery.js +18 -1
  105. package/dist/hooks/shared/hook-timing.js +6 -1
  106. package/dist/hooks/shared/stop-triggers.d.ts +29 -2
  107. package/dist/hooks/shared/stop-triggers.js +35 -2
  108. package/dist/hooks/solution-injector.d.ts +28 -0
  109. package/dist/hooks/solution-injector.js +126 -28
  110. package/dist/hooks/stop-guard.js +5 -2
  111. package/dist/hooks/subagent-stop-guard.js +4 -1
  112. package/dist/host/capabilities-claude.js +1 -0
  113. package/dist/host/capabilities-codex.js +1 -0
  114. package/dist/host/capabilities-opencode.d.ts +26 -0
  115. package/dist/host/capabilities-opencode.js +78 -0
  116. package/dist/host/capabilities-registry.d.ts +7 -0
  117. package/dist/host/capabilities-registry.js +14 -0
  118. package/dist/host/codex-rules-context.d.ts +25 -0
  119. package/dist/host/codex-rules-context.js +62 -0
  120. package/dist/host/exec-host.d.ts +14 -3
  121. package/dist/host/exec-host.js +15 -2
  122. package/dist/host/host-binding.d.ts +27 -0
  123. package/dist/host/host-binding.js +11 -0
  124. package/dist/host/host-runtime.js +18 -0
  125. package/dist/host/install-codex.d.ts +66 -0
  126. package/dist/host/install-codex.js +293 -25
  127. package/dist/host/install-opencode.d.ts +38 -0
  128. package/dist/host/install-opencode.js +148 -0
  129. package/dist/host/install-orchestrator.d.ts +4 -1
  130. package/dist/host/install-orchestrator.js +30 -1
  131. package/dist/host/invoke-agent.d.ts +3 -2
  132. package/dist/host/invoke-agent.js +1 -0
  133. package/dist/host/opencode/context-cli.d.ts +15 -0
  134. package/dist/host/opencode/context-cli.js +25 -0
  135. package/dist/host/opencode/guard-cli.d.ts +13 -0
  136. package/dist/host/opencode/guard-cli.js +39 -0
  137. package/dist/host/opencode/plugin/forgen.d.ts +31 -0
  138. package/dist/host/opencode/plugin/forgen.js +60 -0
  139. package/dist/host/opencode/translate.d.ts +49 -0
  140. package/dist/host/opencode/translate.js +96 -0
  141. package/dist/host/parity-harness.d.ts +10 -2
  142. package/dist/host/parity-harness.js +7 -3
  143. package/dist/host/projection.d.ts +37 -3
  144. package/dist/host/projection.js +104 -42
  145. package/dist/mcp/tools.js +17 -4
  146. package/dist/store/evidence-store.d.ts +2 -6
  147. package/dist/store/evidence-store.js +164 -46
  148. package/dist/store/host-mismatch.d.ts +2 -1
  149. package/dist/store/host-mismatch.js +8 -8
  150. package/dist/store/profile-store.d.ts +3 -2
  151. package/dist/store/rule-store.d.ts +41 -0
  152. package/dist/store/rule-store.js +77 -2
  153. package/dist/store/types.d.ts +9 -2
  154. package/hooks/hooks.json +13 -1
  155. package/package.json +3 -2
  156. package/plugin.json +2 -2
  157. package/skills/architecture-decision/SKILL.md +18 -0
  158. package/skills/calibrate/SKILL.md +18 -0
  159. package/skills/code-review/SKILL.md +17 -0
  160. package/skills/compound/SKILL.md +17 -0
  161. package/skills/deep-interview/SKILL.md +7 -0
  162. package/skills/docker/SKILL.md +18 -0
  163. package/skills/forge-loop/SKILL.md +30 -1
  164. package/skills/learn/SKILL.md +15 -0
  165. package/skills/retro/SKILL.md +16 -0
  166. package/skills/ship/SKILL.md +36 -1
@@ -10,4 +10,11 @@
10
10
  * Line 4: (TODO: tool counts — 추적 인프라 없음)
11
11
  * Line 5: (TODO: active task — 추적 인프라 없음)
12
12
  */
13
+ /**
14
+ * W1-2 (feature-audit 2026-07-21): forgen 가치 카운터 라인 — invisible 가치 가시화.
15
+ * native 플랜/토큰 usage(→ /usage, W2에서 물러남)가 아니라 forgen *자체 활동량*을
16
+ * 노출한다: recall 주입·surfaced·교정 캡처·ROI 강등·차단. 전부 computeStats()가 이미
17
+ * 실측 중인 카운터라 조작 위험 0. computeStats 실패 시 라인 생략(fail-open).
18
+ */
19
+ export declare function buildValueLine(): string | null;
13
20
  export declare function handleStatusline(): Promise<void>;
@@ -15,9 +15,10 @@ import * as path from 'node:path';
15
15
  import * as os from 'node:os';
16
16
  import { execSync } from 'node:child_process';
17
17
  import { loadActiveRules } from '../store/rule-store.js';
18
- import { getUsageStats } from './usage-telemetry.js';
19
18
  import { STATE_DIR } from './paths.js';
20
19
  import { classifySolutions } from './lifecycle-classifier.js';
20
+ import { computeStats } from './stats-cli.js';
21
+ import { loadRoiDemotions } from '../engine/roi-demotion.js';
21
22
  // 0.4.6 perf #13 — statusline 출력을 5초 캐싱.
22
23
  // claude statusLine 은 짧은 간격으로 재호출되는데 매번 git/find/rule-store 를
23
24
  // 실행하면 ~100ms 누적. CACHE_TTL_MS 동안 동일 출력 재사용.
@@ -148,18 +149,48 @@ function buildLifecycleLine() {
148
149
  return null;
149
150
  }
150
151
  }
151
- /** Build usage line: "📊 87/5h · 412/wk (claude)" — 0.4.6 신설 */
152
+ /**
153
+ * ADR-010 W2-2: 사용량 세그먼트("📊 N/5h · N/wk") 제거 — native /usage 가
154
+ * plan limit 를 정확히 분해한다. 이관 사실을 딱 1회만 공지 (state flag).
155
+ */
152
156
  function buildUsageLine() {
153
157
  try {
154
- const stats = getUsageStats();
155
- if (stats.week.total === 0)
156
- return null;
157
- const dominant = stats.week.codex > stats.week.claude ? 'codex' : 'claude';
158
- return [
159
- `${YELLOW}📊 ${stats.hour5.total}/5h${RESET}`,
160
- `${YELLOW}${stats.week.total}/wk${RESET}`,
161
- `${DIM}(${dominant})${RESET}`,
162
- ].join(` ${DIM}·${RESET} `);
158
+ const noticeFlag = path.join(STATE_DIR, 'usage-notice-shown');
159
+ if (!fs.existsSync(noticeFlag)) {
160
+ fs.mkdirSync(STATE_DIR, { recursive: true });
161
+ fs.writeFileSync(noticeFlag, new Date().toISOString());
162
+ return `${DIM}ℹ 사용량 표시는 native /usage 로 이동했습니다 (이 안내는 1회만 표시)${RESET}`;
163
+ }
164
+ return null;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * W1-2 (feature-audit 2026-07-21): forgen 가치 카운터 라인 — invisible 가치 가시화.
172
+ * native 플랜/토큰 usage(→ /usage, W2에서 물러남)가 아니라 forgen *자체 활동량*을
173
+ * 노출한다: recall 주입·surfaced·교정 캡처·ROI 강등·차단. 전부 computeStats()가 이미
174
+ * 실측 중인 카운터라 조작 위험 0. computeStats 실패 시 라인 생략(fail-open).
175
+ */
176
+ export function buildValueLine() {
177
+ try {
178
+ let roiDemoted = 0;
179
+ try {
180
+ roiDemoted = Object.keys(loadRoiDemotions()).length;
181
+ }
182
+ catch { /* roi store 없음 → 0 */ }
183
+ const s = computeStats();
184
+ const a = s.assistToday;
185
+ const parts = [
186
+ `${YELLOW}✦${RESET}`,
187
+ `${DIM}recall${RESET} ${a.recallHits}`,
188
+ `${DIM}surfaced${RESET} ${a.surfaced}`,
189
+ `${DIM}교정${RESET} +${s.corrections7d}${DIM}(7d)${RESET}`,
190
+ `${DIM}ROI↓${RESET} ${roiDemoted}`,
191
+ `${DIM}차단${RESET} ${s.blocks7d}${DIM}(7d)${RESET}`,
192
+ ];
193
+ return parts.join(` ${DIM}·${RESET} `);
163
194
  }
164
195
  catch {
165
196
  return null;
@@ -204,7 +235,19 @@ function writeCache(content) {
204
235
  catch { /* fail-open */ }
205
236
  }
206
237
  export async function handleStatusline() {
207
- // 캐시 hit 시 stdin payload 무시하고 바로 출력 (5초 윈도우 내 동일 출력 가정).
238
+ // W4-3 (ADR-010): hook stdin 엔 모델 필드가 없으므로 statusline 이 세션별
239
+ // 모델을 캐시 → Stop/SubagentStop 가드가 per-model 프로필 조회에 사용.
240
+ // 리뷰 SEV-2: 이 기록은 반드시 5초 표시-캐시 early-return **앞**에 있어야
241
+ // 한다 — /model 로 세션 중 모델을 바꾸면 cache-hit 렌더가 payload 를 아예
242
+ // 안 읽어 stale 모델(잘못된 가드 모드)이 유지되는 창이 생긴다.
243
+ // 필드 부재(구버전 CC 등) 시 기록 안 함 = 가드는 'unknown' → block 유지.
244
+ const payload = readStdinJson();
245
+ if (payload.session_id && payload.model?.id) {
246
+ const { cacheSessionModel } = await import('../checks/_shared/model-profile.js');
247
+ const { sanitizeId } = await import('../hooks/shared/sanitize-id.js');
248
+ cacheSessionModel(sanitizeId(payload.session_id), payload.model.id);
249
+ }
250
+ // 캐시 hit 시 표시만 캐시에서 (5초 윈도우 내 동일 출력 가정).
208
251
  // 라인 단위 cache → console.log 라인별 (테스트 호환).
209
252
  const cached = readCacheIfFresh();
210
253
  if (cached !== null) {
@@ -212,27 +255,28 @@ export async function handleStatusline() {
212
255
  console.log(line);
213
256
  return;
214
257
  }
215
- const payload = readStdinJson();
216
258
  const cwd = payload.workspace?.current_dir ?? process.cwd();
217
259
  const claudeDir = path.join(os.homedir(), '.claude');
218
260
  const line1 = buildLine1(payload, cwd);
219
261
  const line3 = buildLine3(claudeDir, cwd);
220
262
  const usageLine = buildUsageLine();
263
+ const valueLine = buildValueLine(); // W1-2: forgen 가치 카운터
221
264
  const lifecycleLine = buildLifecycleLine();
222
- // Line 2 (context/usage): stdin JSON spec 미확인으로 생략 — TODO
223
- // Line 4 (tool counts): 추적 인프라 없음 — TODO
224
- // Line 5 (active task): 추적 인프라 없음 — TODO
225
265
  console.log(line1);
226
266
  console.log(line3);
227
267
  if (usageLine)
228
268
  console.log(usageLine);
269
+ if (valueLine)
270
+ console.log(valueLine);
229
271
  if (lifecycleLine)
230
272
  console.log(lifecycleLine);
273
+ // W2-2: 1회 공지(usageLine)는 캐시에 넣지 않는다 — 캐시 재생 시
274
+ // "1회만" 약속이 5초 창 동안 반복 위반되는 실측 버그 방지.
231
275
  const cacheLines = [line1, line3];
232
- if (usageLine)
233
- cacheLines.push(usageLine);
276
+ if (valueLine)
277
+ cacheLines.push(valueLine);
234
278
  if (lifecycleLine)
235
279
  cacheLines.push(lifecycleLine);
236
- const cacheBody = cacheLines.join('\n') + '\n';
280
+ const cacheBody = `${cacheLines.join('\n')}\n`;
237
281
  writeCache(cacheBody);
238
282
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Transcript → 요약 텍스트 (순수, IO 없음 — 테스트 가능).
3
+ *
4
+ * auto-compound-runner 의 extractSummary 에서 분리(2026-08-04). 분리 이유: 실 Claude
5
+ * transcript 스키마 버그(text 가 `entry.message.content` 에 있는데 top-level `entry.content`
6
+ * 만 읽어 전 user/assistant 턴 누락 → auto-compound 가 실데이터에서 dead)를 회귀 테스트로
7
+ * 고정하기 위함. 러너 본문은 process.argv 실행부와 묶여 있어 직접 unit-test 불가였다.
8
+ *
9
+ * 스키마:
10
+ * Claude: {type:'user'|'assistant', message:{role, content: string | [{type:'text',text}]}}
11
+ * (일부 queue-operation 등은 top-level `content` 에 저장 → fallback)
12
+ * Codex: {type:'response_item', payload:{role, content:[{type:'input_text', text}]}}
13
+ */
14
+ export declare function extractText(c: unknown): string;
15
+ /** Codex content array → flat string. content: [{type:'input_text', text}] */
16
+ export declare function extractCodexText(content: unknown): string;
17
+ /** JSONL 원문 → `[User]/[Assistant]` 요약. maxChars 초과 시 조기 종료. */
18
+ export declare function summarizeTranscript(rawText: string, maxChars?: number): string;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Transcript → 요약 텍스트 (순수, IO 없음 — 테스트 가능).
3
+ *
4
+ * auto-compound-runner 의 extractSummary 에서 분리(2026-08-04). 분리 이유: 실 Claude
5
+ * transcript 스키마 버그(text 가 `entry.message.content` 에 있는데 top-level `entry.content`
6
+ * 만 읽어 전 user/assistant 턴 누락 → auto-compound 가 실데이터에서 dead)를 회귀 테스트로
7
+ * 고정하기 위함. 러너 본문은 process.argv 실행부와 묶여 있어 직접 unit-test 불가였다.
8
+ *
9
+ * 스키마:
10
+ * Claude: {type:'user'|'assistant', message:{role, content: string | [{type:'text',text}]}}
11
+ * (일부 queue-operation 등은 top-level `content` 에 저장 → fallback)
12
+ * Codex: {type:'response_item', payload:{role, content:[{type:'input_text', text}]}}
13
+ */
14
+ export function extractText(c) {
15
+ if (typeof c === 'string')
16
+ return c;
17
+ if (Array.isArray(c)) {
18
+ return c
19
+ .filter((x) => typeof x === 'object' && x !== null && x.type === 'text')
20
+ .map((x) => (typeof x.text === 'string' ? x.text : ''))
21
+ .join('\n');
22
+ }
23
+ return '';
24
+ }
25
+ /** Codex content array → flat string. content: [{type:'input_text', text}] */
26
+ export function extractCodexText(content) {
27
+ if (!Array.isArray(content))
28
+ return '';
29
+ const parts = [];
30
+ for (const item of content) {
31
+ if (item && typeof item === 'object' && 'text' in item && typeof item.text === 'string') {
32
+ parts.push(item.text);
33
+ }
34
+ }
35
+ return parts.join('\n');
36
+ }
37
+ /** JSONL 원문 → `[User]/[Assistant]` 요약. maxChars 초과 시 조기 종료. */
38
+ export function summarizeTranscript(rawText, maxChars = 8000) {
39
+ const lines = rawText.split('\n').filter(Boolean);
40
+ const messages = [];
41
+ let totalChars = 0;
42
+ for (const line of lines) {
43
+ try {
44
+ const entry = JSON.parse(line);
45
+ // Claude schema — text 는 message.content 우선(실버그 수정), top-level 은 fallback.
46
+ if (entry.type === 'user' || entry.type === 'queue-operation') {
47
+ const text = extractText(entry.message?.content ?? entry.content);
48
+ if (text) {
49
+ messages.push(`[User] ${text.slice(0, 500)}`);
50
+ totalChars += text.length;
51
+ }
52
+ }
53
+ else if (entry.type === 'assistant') {
54
+ const text = extractText(entry.message?.content ?? entry.content);
55
+ if (text) {
56
+ messages.push(`[Assistant] ${text.slice(0, 500)}`);
57
+ totalChars += text.length;
58
+ }
59
+ }
60
+ // Codex schema
61
+ else if (entry.type === 'response_item' && entry.payload?.role === 'user') {
62
+ const text = extractCodexText(entry.payload.content);
63
+ if (text) {
64
+ messages.push(`[User] ${text.slice(0, 500)}`);
65
+ totalChars += text.length;
66
+ }
67
+ }
68
+ else if (entry.type === 'response_item' && entry.payload?.role === 'assistant') {
69
+ const text = extractCodexText(entry.payload.content);
70
+ if (text) {
71
+ messages.push(`[Assistant] ${text.slice(0, 500)}`);
72
+ totalChars += text.length;
73
+ }
74
+ }
75
+ }
76
+ catch { /* skip malformed line */ }
77
+ if (totalChars > maxChars)
78
+ break;
79
+ }
80
+ return messages.join('\n\n');
81
+ }
@@ -19,9 +19,29 @@ export interface CapabilityDeclaration {
19
19
  /** source-of-truth (spec 또는 외부 docs/source 인용). */
20
20
  readonly source?: string;
21
21
  }
22
- export type HostId = 'claude' | 'codex';
22
+ /**
23
+ * 지원 host 의 정준 런타임 목록(단일 소스). `HostId` 는 여기서 파생되므로 새 host 추가 시
24
+ * 이 배열만 넓히면 타입·런타임이 함께 확장된다. 자유형 `host === 'claude'` 이진 비교 대신
25
+ * `(HOST_IDS as readonly string[]).includes(host)` 로 "유효 host 인지" 를 판정하라
26
+ * (W3-3 리뷰 SEV-3 #5: Record<HostId> 는 Record 리터럴만 강제, 자유비교는 미포착).
27
+ */
28
+ export declare const HOST_IDS: readonly ["claude", "codex", "opencode"];
29
+ export type HostId = (typeof HOST_IDS)[number];
30
+ /**
31
+ * 능력 선언의 검증 수준 (W3-3 리뷰 SEV-3 #1).
32
+ * - 'runtime': forgen 이 이 host 에서 실제로 강제/실행함(레퍼런스 host — claude).
33
+ * - 'source' : host 의 hook schema 소스로 검증했고 forgen 배선 완료(codex).
34
+ * - 'docs' : host 문서 기반 선언이나 **forgen 배선 미완**(opencode P1 — plugin 슬림 전).
35
+ *
36
+ * status='supported' 의 의미가 host 마다 다른 문제를 구조화한다: 'runtime'/'source' 의
37
+ * supported 는 "forgen 이 강제함", 'docs' 의 supported 는 "플랫폼이 가능하나 forgen 미배선".
38
+ * 프로그램 소비자는 verificationLevel 로 둘을 게이트해야 한다(intentEnforced 참조).
39
+ */
40
+ export type CapabilityVerificationLevel = 'runtime' | 'source' | 'docs';
23
41
  export interface HostCapabilities {
24
42
  readonly hostId: HostId;
43
+ /** 이 host 선언 전체의 검증 수준. 'docs' 면 아래 status 는 "플랫폼-가능"이지 "forgen-배선"이 아니다. */
44
+ readonly verificationLevel: CapabilityVerificationLevel;
25
45
  /**
26
46
  * 모든 TrustLayerIntent 에 대한 선언. `Record<TrustLayerIntent, _>` 타입이
27
47
  * 컴파일 타임에 누락을 차단한다.
@@ -16,6 +16,13 @@ export const TRUST_LAYER_INTENTS = [
16
16
  'forge-loop-state-inject',
17
17
  'self-evidence-record',
18
18
  ];
19
+ /**
20
+ * 지원 host 의 정준 런타임 목록(단일 소스). `HostId` 는 여기서 파생되므로 새 host 추가 시
21
+ * 이 배열만 넓히면 타입·런타임이 함께 확장된다. 자유형 `host === 'claude'` 이진 비교 대신
22
+ * `(HOST_IDS as readonly string[]).includes(host)` 로 "유효 host 인지" 를 판정하라
23
+ * (W3-3 리뷰 SEV-3 #5: Record<HostId> 는 Record 리터럴만 강제, 자유비교는 미포착).
24
+ */
25
+ export const HOST_IDS = ['claude', 'codex', 'opencode'];
19
26
  /**
20
27
  * 런타임 assertion — host adapter 가 새 의도 추가를 누락한 경우 fail.
21
28
  * 컴파일 타임 가드를 우회하는 동적 생성 코드를 위한 안전망.
@@ -1,3 +1,4 @@
1
+ import type { HostId } from './trust-layer-intent.js';
1
2
  export interface Principle {
2
3
  belief: string;
3
4
  /**
@@ -106,8 +107,8 @@ export interface HarnessContext {
106
107
  /** 모델 라우팅 프리셋 (default, cost-saving, max-quality) */
107
108
  routingPreset?: string;
108
109
  }
109
- /** 런타임 Host */
110
- export type RuntimeHost = 'claude' | 'codex';
110
+ /** 런타임 Host — 정식 정의는 `trust-layer-intent.ts` 의 HostId (Multi-Harness Adapter Plan P0). */
111
+ export type RuntimeHost = HostId;
111
112
  /** 런칭 컨텍스트 — CLI에서 runtime/args 결정을 모델화 */
112
113
  export interface LaunchContext {
113
114
  runtime: RuntimeHost;
@@ -236,6 +236,10 @@ function cleanCompoundRules(cwd) {
236
236
  // v4.1+ consolidated
237
237
  'project-context.md',
238
238
  'routing.md',
239
+ // v0.5.0+ project-scoped (ADR-010 W1-3)
240
+ 'v1-rules.md',
241
+ 'forge-behavioral.md',
242
+ 'user-profile.md',
239
243
  // legacy (v4.0 and earlier)
240
244
  'security.md',
241
245
  'golden-principles.md',
@@ -257,6 +261,14 @@ function cleanCompoundRules(cwd) {
257
261
  fs.unlinkSync(legacyPath);
258
262
  removedCount++;
259
263
  }
264
+ // 레거시 글로벌 사이드채널 (ADR-010 W1-3 이전 버전이 ~/.claude/rules/ 에 씀).
265
+ // 새 세션이 열리기 전에 uninstall 하는 경우 harness 마이그레이션이 못 지우므로
266
+ // 여기서도 회수한다 — 방치 시 언인스톨 후에도 전 세션에 주입된다.
267
+ const legacyGlobalBehavioral = path.join(os.homedir(), '.claude', 'rules', 'forge-behavioral.md');
268
+ if (fs.existsSync(legacyGlobalBehavioral)) {
269
+ fs.unlinkSync(legacyGlobalBehavioral);
270
+ removedCount++;
271
+ }
260
272
  if (removedCount > 0) {
261
273
  console.log(` ✓ Removed ${removedCount} rule file(s)`);
262
274
  }
@@ -13,6 +13,7 @@
13
13
  * 0.4.6 신설. limit prediction 은 의도적으로 제외 — Anthropic 의 실제 limit 가
14
14
  * 계정/플랜별 가변이라 hard-code 부정확. raw count 만 노출하고 사용자가 판단.
15
15
  */
16
+ import type { HostId } from './trust-layer-intent.js';
16
17
  export interface UsageStats {
17
18
  hour5: {
18
19
  claude: number;
@@ -25,7 +26,12 @@ export interface UsageStats {
25
26
  total: number;
26
27
  };
27
28
  }
28
- export declare function recordToolCall(runtime?: 'claude' | 'codex'): void;
29
+ /**
30
+ * @deprecated ADR-010 W2-2 (2026-07-16): native `/usage` 가 plan limit 를
31
+ * skill/subagent/plugin/MCP 별로 정확히 분해하므로 forgen 의 raw count 는
32
+ * 중복 표면. 기록 중단(no-op shim — API 만 유지), v0.6.0 에서 모듈 삭제 예정.
33
+ */
34
+ export declare function recordToolCall(_runtime?: HostId): void;
29
35
  /**
30
36
  * sliding window count. fail-open: 파일 미존재/parse 실패는 0 반환.
31
37
  *
@@ -22,15 +22,13 @@ const TELEMETRY_PATH = path.join(STATE_DIR, 'usage-telemetry.jsonl');
22
22
  const HOUR5_MS = 5 * 60 * 60 * 1000;
23
23
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
24
24
  const PRUNE_THRESHOLD = 10_000; // 10K lines 누적 시 prune
25
- export function recordToolCall(runtime = 'claude') {
26
- try {
27
- fs.mkdirSync(STATE_DIR, { recursive: true });
28
- const entry = { ts: Date.now(), rt: runtime };
29
- fs.appendFileSync(TELEMETRY_PATH, `${JSON.stringify(entry)}\n`);
30
- }
31
- catch (e) {
32
- log.debug('telemetry append 실패', e);
33
- }
25
+ /**
26
+ * @deprecated ADR-010 W2-2 (2026-07-16): native `/usage` 가 plan limit 를
27
+ * skill/subagent/plugin/MCP 별로 정확히 분해하므로 forgen 의 raw count 는
28
+ * 중복 표면. 기록 중단(no-op shim — API 만 유지), v0.6.0 에서 모듈 삭제 예정.
29
+ */
30
+ export function recordToolCall(_runtime = 'claude') {
31
+ // no-op — 사용량 추적은 native /usage 로 이관됨.
34
32
  }
35
33
  /**
36
34
  * sliding window count. fail-open: 파일 미존재/parse 실패는 0 반환.
@@ -129,7 +129,7 @@ export function bootstrapV1Session() {
129
129
  mismatchResult = detectMismatch(allSignals);
130
130
  // mismatch 감지 시 재추천 생성
131
131
  if (mismatchResult.quality_mismatch || mismatchResult.autonomy_mismatch) {
132
- session.warnings.push(`Pack mismatch 감지: quality=${mismatchResult.quality_score}, autonomy=${mismatchResult.autonomy_score}. forgen forge --reset soft 로 재설정하거나 forgen onboarding 으로 재추천을 받으세요.`);
132
+ session.warnings.push(`Pack mismatch 감지: quality=${mismatchResult.quality_score}, autonomy=${mismatchResult.autonomy_score}. forgen forge --reset soft 로 재설정하거나 forgen forge --onboarding 으로 재추천을 받으세요.`);
133
133
  const rec = createRecommendation({
134
134
  source: 'mismatch_recommendation',
135
135
  quality_pack: mismatchResult.quality_mismatch
@@ -136,7 +136,7 @@ export async function handleWatch() {
136
136
  format: formatMatchEval,
137
137
  },
138
138
  ];
139
- console.log(`\n ${C.cyan}forgen watch${C.reset} — real-time event stream`);
139
+ console.log(`\n ${C.cyan}forgen status --live${C.reset} — real-time event stream`);
140
140
  console.log(` ${C.dim}Watching: hook-timing, violations, match-eval-log${C.reset}`);
141
141
  console.log(` ${C.dim}Press Ctrl+C to stop${C.reset}\n`);
142
142
  // Show recent events (last 10 per source)
@@ -21,6 +21,7 @@ import { getNewCommits, getCommitMessages, getGitDiff, getDiffStats } from './ex
21
21
  import { gate0, evaluateExtractedSolution } from './extraction-gates.js';
22
22
  import { extractFromDiff } from './extraction-diff.js';
23
23
  import { extractFromSessionContext } from './extraction-session.js';
24
+ import { stripPrivate, isFullyPrivate } from './private-filter.js';
24
25
  import { loadLastExtraction, saveLastExtraction, saveExtractedSolution, updateReExtractedCounter, emitCompoundExtractActedOn, } from './extraction-persistence.js';
25
26
  const log = createLogger('compound-extractor');
26
27
  const MAX_EXTRACTIONS_PER_DAY = 5;
@@ -172,6 +173,15 @@ export function processExtractionResults(rawJson, sessionId) {
172
173
  return { saved, skipped };
173
174
  }
174
175
  for (const sol of solutions.slice(0, 3)) {
176
+ // W2-5 (private 태그): <private> 범위를 솔루션 content/context 에서 제거 후 저장.
177
+ // 통째로 private 이면 저장 skip (학습 코퍼스에 남기지 않음).
178
+ if (isFullyPrivate(sol.content) || isFullyPrivate(sol.context ?? '')) {
179
+ skipped.push(`${sol.name}: <private> (제외)`);
180
+ continue;
181
+ }
182
+ sol.content = stripPrivate(sol.content).cleaned;
183
+ if (sol.context)
184
+ sol.context = stripPrivate(sol.context).cleaned;
175
185
  const evaluation = evaluateExtractedSolution(sol);
176
186
  if (evaluation.action === 'skip' || evaluation.action === 'duplicate') {
177
187
  skipped.push(evaluation.message ?? `${sol.name}: skipped`);
@@ -190,11 +190,17 @@ export async function handleCompound(args) {
190
190
  forgen compound --lifecycle Run promotion/demotion/circuit-breaker check
191
191
  forgen compound --verify <name> Manually promote solution to verified
192
192
 
193
- Export/Import:
193
+ Export/Import (whole store, tar.gz):
194
194
  forgen compound export [--output path]
195
195
  Export knowledge to tar.gz archive
196
196
  forgen compound import <path> Import knowledge from archive (skip existing)
197
197
 
198
+ Export/Import (per-pattern, JSON — with confidence + provenance):
199
+ forgen compound export <name...> [--out file]
200
+ Export named solutions/rules as a portable bundle
201
+ forgen compound import <bundle.json> [--dry-run]
202
+ Import a bundle (probation trust, never overwrites)
203
+
198
204
  Auto-extraction:
199
205
  forgen compound --pause-auto Pause auto-extraction
200
206
  forgen compound --resume-auto Resume auto-extraction
@@ -204,16 +210,50 @@ export async function handleCompound(args) {
204
210
  `);
205
211
  return;
206
212
  }
213
+ // --- manual add (defense-in-depth, 2026-08-20) ---
214
+ // manual add(--solution/--rule/--convention/--pattern)를 아래 서브커맨드 dispatch 보다
215
+ // 먼저 처리한다. dispatch 는 args.includes('remove'|'clean-stale'|...) 로 전체 인자를
216
+ // 스캔하므로, add 의 위치인자(title/content)에 그런 토큰이 섞이면 삭제·정리 분기를
217
+ // 탈취할 수 있다(argument confusion — auto-compound 러너가 untrusted 모델 출력을 넘기던
218
+ // 경로에서 실증). type flag 는 add 전용이라 우선 처리해도 legit 동작 불변이며, 이렇게
219
+ // 하면 title/content 가 dispatch 스캔에 아예 닿지 않는다. (러너측 대시-선두 가드에 더한 2중 방어)
220
+ if (['--solution', '--rule', '--convention', '--pattern'].some(f => args.includes(f))) {
221
+ await handleManualAdd(args, cwd, scope);
222
+ return;
223
+ }
207
224
  // --- export command ---
225
+ // 통짜 tar.gz export(플래그만, positional 없음) vs 이름 지정 패턴 export를
226
+ // positional 인자 유무로 구분한다. `compound export foo` = 패턴 export,
227
+ // `compound export --output x` = 기존 whole-store export.
208
228
  if (args[0] === 'export') {
209
- const { handleExport } = await import('./compound-export.js');
210
- await handleExport(args.slice(1));
229
+ const rest = args.slice(1);
230
+ const hasPatternNames = rest.length > 0 && !rest[0].startsWith('--');
231
+ if (hasPatternNames) {
232
+ const { handleShareExport } = await import('./compound-share.js');
233
+ await handleShareExport(rest);
234
+ }
235
+ else {
236
+ const { handleExport } = await import('./compound-export.js');
237
+ await handleExport(rest);
238
+ }
211
239
  return;
212
240
  }
213
241
  // --- import command ---
242
+ // 대상 파일이 JSON 패턴 번들인지 tar.gz 아카이브인지 확장자/매직바이트로
243
+ // sniff해서 라우팅한다 (looksLikeShareBundle). 기존 tar.gz import 동작은
244
+ // 그대로 유지.
214
245
  if (args[0] === 'import') {
215
- const { handleImport } = await import('./compound-export.js');
216
- await handleImport(args.slice(1));
246
+ const rest = args.slice(1);
247
+ const filePath = rest.find(a => !a.startsWith('--'));
248
+ const { looksLikeShareBundle } = await import('./compound-share.js');
249
+ if (filePath && looksLikeShareBundle(path.resolve(filePath))) {
250
+ const { handleShareImport } = await import('./compound-share.js');
251
+ await handleShareImport(rest);
252
+ }
253
+ else {
254
+ const { handleImport } = await import('./compound-export.js');
255
+ await handleImport(rest);
256
+ }
217
257
  return;
218
258
  }
219
259
  // --pause-auto / --resume-auto
@@ -399,17 +439,25 @@ export async function handleCompound(args) {
399
439
  '--lifecycle', '--verify', '--save', '--interactive',
400
440
  'list', 'inspect', 'remove', 'rollback', 'retag', 'lifecycle',
401
441
  '--list', '--inspect', '--remove', '--rollback', '--retag', '--since', 'interactive',
402
- 'export', 'import', '--output',
442
+ 'export', 'import', '--output', '--out', '--dry-run',
403
443
  ];
404
444
  const hasTypeFlag = knownFlags.some(f => args.includes(f));
405
445
  if (!hasTypeFlag) {
406
446
  console.log(' Unknown compound arguments. Run `forgen compound --help` for usage.\n');
407
447
  return;
408
448
  }
449
+ await handleManualAdd(args, cwd, scope);
450
+ }
451
+ /**
452
+ * 수동 인사이트 추가 (--solution/--rule/--convention/--pattern).
453
+ * handleCompound 에서 두 경로로 호출된다:
454
+ * (1) hoisted early-dispatch — type flag 가 있으면 서브커맨드 스캔 전에 즉시(arg-confusion 방어).
455
+ * (2) fallthrough — type flag 없이 다른 known flag(예: --to)만 있는 legacy 경로(동작 보존).
456
+ */
457
+ async function handleManualAdd(args, cwd, scope) {
409
458
  console.log('\n Compound Loop — Accumulating insights\n');
410
459
  console.log(` Scope: ${scope.summary}`);
411
460
  console.log();
412
- // 수동 인사이트 추가
413
461
  const type = args.includes('--solution') ? 'solution'
414
462
  : args.includes('--rule') ? 'rule'
415
463
  : args.includes('--convention') ? 'convention'
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Forgen — Compound Pattern Share (패턴별 export/import)
3
+ *
4
+ * `compound-export.ts`의 `export`/`import`는 `~/.forgen/me/` 전체를 tar.gz로
5
+ * 통짜 백업/이관한다. 이 모듈은 그와 달리 **이름 지정된 패턴 단위**로 신뢰도
6
+ * (confidence/status/evidence)와 provenance를 함께 담은 JSON 번들을 만들고,
7
+ * 받는 쪽에서 안전하게 병합한다 (ECC `/instinct-import/export` 대응, OSS gap #1).
8
+ *
9
+ * 핵심 설계:
10
+ * - 번들은 스키마 버전 고정 JSON. 최상위/패턴 필드 모두 화이트리스트 검증 —
11
+ * 예상 못한 필드가 있으면 통째로 reject (실행 가능한 콘텐츠가 섞여 들어올
12
+ * 여지 자체를 차단).
13
+ * - 패턴마다 contentHash(sha256)를 동봉 — import 시 재계산해 일치하지 않으면
14
+ * reject (변조/손상 탐지).
15
+ * - 이름 충돌 시: 로컬 콘텐츠 해시가 같으면 "동일 패턴 재발견"으로 간주해
16
+ * `reExtracted` 카운터만 증가(기존 신뢰도는 건드리지 않음 — 이미 solution-writer.ts
17
+ * 의 dual-path 금지 불변식과 일치). 다르면 절대 덮어쓰지 않고 suffix된
18
+ * 이름으로 새로 생성.
19
+ * - 신규 생성되는 패턴은 항상 probation: status='experiment', confidence는
20
+ * 신규 솔루션 표준 베이스라인(statusConfidence('experiment')=0.3, 참고:
21
+ * extraction-persistence.ts saveExtractedSolution)을 상한으로 export 시점
22
+ * confidence의 절반만 반영. evidence는 전부 0으로 리셋 — exporter의 로컬
23
+ * 사용 이력을 이 머신이 검증 없이 물려받지 않는다. 이후 승급은 기존
24
+ * compound-lifecycle.ts의 단일 경로(runLifecycleCheck)로만 진행된다.
25
+ */
26
+ import { type SolutionFrontmatter } from './solution-format.js';
27
+ export declare const SHARE_BUNDLE_SCHEMA_VERSION: 1;
28
+ export interface ShareBundlePatternV1 {
29
+ name: string;
30
+ category: 'solution' | 'rule';
31
+ frontmatter: SolutionFrontmatter;
32
+ context: string;
33
+ content: string;
34
+ /** sha256(type + sorted tags/identifiers + context + content) — confidence/evidence/timestamp 제외한 콘텐츠 지문. */
35
+ contentHash: string;
36
+ }
37
+ export interface ShareBundleV1 {
38
+ schemaVersion: 1;
39
+ exportedAt: string;
40
+ /** sha256(hostname:username) 앞 16자 — 원본 식별용, PII 비가역. */
41
+ originHash: string;
42
+ patterns: ShareBundlePatternV1[];
43
+ }
44
+ export interface BuildBundleResult {
45
+ bundle: ShareBundleV1;
46
+ notFound: string[];
47
+ rejectedSecrets: string[];
48
+ }
49
+ /**
50
+ * 이름 목록으로 패턴 번들 생성.
51
+ *
52
+ * 시크릿 감지된 패턴은 조용히 스킵하지 않고 `rejectedSecrets`로 보고하되,
53
+ * 번들 자체는 나머지 clean한 패턴으로 계속 진행한다 (부분 실패 허용).
54
+ */
55
+ export declare function buildShareBundle(names: string[]): BuildBundleResult;
56
+ export interface ShareBundleValidation {
57
+ ok: boolean;
58
+ bundle: ShareBundleV1 | null;
59
+ errors: string[];
60
+ }
61
+ /**
62
+ * 번들 검증: 크기 캡 → 최상위 필드 화이트리스트 → 패턴별 필드 화이트리스트 →
63
+ * frontmatter 정합성 → contentHash 재계산 일치. 하나라도 실패하면 번들 전체를
64
+ * reject한다 (부분 신뢰 없음 — 손상/변조된 번들은 통째로 버린다).
65
+ */
66
+ export declare function validateShareBundle(raw: unknown, rawSize: number): ShareBundleValidation;
67
+ export interface ShareImportAction {
68
+ sourceName: string;
69
+ category: 'solution' | 'rule';
70
+ action: 'merge-reextract' | 'create' | 'create-suffixed';
71
+ targetName: string;
72
+ detail: string;
73
+ }
74
+ export interface ShareImportSummary {
75
+ dryRun: boolean;
76
+ actions: ShareImportAction[];
77
+ }
78
+ /** dry-run과 실제 실행이 공유하는 계획 수립 — 파일시스템에 아무것도 쓰지 않는다. */
79
+ export declare function planShareImport(bundle: ShareBundleV1): ShareImportAction[];
80
+ export declare function executeShareImport(bundle: ShareBundleV1, opts?: {
81
+ dryRun?: boolean;
82
+ }): ShareImportSummary;
83
+ export declare function looksLikeShareBundle(filePath: string): boolean;
84
+ export declare function handleShareExport(args: string[]): Promise<void>;
85
+ export declare function handleShareImport(args: string[]): Promise<void>;