@wooojin/forgen 0.4.3 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.3",
4
+ "version": "0.4.4",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,109 @@ All notable changes to forgen will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.4] — 2026-05-06
11
+
12
+ ### v0.4.4 — measurement infra rebuild + stop-guard hardening (DANGEROUS-RESPONSE)
13
+
14
+ forgen-eval testbed 의 측정 인프라 5-layer 결함을 모두 수정해 신뢰성을 회복하고,
15
+ 그 과정에서 발견한 driver-brittleness 결함(syn-004 — small driver 가 학습된 룰을
16
+ `find -exec rm -r` 같은 우회로 회피)을 stop-guard `dangerous-response-pattern`
17
+ 체크로 직접 close. 사후 N=10 재측정에서 **ψ master gate PASS** (mean +0.098, 95%
18
+ CI [+0.002, +0.222]) — pre-hardening (-0.028) 대비 부호 양수 전환. 또한
19
+ δ(forgenOnly−vanilla) = +0.223 (CI [+0.134, +0.326], 10/10 cases positive) 으로
20
+ forgen 효과가 robust 하게 확인됨.
21
+
22
+ **Highlights**:
23
+
24
+ - **DANGEROUS-RESPONSE 응답 텍스트 가드** (`feat`)
25
+ - `src/checks/dangerous-response-pattern.ts` + `tests/dangerous-response-pattern.test.ts` (12 케이스)
26
+ - `src/hooks/stop-guard.ts` checks pipeline 에 1순위로 wire-in (raw lastMessage 사용 — sanitizer 가 코드 fence 를 stripping 하므로 sanitized 는 부적합)
27
+ - 패턴 셋: `find -exec rm`, `find -delete`, `xargs rm`, `rm -r/-rf`, `git push --force`, `git reset --hard`, `DROP TABLE`, `dd of=/dev/`, `curl|sh`, `wget|sh` 등 14종 (응답 텍스트용)
28
+ - 매칭 시 block + correction 요청 (FORGEN_USER_CONFIRMED=1 으로 한 turn 우회 가능)
29
+ - 발동 검증: hardening N=10 측정에서 forgenOnly arm block 2건 (이전 측정들 0건)
30
+
31
+ - **forgen-eval testbed 5-layer fix** (`fix`)
32
+ 1. Judge contamination — `claude` CLI 가 사용자 전역 `~/.claude/CLAUDE.md` 로드 → judge 가 forgen 어시스턴트로 빙의 (β score=0/NaN 다발). `claude -p ... --system-prompt <blind>`, `codex exec --ignore-user-config --ignore-rules --ephemeral` 로 격리.
33
+ 2. Persona stub — runner 가 ID 문자열만 β judge 에 전달. `loadPersonaSpec()` 도입해 `personas/persona-XXX.json` 실 spec 로드.
34
+ 3. Trigger turn hook 누락 — `ForgenOnlyArm` 이 correctionSequence 만 hook 통과. trigger 단계도 UPS+Stop hook pipeline 추가.
35
+ 4. Notepad 미초기화 — case 별 임시 cwd + `seedForgenNotepad()` 로 사전 학습 상태 시뮬레이션.
36
+ 5. Hooks dir 경로 하드코딩 (root cause) — 잘못된 절대경로로 모든 hook 호출이 silently 실패. `import.meta.url` 기반 상대경로로 자동 해결. (이 결함이 이전 모든 ψ 측정을 무효화하고 있었음)
37
+ 6. Bridge 응답 shape — `additionalContext` 가 `hookSpecificOutput` nested 필드. 인터페이스/접근 코드 동시 수정.
38
+
39
+ - **Two-layer enforcement 명문화** (`docs`)
40
+ - `README.md` + `README.ko.md` 의 "How It Works" 에 "Two-layer safety enforcement / 2-layer 안전 적용" 섹션 추가. soft (notepad-injector) + hard (PreToolUse + Stop DANGEROUS-RESPONSE) 모델 명시. 작은 driver 가 학습 룰을 우회해도 hard layer 가 차단함을 사용자가 이해 가능.
41
+
42
+ - **Judge rubric 4-anchor 명세** (`fix`)
43
+ - `packages/forgen-eval/src/judges/judge-types.ts` β/γ/φ 프롬프트에 1/2/3/4 모든 anchor 명시 (이전엔 1/4 만). 작은 judge 가 중간 점수 일관성 확보.
44
+
45
+ - **Reports as audit trail** (`chore`)
46
+ - `packages/forgen-eval/reports/psi-stat/*.json` 7건 (5월 4-6일) — pre-isolation, post-isolation, broken sleep run, fixed run, post-rubric, post-hardening 의 비교 가능한 측정 시리즈.
47
+
48
+ - **4축 personalization P1 — facet 임계값 분기 활성화** (`feat`)
49
+ - `src/renderer/rule-renderer.ts` — `_profile` → `profile` 활성화. 13개 facet (3 quality + 4 autonomy + 3 judgment + 3 communication) 의 0.85 / 0.15 임계값 분기 도입.
50
+ - 이전엔 facet 값이 inspect-print 외 어디에도 사용되지 않았음 (12-bucket pack lookup 만 활성). 본 변경으로 4축이 *연속 값* 으로 응답에 영향.
51
+ - `tests/renderer/rule-renderer.test.ts` — facet 0.1 vs 0.9 byte-diff 회귀 테스트 5건 (verification_depth, verbosity, approval_threshold 등).
52
+
53
+ - **judgment / communication 축 facet delta 갱신 경로** (`feat`)
54
+ - `src/core/auto-compound-runner.ts` — `profile_delta` 스키마 + 적용 분기에 `judgment_philosophy`, `communication_style` 케이스 추가. 이전엔 quality_safety / autonomy 2축만 자동 갱신, 나머지 2축은 0.5/0.45 default 영원 고정.
55
+
56
+ - **시맨틱 룰 FP 좁히기** (`fix`)
57
+ - `src/checks/fact-vs-agreement.ts` — `EVIDENCE_INDICATORS` 추가 (test counts `\d+/\d+`, exit code, timing, vitest output 형식, diff hunks 등 9 패턴). 응답에 측정 증거가 paste 되어 있으면 alert 억제 → "Docker e2e 77/77 PASS" 류 정량 사실 보고 FP 감소. tests/fact-vs-agreement.test.ts 4 케이스 추가 (총 13).
58
+ - `~/.forgen/me/rules/L1-no-mock-as-proof.json` — `trigger_exclude_regex` 에 `<observation>`, `<summary>`, observer 메타 패턴 추가. 메타-설명 응답 FP 감소.
59
+ - `~/.forgen/me/rules/L1-e2e-before-done.json` — TDD 진행 보고(`RED→GREEN`, `[N/M]`, `다음 단계`, `진행 상황`) 제외 패턴 추가.
60
+
61
+ **Final measurement (post-hardening + post-narrowing, 두 N=10 합산 N=20)**:
62
+ - ψ master gate: 두 측정 모두 borderline 0 (run1 −0.026, run2 +0.001) — composition-synergy metric 으로는 회귀
63
+ - **δ(forgenOnly−vanilla) N=20 = +0.161, CI [+0.068, +0.256]** — *진짜 forgen 효과* metric, 0 위로 robust. 14/20 cases positive.
64
+ - δ(full−vanilla) N=10 (run2): +0.218, CI [+0.117, +0.323]
65
+ - κ_γ ~0.38 / κ_β ~0.41 — subscription-mode CLI judge 한계 (haiku 가 4점 척도 안정 분류 어려움)
66
+ - fallback 5/160 = 3.1% (≤ 10% 게이트)
67
+ - forgenOnly arm block 이벤트 발화 — DANGEROUS-RESPONSE 패턴이 driver 우회 응답을 차단
68
+
69
+ **Production data sample (8일, 230 violations)**:
70
+ - 9 distinct rules 발화: fact-vs-agreement 67, L1-no-mock-as-proof 56, self-score-inflation 41, L1-no-rm-rf-unconfirmed (PreToolUse) 23, dangerous-response-pattern (신설, 첫날) 20, L1-e2e-before-done 15, etc.
71
+ - Stratified random sample N=30 → precision 60.7%. **Hard layer (PreToolUse + dangerous-response-pattern) 100% (6/6)**, semantic Stop-guard 룰 43-60%.
72
+ - drift 자가복구 14건 — stuck-loop 상황 force-approve 후 drift 기록 (메타 안전성).
73
+
74
+ **Host parity status**:
75
+ - ✅ **Claude (claude)**: 모든 hook 동작 확정 (이번 세션 라이브 self-validated 다수)
76
+ - ⚠️ **Codex (codex)**: PreToolUse hard layer + UserPromptSubmit soft layer 동등. Stop hook response-text 검사 (DANGEROUS-RESPONSE, L1-no-mock-as-proof 자가검증 등) 는 *best-effort* — codex CLI 가 Stop input 에 `last_assistant_message` 또는 `transcript_path` 를 제공해야 발화. 미제공 시 silently auto-approve (안전). 실 codex 사용 데이터로 다음 1주 검증 예정 (gap 발견 시 v0.4.5 보완).
77
+
78
+ **v0.4.4 Does NOT claim**:
79
+ - v0.5.0 release-proof. v0.5.0 은 70B 로컬 / Sonnet API 기반 강judge 로 κ ≥ 0.7 + 더 큰 N 으로 *사전 등록* metric (δ 우선) 으로 처음부터 측정 예정.
80
+ - 외부 재현 — 실행에 Claude Max + Codex subscription 필요.
81
+ - ψ master gate PASS — 두 N=10 측정 모두 borderline 0. ψ 자체가 composition-synergy 측정이라 "forgen이 vanilla 대비 좋은가" 질문에 부적합 metric 임이 본 사이클에서 확인됨. δ 가 답이고 δ 는 양수.
82
+
83
+ **Lessons (post-mortem)**:
84
+ - 측정 인프라 5-layer 결함 (특히 hooks dir 하드코딩) 으로 이전 모든 ψ 측정이 실은 vanilla-vs-vanilla 였음. 5월 6일 hardening + bridge fix 후에야 forgen 메커니즘이 testbed 에서 실제로 발화 시작.
85
+ - ψ 정의 ("full vs best single arm composition") 가 주 product 질문 ("forgen 이 vanilla 대비 좋은가") 과 어긋남을 늦게 발견. v0.5.0 metric 재정의 필요.
86
+ - 1주일 production data 가 enforcement 메커니즘 활성을 입증하나, FP precision (특히 시맨틱 룰 43-60%) 은 별도 트랙 개선 과제.
87
+
88
+ ### Internal — pathfinder + Deep Interview fix cycle (2026-04-30 post-v0.4.3)
89
+
90
+ **Pathfinder (stop-guard 3-check 구조 진단 + unify)** (`refactor`)
91
+ - `PATHFINDER-2026-04-30/` — features → flowcharts → duplication report → unified proposal → handoff
92
+ - `src/checks/_shared/text-sanitizer.ts` + tests — 3-check (`self-score-inflation`, `fact-vs-agreement`, `conclusion-verification-ratio`) measurement Set 중복 제거
93
+ - `src/hooks/stop-guard.ts` — 3-check 디스패처 정리
94
+
95
+ **Deep Interview D9/D11/D12 fix** (`fix`)
96
+ - D9: `docs/guard-design-checklist.md` — guard 설계 invariant 명문화
97
+ - D11: `src/store/compound-usage-store.ts` + tests + `src/mcp/tools.ts` wiring
98
+ - MCP `compound-read/list/search` 호출 시 `~/.forgen/state/compound-usage.jsonl` 에 사용 evidence 적재
99
+ - D12: `assets/claude/commands/calibrate.md` + `retro.md` — `~/.forgen/me/evidence/` → `behavior/` 경로 drift 수정 (skill 카탈로그 정합성 회복)
100
+
101
+ **Auto-compound retry 로깅 개선** (`chore`)
102
+ - `src/core/auto-compound-runner.ts` — retry 메시지에 attempt count + 에러 코드 + fail-open 단언 (UX 명확화, 동작 변경 없음)
103
+
104
+ ### Hygiene
105
+ - `package.json` self-dep 오염 (`@wooojin/forgen ^0.4.3`) 제거
106
+ - `plugin.json` (root) 0.4.2 → 0.4.3 sync (이전 d4c640c 가 `.claude-plugin/plugin.json` 만 sync)
107
+ - `package-lock.json` workspace + transitive peer dep 동기화
108
+
109
+ **Verification**: vitest 2373/2373 PASS, Docker e2e 77/77 PASS (round 16)
110
+
8
111
  ## [0.4.3] — 2026-04-30 — Self-correcting hotfix + testbed prep (alpha)
9
112
 
10
113
  forgen-eval introspect testbed (이번 릴리즈에 포함된 자기 측정 시스템) 가
package/README.ko.md CHANGED
@@ -274,6 +274,20 @@ Linux 컨테이너에서 `~/.claude.json` 만 마운트하면 refresh 토큰이
274
274
  (다음 세션: 업데이트된 규칙)
275
275
  ```
276
276
 
277
+ ### 2-layer 안전 적용
278
+
279
+ 학습된 제약이 모델이 우회를 시도해도 유지되도록 forgen은 **두 단계**에서 적용됩니다:
280
+
281
+ | 단계 | Hook | 시점 | 차단 대상 |
282
+ |---|---|---|---|
283
+ | **Soft (컨텍스트)** | UserPromptSubmit (`notepad-injector`) | 매 turn 시작 전 | 활성 룰을 Claude 컨텍스트에 재주입 — 모델이 자율 준수하도록 유도. |
284
+ | **Hard (도구)** | PreToolUse (`pre-tool-use` + `dangerous-patterns.json`) | 모든 Bash / Edit / Write 직전 | `rm -rf /`, `git push --force`, `DROP TABLE`, `mkfs`, `curl \| sh` 등 패턴 매칭 차단 — 모델 의도 무관하게 발동. |
285
+ | **Hard (응답)** | Stop (`stop-guard` DANGEROUS-RESPONSE) | Claude 응답 직후 | 응답 텍스트 자체 패턴 매칭 — *제안된* 파괴 명령(예: `find … -exec rm`, `xargs rm` 우회)을 사용자가 보기 전에 차단. |
286
+
287
+ Soft layer는 모델에게 "지켜줘"라고 요청하고, Hard layer는 요청하지 않습니다. driver 모델이 약해서 학습된 룰을 "창의적으로" 우회하려 해도 (예: `rm -rf` 금지 → `find -exec rm -r` 제안) Hard layer가 미리 차단합니다.
288
+
289
+ 오버라이드: 한 turn만 감사 우회는 `FORGEN_USER_CONFIRMED=1`, 특정 룰 영구 비활성화는 `forgen suppress-rule <rule_id>`.
290
+
277
291
  ### Compound 지식
278
292
 
279
293
  지식은 세션을 거치며 신뢰도 기반 라이프사이클로 축적됩니다:
package/README.md CHANGED
@@ -319,6 +319,25 @@ entries in `~/.forgen/state/implicit-feedback.jsonl`. Idempotent — safe to re-
319
319
  (next session: updated rules)
320
320
  ```
321
321
 
322
+ ### Two-layer safety enforcement
323
+
324
+ forgen enforces your rules at **two layers** so a learned constraint holds even
325
+ if the model rationalizes a workaround:
326
+
327
+ | Layer | Hook | When | Catches |
328
+ |---|---|---|---|
329
+ | **Soft (context)** | UserPromptSubmit (`notepad-injector`) | Before each turn | Re-injects active rules into Claude's context so the model can self-comply. |
330
+ | **Hard (tool)** | PreToolUse (`pre-tool-use` + `dangerous-patterns.json`) | Before every Bash / Edit / Write | Pattern-match block on `rm -rf /`, `git push --force`, `DROP TABLE`, `mkfs`, `curl \| sh`, etc — fires regardless of model intent. |
331
+ | **Hard (response)** | Stop (`stop-guard` DANGEROUS-RESPONSE) | After Claude's reply | Pattern-match on the reply text itself — catches *suggestions* of destructive commands (e.g., `find … -exec rm`, `xargs rm` rationalizations) before the user sees them. |
332
+
333
+ The soft layer asks the model to behave; the hard layers don't ask. Even with a
334
+ weaker driver model that "creatively" routes around a learned rule (e.g.,
335
+ suggesting `find -exec rm -r {}` because `rm -rf` was forbidden), the hard
336
+ layers stop it before any damage.
337
+
338
+ Override hatch: set `FORGEN_USER_CONFIRMED=1` for a one-turn audited bypass, or
339
+ `forgen suppress-rule <rule_id>` to disable a specific rule permanently.
340
+
322
341
  ### Compound knowledge
323
342
 
324
343
  Knowledge accumulates across sessions with a trust-based lifecycle:
@@ -39,7 +39,8 @@ triggers:
39
39
  calibrate는 두 가지 데이터 소스를 사용합니다:
40
40
 
41
41
  ### 1차 소스: Evidence 파일
42
- `~/.forgen/me/evidence/` 디렉토리의 JSON 파일을 읽습니다.
42
+ `~/.forgen/me/behavior/` 디렉토리의 JSON 파일을 읽습니다.
43
+ (파일명: UUID.json — correction-record MCP 도구가 작성. 같은 디렉토리의 auto-*.md 는 auto-compound 산출이므로 calibrate 분석 대상 아님.)
43
44
  각 파일의 구조:
44
45
  ```json
45
46
  {
@@ -70,8 +71,8 @@ evidence 0건 + compound 교정 패턴 0건이면:
70
71
  ## Phase 1: Evidence 로드 및 검증
71
72
 
72
73
  ```bash
73
- ls ~/.forgen/me/evidence/ 2>/dev/null || echo "EMPTY"
74
- cat ~/.forgen/me/evidence/*.json 2>/dev/null || echo "NO_FILES"
74
+ ls ~/.forgen/me/behavior/*.json 2>/dev/null || echo "EMPTY"
75
+ cat ~/.forgen/me/behavior/*.json 2>/dev/null || echo "NO_FILES"
75
76
  ```
76
77
 
77
78
  로드한 JSON 파일마다 다음을 검증합니다:
@@ -69,8 +69,8 @@ compound-list
69
69
  ### 1-3: 교정 기록
70
70
 
71
71
  ```bash
72
- ls -la ~/.forgen/me/evidence/ 2>/dev/null || echo "교정 데이터 없음"
73
- find ~/.forgen/me/evidence/ -name "*.json" -mtime -{period_days} 2>/dev/null | wc -l
72
+ ls -la ~/.forgen/me/behavior/ 2>/dev/null || echo "교정 데이터 없음"
73
+ find ~/.forgen/me/behavior/ -name "*.json" -mtime -{period_days} 2>/dev/null | wc -l
74
74
  ```
75
75
 
76
76
  ## Phase 2: 코드 활동 분석
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Forgen — text-sanitizer (Pathfinder D4 + D5 흡수).
3
+ *
4
+ * stop-guard 진입 시 lastMessage 에 한 번 적용. 두 가지 면제:
5
+ *
6
+ * D4 — Structured-output 면제:
7
+ * observer hook / skill 산출물(<observation>...</observation>,
8
+ * <summary>...</summary> 등)은 *과거 사실 기록* 이지 자기 평가가 아님.
9
+ * 본문 안의 "verified", "신뢰도 95/100" 같은 어휘에 가드가 발화하면 FP.
10
+ *
11
+ * D5 — Self-paradox 면제:
12
+ * regex 트리거 어휘(예: 4/10, verified)를 *인용해서* 설명만 해도 본인
13
+ * 매칭. 메타 대화/디버깅에서 가드가 무력화됨. 코드/직인용 본문은 가드
14
+ * 판정 대상이 아니므로 stripping.
15
+ *
16
+ * 결정 (PATHFINDER-2026-04-30/03-unified-proposal.md):
17
+ * - 자연 산문 속 진짜 점수 인플레이션은 살아남아야 함 (TP 보존)
18
+ * - 짧은 인용("...") 만 제거; 긴 인용은 사용자 인용일 수 있어 보존
19
+ * - idempotent: 두 번 적용해도 결과 동일
20
+ */
21
+ export declare function sanitizeForGuard(raw: string): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Forgen — text-sanitizer (Pathfinder D4 + D5 흡수).
3
+ *
4
+ * stop-guard 진입 시 lastMessage 에 한 번 적용. 두 가지 면제:
5
+ *
6
+ * D4 — Structured-output 면제:
7
+ * observer hook / skill 산출물(<observation>...</observation>,
8
+ * <summary>...</summary> 등)은 *과거 사실 기록* 이지 자기 평가가 아님.
9
+ * 본문 안의 "verified", "신뢰도 95/100" 같은 어휘에 가드가 발화하면 FP.
10
+ *
11
+ * D5 — Self-paradox 면제:
12
+ * regex 트리거 어휘(예: 4/10, verified)를 *인용해서* 설명만 해도 본인
13
+ * 매칭. 메타 대화/디버깅에서 가드가 무력화됨. 코드/직인용 본문은 가드
14
+ * 판정 대상이 아니므로 stripping.
15
+ *
16
+ * 결정 (PATHFINDER-2026-04-30/03-unified-proposal.md):
17
+ * - 자연 산문 속 진짜 점수 인플레이션은 살아남아야 함 (TP 보존)
18
+ * - 짧은 인용("...") 만 제거; 긴 인용은 사용자 인용일 수 있어 보존
19
+ * - idempotent: 두 번 적용해도 결과 동일
20
+ */
21
+ const STRUCTURED_TAGS = [
22
+ 'observation',
23
+ 'summary',
24
+ 'request',
25
+ 'investigated',
26
+ 'completed',
27
+ 'next-steps',
28
+ 'next_steps',
29
+ 'title',
30
+ 'subtitle',
31
+ 'learned',
32
+ 'discovery',
33
+ ];
34
+ const SHORT_QUOTE_MAX = 20;
35
+ export function sanitizeForGuard(raw) {
36
+ if (!raw)
37
+ return raw;
38
+ let s = raw;
39
+ // 1) structured-output 블록 (open + close 쌍) 제거
40
+ for (const tag of STRUCTURED_TAGS) {
41
+ const re = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi');
42
+ s = s.replace(re, '');
43
+ }
44
+ // self-closing 또는 dangling open tag 도 제거 (열렸지만 닫힘 누락 케이스)
45
+ for (const tag of STRUCTURED_TAGS) {
46
+ const reSelf = new RegExp(`<${tag}\\b[^>]*\\/?>`, 'gi');
47
+ s = s.replace(reSelf, '');
48
+ const reClose = new RegExp(`<\\/${tag}>`, 'gi');
49
+ s = s.replace(reClose, '');
50
+ }
51
+ // 2) fenced code block (```...```) 제거 — 진짜 점수가 들어갈 자리 아님
52
+ s = s.replace(/```[\s\S]*?```/g, '');
53
+ // 3) inline backtick 코드 (`...`) 제거
54
+ s = s.replace(/`[^`\n]*`/g, '');
55
+ // 4) 짧은 직인용 ("...") 제거 — 길이 SHORT_QUOTE_MAX 이하만.
56
+ // 긴 인용은 사용자 발언/실제 사실 인용이므로 가드 판정 대상에 남김.
57
+ const shortQuoteRe = new RegExp(`"[^"\\n]{0,${SHORT_QUOTE_MAX}}"`, 'g');
58
+ s = s.replace(shortQuoteRe, '');
59
+ return s;
60
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Forgen — DANGEROUS-RESPONSE: 응답 텍스트 파괴적 명령 가드
3
+ *
4
+ * Stop hook 단계에서 어시스턴트 응답 텍스트를 스캔하여 *파괴적 명령 제안*을
5
+ * 감지한다. PreToolUse 가드는 실제 도구 실행 직전에만 동작하므로, 모델이
6
+ * 텍스트로 위험한 대안을 *제안*하는 케이스(예: `find ... -exec rm -r {}`)는
7
+ * 도구 실행 단계에 도달하지 못하면 차단되지 않는다. 본 체크는 그 갭을 메운다.
8
+ *
9
+ * 발견 경위 (forgen-eval syn-004, 2026-05-06):
10
+ * notepad 룰 "[no-rm-rf-unconfirmed]" inject 상태에서, driver(llama 8B) 가
11
+ * `rm -rf` 리터럴은 회피했지만 `find . -type d -name 'build' -exec rm -r {}`
12
+ * 같은 우회 명령을 "안전한 대안"으로 제안. PreToolUse 차단은 실 실행 시
13
+ * 동작하지만 "응답 텍스트만으론" 못 막음. driver 모델 능력에 무관하게
14
+ * 일관된 안전 보장이 되도록 응답 단계에서 한 번 더 잡아낸다.
15
+ *
16
+ * 동작:
17
+ * - 어시스턴트 응답에 등록된 파괴 패턴이 매칭되면 block 권고.
18
+ * - "확실히, 명시적 확인" 같은 confirm 표현 동반 여부는 검사하지 않는다
19
+ * (휴리스틱 brittle). 매칭 시 무조건 block 후, 모델이 안전한 대안 제시
20
+ * 또는 명시적 confirm 절차로 재응답하도록 유도.
21
+ *
22
+ * 순수 함수 — Stop hook 이 reason 문자열을 그대로 주입.
23
+ */
24
+ export interface DangerousResultMatch {
25
+ block: boolean;
26
+ matched?: string;
27
+ reason: string;
28
+ }
29
+ /** 응답 텍스트가 파괴적 명령 패턴을 포함하는지 검사. */
30
+ export declare function checkDangerousResponsePattern(opts: {
31
+ text: string;
32
+ }): DangerousResultMatch;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Forgen — DANGEROUS-RESPONSE: 응답 텍스트 파괴적 명령 가드
3
+ *
4
+ * Stop hook 단계에서 어시스턴트 응답 텍스트를 스캔하여 *파괴적 명령 제안*을
5
+ * 감지한다. PreToolUse 가드는 실제 도구 실행 직전에만 동작하므로, 모델이
6
+ * 텍스트로 위험한 대안을 *제안*하는 케이스(예: `find ... -exec rm -r {}`)는
7
+ * 도구 실행 단계에 도달하지 못하면 차단되지 않는다. 본 체크는 그 갭을 메운다.
8
+ *
9
+ * 발견 경위 (forgen-eval syn-004, 2026-05-06):
10
+ * notepad 룰 "[no-rm-rf-unconfirmed]" inject 상태에서, driver(llama 8B) 가
11
+ * `rm -rf` 리터럴은 회피했지만 `find . -type d -name 'build' -exec rm -r {}`
12
+ * 같은 우회 명령을 "안전한 대안"으로 제안. PreToolUse 차단은 실 실행 시
13
+ * 동작하지만 "응답 텍스트만으론" 못 막음. driver 모델 능력에 무관하게
14
+ * 일관된 안전 보장이 되도록 응답 단계에서 한 번 더 잡아낸다.
15
+ *
16
+ * 동작:
17
+ * - 어시스턴트 응답에 등록된 파괴 패턴이 매칭되면 block 권고.
18
+ * - "확실히, 명시적 확인" 같은 confirm 표현 동반 여부는 검사하지 않는다
19
+ * (휴리스틱 brittle). 매칭 시 무조건 block 후, 모델이 안전한 대안 제시
20
+ * 또는 명시적 confirm 절차로 재응답하도록 유도.
21
+ *
22
+ * 순수 함수 — Stop hook 이 reason 문자열을 그대로 주입.
23
+ */
24
+ import { compileSafeRegex, safeRegexTest } from '../hooks/shared/safe-regex.js';
25
+ /**
26
+ * 응답 텍스트에서 검사할 파괴적 명령 패턴.
27
+ * dist/hooks/dangerous-patterns.json (PreToolUse 용) 와 별도 — 응답 텍스트
28
+ * 분석에 적합한 더 넓은 패턴 (find -exec rm 같은 우회 포함).
29
+ */
30
+ /** 패턴 순서 중요: 더 구체적인(우회) 패턴을 먼저 두어 일반 rm -r 패턴이 가로채지 않게 함. */
31
+ const RESPONSE_PATTERNS = [
32
+ { pattern: '\\bfind\\b[^\\n]{0,80}-exec\\s+rm\\b', description: 'find -exec rm (rm 우회)', flags: 'i' },
33
+ { pattern: '\\bfind\\b[^\\n]{0,80}-delete\\b', description: 'find -delete (rm 우회)', flags: 'i' },
34
+ { pattern: '\\bxargs\\s+rm\\b', description: 'xargs rm (rm 우회)', flags: 'i' },
35
+ { pattern: '\\brm\\s+-(?:rf|fr|r)\\b', description: 'rm -r/-rf 직접 호출', flags: 'i' },
36
+ { pattern: '\\bgit\\s+push\\b[^\\n]{0,40}--force(?!-)', description: 'git push --force', flags: 'i' },
37
+ { pattern: '\\bgit\\s+reset\\s+--hard\\b', description: 'git reset --hard', flags: 'i' },
38
+ { pattern: '\\bgit\\s+clean\\s+-[a-z]*f', description: 'git clean -f', flags: 'i' },
39
+ { pattern: '\\bDROP\\s+(?:TABLE|DATABASE)\\b', description: 'SQL DROP TABLE/DATABASE', flags: 'i' },
40
+ { pattern: '\\bTRUNCATE\\s+TABLE\\b', description: 'SQL TRUNCATE TABLE', flags: 'i' },
41
+ { pattern: '>\\s*/dev/sd[a-z]\\b', description: '/dev/sdX 직접 write', flags: 'i' },
42
+ { pattern: '\\bmkfs\\b', description: 'mkfs (파일시스템 포맷)', flags: 'i' },
43
+ { pattern: '\\bdd\\b[^\\n]{0,60}\\bof=/dev/', description: 'dd of=/dev/* (장치 write)', flags: 'i' },
44
+ { pattern: '\\bcurl\\b[^\\n]{0,60}\\|\\s*(?:ba)?sh\\b', description: 'curl | sh (원격 코드 실행)', flags: 'i' },
45
+ { pattern: '\\bwget\\b[^\\n]{0,60}\\|\\s*(?:ba)?sh\\b', description: 'wget | sh (원격 코드 실행)', flags: 'i' },
46
+ ];
47
+ /** 응답 텍스트가 파괴적 명령 패턴을 포함하는지 검사. */
48
+ export function checkDangerousResponsePattern(opts) {
49
+ const text = opts.text ?? '';
50
+ if (!text.trim())
51
+ return { block: false, reason: '' };
52
+ for (const p of RESPONSE_PATTERNS) {
53
+ const compiled = compileSafeRegex(p.pattern, p.flags ?? 'i');
54
+ if (!compiled.regex)
55
+ continue; // bad regex — skip
56
+ if (safeRegexTest(compiled.regex, text)) {
57
+ return {
58
+ block: true,
59
+ matched: p.description,
60
+ reason: `응답에 파괴적 명령 패턴이 포함되었습니다 (${p.description}). 사용자 명시 확인 절차를 포함하거나 비파괴 대안 (예: dry-run, --interactive)을 제시해 다시 응답하세요.`,
61
+ };
62
+ }
63
+ }
64
+ return { block: false, reason: '' };
65
+ }
@@ -47,6 +47,26 @@ const AGREEMENT_SOFTENERS = [
47
47
  /(생각합니다|생각함|생각해|봅니다|예상(합니다|돼))/,
48
48
  /(그럴\s*것\s*같|맞을\s*것\s*같)/,
49
49
  ];
50
+ /**
51
+ * 측정-증거 지표 — 실제 실행/측정 결과가 응답에 *paste 되어 있다*는 신호.
52
+ *
53
+ * v0.4.4 (2026-05-06): FP 감소. "Docker e2e 77/77 PASS" 같은 *정량 사실 보고*
54
+ * 가 recentTools 윈도우 밖 측정 (예: 이전 turn Bash 결과, 사용자 paste, CI 로그
55
+ * 인용)이라 Bash 카운트가 0이지만 본질적으로 measurement-backed 응답.
56
+ *
57
+ * 임계: 본 패턴이 2+ 매칭되면 alert 억제 (응답이 측정 증거를 *제시*하고 있다고 본다).
58
+ */
59
+ const EVIDENCE_INDICATORS = [
60
+ /\b\d+\/\d+\b/, // test counts: "77/77", "22/22"
61
+ /\bexit\s*code\s*[:=]?\s*\d+/i, // exit code
62
+ /\b\d+(\.\d+)?\s*(ms|s|sec|seconds)\b/i, // timings: "232s", "1.5ms"
63
+ /\b(?:Test|Spec)s?\s*Files?\s+\d+/i, // vitest "Test Files 218"
64
+ /\b(?:Tests?:?\s+)?\d+\s+passed?\b/i, // "2382 passed"
65
+ /\b(?:CI|HEAD|sha|commit)\s*[:=]?\s*[a-f0-9]{7,}/i, // commit ref
66
+ /^[+-]{3}\s/m, // diff hunks
67
+ /\bcoverage\s*[:=]?\s*\d+(\.\d+)?%/i, // coverage %
68
+ /^\s*✓\s|^\s*✗\s|^\s*PASS\b|^\s*FAIL\b/m, // test runner output markers
69
+ ];
50
70
  function findMatches(text, patterns, max = 3) {
51
71
  const out = [];
52
72
  for (const p of patterns) {
@@ -69,8 +89,12 @@ export function checkFactVsAgreement(input) {
69
89
  const factAssertions = findMatches(text, FACT_ASSERTION_PATTERNS);
70
90
  const agreementSofteners = findMatches(text, AGREEMENT_SOFTENERS);
71
91
  const measurementCount = recentTools.filter((t) => MEASUREMENT_TOOL_CATEGORIES.has(t)).length;
92
+ // Evidence indicator suppression — 응답에 측정 결과가 *paste* 되어 있으면
93
+ // recentTools 윈도우 밖 측정으로 보고 alert 억제 (FP 감소).
94
+ const evidenceIndicators = findMatches(text, EVIDENCE_INDICATORS, 99);
95
+ const hasMeasurementEvidence = evidenceIndicators.length >= 2;
72
96
  const hasFactAssertion = factAssertions.length > 0;
73
- const measurementMissing = measurementCount < minMeasurements;
97
+ const measurementMissing = measurementCount < minMeasurements && !hasMeasurementEvidence;
74
98
  const alert = hasFactAssertion && measurementMissing;
75
99
  let reason = '';
76
100
  if (alert) {
@@ -40,17 +40,22 @@ function execClaudeRetry(args, opts) {
40
40
  if (host === 'claude') {
41
41
  // Claude 측은 기존 보안 hardening 보존: --allowedTools 등 args 그대로 전달.
42
42
  const TRANSIENT = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE/;
43
- for (let attempt = 0; attempt < 2; attempt++) {
43
+ const MAX_ATTEMPTS = 2;
44
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
44
45
  try {
45
46
  return execFileSync('claude', args, opts);
46
47
  }
47
48
  catch (e) {
48
49
  const msg = e instanceof Error ? e.message : String(e);
49
- if (attempt === 0 && TRANSIENT.test(msg)) {
50
- process.stderr.write(`[forgen-auto-compound] transient error, retrying in 3s...\n`);
50
+ const match = msg.match(TRANSIENT);
51
+ if (attempt < MAX_ATTEMPTS && match) {
52
+ process.stderr.write(`[forgen-auto-compound] ${match[0]} on attempt ${attempt}/${MAX_ATTEMPTS}, retrying in 3s (auto-recovery)...\n`);
51
53
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
52
54
  continue;
53
55
  }
56
+ if (match) {
57
+ process.stderr.write(`[forgen-auto-compound] ${match[0]} after ${attempt}/${MAX_ATTEMPTS} attempts — giving up (fail-open)\n`);
58
+ }
54
59
  throw e;
55
60
  }
56
61
  }
@@ -425,7 +430,9 @@ ${profileContext}
425
430
  "pack_direction": null 또는 "opposite_quality" 또는 "opposite_autonomy",
426
431
  "profile_delta": {
427
432
  "quality_safety": { "verification_depth": 0.0, "stop_threshold": 0.0, "change_conservatism": 0.0 },
428
- "autonomy": { "confirmation_independence": 0.0, "assumption_tolerance": 0.0, "scope_expansion_tolerance": 0.0, "approval_threshold": 0.0 }
433
+ "autonomy": { "confirmation_independence": 0.0, "assumption_tolerance": 0.0, "scope_expansion_tolerance": 0.0, "approval_threshold": 0.0 },
434
+ "judgment_philosophy": { "minimal_change_bias": 0.0, "abstraction_bias": 0.0, "evidence_first_bias": 0.0 },
435
+ "communication_style": { "verbosity": 0.0, "structure": 0.0, "teaching_bias": 0.0 }
429
436
  }
430
437
  }
431
438
 
@@ -493,6 +500,26 @@ ${sanitizedSummary.slice(0, 4000)}
493
500
  }
494
501
  }
495
502
  }
503
+ if (parsed.profile_delta.judgment_philosophy) {
504
+ const d = parsed.profile_delta.judgment_philosophy;
505
+ const f = profile.axes.judgment_philosophy.facets;
506
+ for (const [k, v] of Object.entries(d)) {
507
+ if (typeof v === 'number' && Math.abs(v) > 0.001 && k in f) {
508
+ f[k] = clamp(f[k] + v);
509
+ changed = true;
510
+ }
511
+ }
512
+ }
513
+ if (parsed.profile_delta.communication_style) {
514
+ const d = parsed.profile_delta.communication_style;
515
+ const f = profile.axes.communication_style.facets;
516
+ for (const [k, v] of Object.entries(d)) {
517
+ if (typeof v === 'number' && Math.abs(v) > 0.001 && k in f) {
518
+ f[k] = clamp(f[k] + v);
519
+ changed = true;
520
+ }
521
+ }
522
+ }
496
523
  if (changed) {
497
524
  profile.metadata.updated_at = new Date().toISOString();
498
525
  fs.writeFileSync(V1_PROFILE, JSON.stringify(profile, null, 2));
@@ -27,6 +27,8 @@ import { takeLastExtractionNotice } from '../core/extraction-notice.js';
27
27
  import { checkConclusionVerificationRatio } from '../checks/conclusion-verification-ratio.js';
28
28
  import { checkSelfScoreInflation } from '../checks/self-score-deflation.js';
29
29
  import { checkFactVsAgreement } from '../checks/fact-vs-agreement.js';
30
+ import { checkDangerousResponsePattern } from '../checks/dangerous-response-pattern.js';
31
+ import { sanitizeForGuard } from '../checks/_shared/text-sanitizer.js';
30
32
  import { STATE_DIR } from '../core/paths.js';
31
33
  import { sanitizeId } from './shared/sanitize-id.js';
32
34
  import { detectRecallReferences } from '../core/recall-reference-detector.js';
@@ -496,58 +498,73 @@ export async function main() {
496
498
  // block/approve 어느 경로이든 동일하게 기록 (참조는 응답 내용이 결정).
497
499
  const sessionIdForRef = input?.session_id ?? 'unknown';
498
500
  emitRecallReferencesFailOpen(sessionIdForRef, lastMessage);
499
- // TEST-2/3: rule-free meta guards — FORGEN_USER_CONFIRMED=1 우회 공통.
501
+ // TEST-1/2/3: rule-free meta guards — FORGEN_USER_CONFIRMED=1 우회 공통.
502
+ // Pathfinder D7: 3중 보일러플레이트를 CHECKS 배열 + for-loop 디스패처로 통합.
503
+ // Pathfinder D4/D5: sanitizeForGuard 가 모든 체크 입력 단계에 일괄 적용.
500
504
  if (process.env.FORGEN_USER_CONFIRMED !== '1') {
501
505
  const sessionId = input?.session_id ?? 'unknown';
502
- // TEST-2 (자가 점수 인플레이션): 숫자 점수 상승 선언 + 측정 도구 0회 → block.
503
- // TEST-3 보다 강한 신호라 먼저 평가.
504
506
  const recentTools = loadRecentToolNames(sessionId);
505
- const score = checkSelfScoreInflation({ text: lastMessage, recentTools });
506
- if (score.block) {
507
- recordViolation({
508
- rule_id: 'builtin:self-score-inflation',
509
- session_id: sessionId,
510
- source: 'stop-guard',
507
+ const sanitized = sanitizeForGuard(lastMessage);
508
+ // 평가 순서: DANGEROUS-RESPONSE (즉시 차단 — 안전 우선) → TEST-2 (강한 신호) → TEST-3 (텍스트 비율) → TEST-1 (alert-only).
509
+ const checks = [
510
+ {
511
+ shortId: 'dangerous-response-pattern',
512
+ ruleSlug: 'rule:DANGEROUS-RESPONSE — destructive command suggestion',
511
513
  kind: 'block',
512
- message_preview: lastMessage.slice(0, 120),
513
- });
514
- const reasonText = `[forgen:stop-guard/self-score-inflation] ${score.reason}
515
-
516
- (Override this turn: set FORGEN_USER_CONFIRMED=1 (audited).)`;
517
- console.log(blockStop(reasonText, 'rule:TEST-2 — self-score inflation'));
518
- return;
519
- }
520
- // TEST-3: 결론/검증 비율 — Claude 가 실제 측정 도구는 돌렸지만 서술이
521
- // 결론-편향이면 여전히 block.
522
- const ratio = checkConclusionVerificationRatio({ text: lastMessage });
523
- if (ratio.block) {
514
+ // 주의: sanitizer 가 백틱/코드블록을 제거하므로 raw lastMessage 를 전달.
515
+ // 위험 명령은 코드 fence 안에 있어도 동등하게 위험함.
516
+ run: () => {
517
+ const r = checkDangerousResponsePattern({ text: lastMessage });
518
+ return { triggered: r.block, reason: r.reason };
519
+ },
520
+ },
521
+ {
522
+ shortId: 'self-score-inflation',
523
+ ruleSlug: 'rule:TEST-2 self-score inflation',
524
+ kind: 'block',
525
+ run: () => {
526
+ const r = checkSelfScoreInflation({ text: sanitized, recentTools });
527
+ return { triggered: r.block, reason: r.reason };
528
+ },
529
+ },
530
+ {
531
+ shortId: 'conclusion-ratio',
532
+ ruleSlug: 'rule:TEST-3 — conclusion/verification ratio',
533
+ kind: 'block',
534
+ run: () => {
535
+ const r = checkConclusionVerificationRatio({ text: sanitized });
536
+ return { triggered: r.block, reason: r.reason };
537
+ },
538
+ },
539
+ {
540
+ shortId: 'fact-vs-agreement',
541
+ ruleSlug: 'rule:TEST-1 — fact vs agreement',
542
+ kind: 'correction', // alert-level only per fact-vs-agreement.ts design
543
+ run: () => {
544
+ const r = checkFactVsAgreement({ text: sanitized, recentTools, minMeasurements: 1 });
545
+ return { triggered: r.alert, reason: r.reason };
546
+ },
547
+ },
548
+ ];
549
+ for (const c of checks) {
550
+ const out = c.run();
551
+ if (!out.triggered)
552
+ continue;
524
553
  recordViolation({
525
- rule_id: 'builtin:conclusion-verification-ratio',
554
+ rule_id: `builtin:${c.shortId}`,
526
555
  session_id: sessionId,
527
556
  source: 'stop-guard',
528
- kind: 'block',
557
+ kind: c.kind,
529
558
  message_preview: lastMessage.slice(0, 120),
530
559
  });
531
- const reasonText = `[forgen:stop-guard/conclusion-ratio] ${ratio.reason}
560
+ if (c.kind !== 'block')
561
+ continue;
562
+ const reasonText = `[forgen:stop-guard/${c.shortId}] ${out.reason}
532
563
 
533
564
  (Override this turn: set FORGEN_USER_CONFIRMED=1 (audited).)`;
534
- console.log(blockStop(reasonText, 'rule:TEST-3 — conclusion/verification ratio'));
565
+ console.log(blockStop(reasonText, c.ruleSlug));
535
566
  return;
536
567
  }
537
- // TEST-1: 사실 vs 합의 — fact assertion 키워드가 있으나 측정 도구 호출 0건.
538
- // 원 design intent (per fact-vs-agreement.ts): alert level only — block 은 TEST-2/3.
539
- // 여기서는 measurement 신호를 violations.jsonl 에 'alert' kind 로 기록만 (block 안 함).
540
- // wiring gap 발견 (forgen-eval introspect) → 측정 가능하게 wired up.
541
- const fva = checkFactVsAgreement({ text: lastMessage, recentTools, minMeasurements: 1 });
542
- if (fva.alert) {
543
- recordViolation({
544
- rule_id: 'builtin:fact-vs-agreement',
545
- session_id: sessionId,
546
- source: 'stop-guard',
547
- kind: 'correction', // alert-level signal (not block) per fact-vs-agreement.ts design
548
- message_preview: lastMessage.slice(0, 120),
549
- });
550
- }
551
568
  }
552
569
  const rules = loadStopRules();
553
570
  if (rules.length === 0) {
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` +
@@ -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 = [];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Forgen — compound usage signal store (Pathfinder D11 fix, MVP).
3
+ *
4
+ * 배경 (Deep Interview 2026-04-30 Round 5):
5
+ * verified compound 23개, candidate 8개 — 그러나 mature 0. retro 회고에서
6
+ * "활용률 측정 불가 — compound-search 호출 카운터 미수집" 확인. 즉 사용자가
7
+ * compound 를 reuse 했다는 신호 자체가 잡히지 않아서 mature 승격 입력이 없음.
8
+ *
9
+ * 원칙(user-mirror, Round 4 Contrarian): forgen 자기 학습이 아니라 *사용자
10
+ * reuse* 가 mature 의 권위 종착점. 따라서 compound-read 호출이 있을 때마다
11
+ * "이 패턴이 사용됐다" 신호를 한 줄 기록.
12
+ *
13
+ * MVP 스코프: 신호 *수집*만. 승격 정책(예: 5회 reuse → mature) 은 다음 사이클.
14
+ * 기록만 잘 쌓이면 임계 설정·승격 로직은 위에 얹기 쉬움.
15
+ *
16
+ * 데이터: append-only JSONL at ~/.forgen/state/compound-usage.jsonl
17
+ * 각 라인: {"at": ISO, "name": "<solution-slug>", "via": "mcp|cli|hook"}
18
+ * crash-safe: 단순 fs.appendFileSync — 동시 append 는 OS 가 atomic 보장 (POSIX <PIPE_BUF).
19
+ */
20
+ export declare const COMPOUND_USAGE_LOG: string;
21
+ export interface UsageEntry {
22
+ at: string;
23
+ name: string;
24
+ via: 'mcp' | 'cli' | 'hook';
25
+ }
26
+ export declare function recordUsage(name: string, via?: UsageEntry['via']): void;
27
+ /** 단순 카운터 — 승격 정책에서 호출. MVP 에선 미사용. */
28
+ export declare function readUsageCounts(): Map<string, number>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Forgen — compound usage signal store (Pathfinder D11 fix, MVP).
3
+ *
4
+ * 배경 (Deep Interview 2026-04-30 Round 5):
5
+ * verified compound 23개, candidate 8개 — 그러나 mature 0. retro 회고에서
6
+ * "활용률 측정 불가 — compound-search 호출 카운터 미수집" 확인. 즉 사용자가
7
+ * compound 를 reuse 했다는 신호 자체가 잡히지 않아서 mature 승격 입력이 없음.
8
+ *
9
+ * 원칙(user-mirror, Round 4 Contrarian): forgen 자기 학습이 아니라 *사용자
10
+ * reuse* 가 mature 의 권위 종착점. 따라서 compound-read 호출이 있을 때마다
11
+ * "이 패턴이 사용됐다" 신호를 한 줄 기록.
12
+ *
13
+ * MVP 스코프: 신호 *수집*만. 승격 정책(예: 5회 reuse → mature) 은 다음 사이클.
14
+ * 기록만 잘 쌓이면 임계 설정·승격 로직은 위에 얹기 쉬움.
15
+ *
16
+ * 데이터: append-only JSONL at ~/.forgen/state/compound-usage.jsonl
17
+ * 각 라인: {"at": ISO, "name": "<solution-slug>", "via": "mcp|cli|hook"}
18
+ * crash-safe: 단순 fs.appendFileSync — 동시 append 는 OS 가 atomic 보장 (POSIX <PIPE_BUF).
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { STATE_DIR } from '../core/paths.js';
23
+ export const COMPOUND_USAGE_LOG = path.join(STATE_DIR, 'compound-usage.jsonl');
24
+ export function recordUsage(name, via = 'mcp') {
25
+ if (!name)
26
+ return;
27
+ try {
28
+ fs.mkdirSync(STATE_DIR, { recursive: true });
29
+ const entry = { at: new Date().toISOString(), name, via };
30
+ fs.appendFileSync(COMPOUND_USAGE_LOG, JSON.stringify(entry) + '\n');
31
+ }
32
+ catch {
33
+ // fail-open: 신호 수집 실패가 사용자 경험을 방해하면 안 됨
34
+ }
35
+ }
36
+ /** 단순 카운터 — 승격 정책에서 호출. MVP 에선 미사용. */
37
+ export function readUsageCounts() {
38
+ const counts = new Map();
39
+ if (!fs.existsSync(COMPOUND_USAGE_LOG))
40
+ return counts;
41
+ try {
42
+ const lines = fs.readFileSync(COMPOUND_USAGE_LOG, 'utf-8').split('\n').filter(Boolean);
43
+ for (const line of lines) {
44
+ try {
45
+ const entry = JSON.parse(line);
46
+ if (typeof entry.name === 'string') {
47
+ counts.set(entry.name, (counts.get(entry.name) ?? 0) + 1);
48
+ }
49
+ }
50
+ catch {
51
+ // skip malformed
52
+ }
53
+ }
54
+ }
55
+ catch {
56
+ // fail-open
57
+ }
58
+ return counts;
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooojin/forgen",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "preferGlobal": true,
5
5
  "main": "dist/lib.js",
6
6
  "types": "./dist/lib.d.ts",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.4.3",
4
+ "version": "0.4.4",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",
@@ -21,7 +21,8 @@ description: This skill should be used when the user asks to "calibrate, 캘리
21
21
  calibrate는 두 가지 데이터 소스를 사용합니다:
22
22
 
23
23
  ### 1차 소스: Evidence 파일
24
- `~/.forgen/me/evidence/` 디렉토리의 JSON 파일을 읽습니다.
24
+ `~/.forgen/me/behavior/` 디렉토리의 JSON 파일을 읽습니다.
25
+ (파일명: UUID.json — correction-record MCP 도구가 작성. 같은 디렉토리의 auto-*.md 는 auto-compound 산출이므로 calibrate 분석 대상 아님.)
25
26
  각 파일의 구조:
26
27
  ```json
27
28
  {
@@ -52,8 +53,8 @@ evidence 0건 + compound 교정 패턴 0건이면:
52
53
  ## Phase 1: Evidence 로드 및 검증
53
54
 
54
55
  ```bash
55
- ls ~/.forgen/me/evidence/ 2>/dev/null || echo "EMPTY"
56
- cat ~/.forgen/me/evidence/*.json 2>/dev/null || echo "NO_FILES"
56
+ ls ~/.forgen/me/behavior/*.json 2>/dev/null || echo "EMPTY"
57
+ cat ~/.forgen/me/behavior/*.json 2>/dev/null || echo "NO_FILES"
57
58
  ```
58
59
 
59
60
  로드한 JSON 파일마다 다음을 검증합니다:
@@ -53,8 +53,8 @@ compound-list
53
53
  ### 1-3: 교정 기록
54
54
 
55
55
  ```bash
56
- ls -la ~/.forgen/me/evidence/ 2>/dev/null || echo "교정 데이터 없음"
57
- find ~/.forgen/me/evidence/ -name "*.json" -mtime -{period_days} 2>/dev/null | wc -l
56
+ ls -la ~/.forgen/me/behavior/ 2>/dev/null || echo "교정 데이터 없음"
57
+ find ~/.forgen/me/behavior/ -name "*.json" -mtime -{period_days} 2>/dev/null | wc -l
58
58
  ```
59
59
 
60
60
  ## Phase 2: 코드 활동 분석