@wooojin/forgen 0.4.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +5 -5
- package/CHANGELOG.md +194 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +74 -9
- package/README.ko.md +77 -12
- package/README.md +127 -25
- package/README.zh.md +43 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
- package/dist/checks/conclusion-verification-ratio.js +86 -0
- package/dist/checks/fact-vs-agreement.d.ts +47 -0
- package/dist/checks/fact-vs-agreement.js +92 -0
- package/dist/checks/self-score-deflation.d.ts +38 -0
- package/dist/checks/self-score-deflation.js +108 -0
- package/dist/cli.js +98 -6
- package/dist/core/auto-compound-runner.js +137 -49
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +41 -2
- package/dist/core/doctor.js +118 -5
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -0
- package/dist/core/init-cli.d.ts +26 -0
- package/dist/core/init-cli.js +104 -0
- package/dist/core/init.js +17 -0
- package/dist/core/inspect-cli.js +1 -2
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +11 -0
- package/dist/core/migrate-cli.js +53 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/paths.d.ts +8 -1
- package/dist/core/paths.js +11 -2
- package/dist/core/recall-cli.d.ts +26 -0
- package/dist/core/recall-cli.js +125 -0
- package/dist/core/recall-reference-detector.d.ts +43 -0
- package/dist/core/recall-reference-detector.js +65 -0
- package/dist/core/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.d.ts +21 -0
- package/dist/core/stats-cli.js +133 -10
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/core/uninstall.js +2 -1
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +5 -6
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
- package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
- package/dist/engine/lifecycle/orchestrator.js +2 -2
- package/dist/engine/lifecycle/signals.js +6 -6
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
- package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
- package/dist/engine/skill-promoter.js +3 -6
- package/dist/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/context-guard.js +1 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +21 -5
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +2 -2
- package/dist/hooks/notepad-injector.js +1 -1
- package/dist/hooks/permission-handler.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +7 -1
- package/dist/hooks/post-tool-use.js +50 -23
- package/dist/hooks/pre-compact.js +2 -2
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +28 -10
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +1 -1
- package/dist/hooks/session-recovery.js +12 -1
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +30 -2
- package/dist/hooks/shared/hook-response.js +61 -3
- package/dist/hooks/skill-injector.js +2 -2
- package/dist/hooks/slop-detector.js +2 -2
- package/dist/hooks/solution-injector.d.ts +9 -0
- package/dist/hooks/solution-injector.js +48 -5
- package/dist/hooks/stop-guard.js +152 -13
- package/dist/hooks/subagent-tracker.js +1 -1
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/i18n/index.js +3 -5
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +45 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/rule-store.js +8 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +7 -5
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/calibrate.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/retro.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge Loop State — RC6 가드 (US-M1)
|
|
3
|
+
*
|
|
4
|
+
* 직전 forge-loop 의 findings 또는 진행 중 stories 를 ≤1KB 요약으로 렌더한다.
|
|
5
|
+
* SessionStart 와 UserPromptSubmit 두 hook 이 공유하는 단일 진입점.
|
|
6
|
+
*
|
|
7
|
+
* RC6 자기증거: 본 세션 R1 에서 head -80 으로 forge-loop.json 을 읽어 findings
|
|
8
|
+
* 8줄(line 92~99)이 잘렸음. 결과적으로 직전 결론을 컨텍스트에 못 가져 같은
|
|
9
|
+
* 가설을 재발. 이 모듈은 그 회귀를 시스템 레벨에서 차단한다.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { STATE_DIR } from '../../core/paths.js';
|
|
14
|
+
const FORGE_LOOP_PATH = path.join(STATE_DIR, 'forge-loop.json');
|
|
15
|
+
const SOFT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
const HARD_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const MAX_INJECT_BYTES = 1024;
|
|
18
|
+
const MAX_TASK_CHARS = 240;
|
|
19
|
+
const MAX_FINDING_CHARS = 240;
|
|
20
|
+
const MAX_PENDING = 5;
|
|
21
|
+
export function readForgeLoopState(filePath = FORGE_LOOP_PATH) {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(filePath))
|
|
24
|
+
return null;
|
|
25
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
26
|
+
if (typeof raw !== 'object' || raw === null)
|
|
27
|
+
return null;
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function ageMs(state, now = Date.now()) {
|
|
35
|
+
const ts = state.completedAt ?? state.startedAt;
|
|
36
|
+
if (!ts)
|
|
37
|
+
return Number.POSITIVE_INFINITY;
|
|
38
|
+
const t = new Date(ts).getTime();
|
|
39
|
+
if (Number.isNaN(t))
|
|
40
|
+
return Number.POSITIVE_INFINITY;
|
|
41
|
+
return now - t;
|
|
42
|
+
}
|
|
43
|
+
function clipBlock(block) {
|
|
44
|
+
if (block.length <= MAX_INJECT_BYTES)
|
|
45
|
+
return block;
|
|
46
|
+
return `${block.slice(0, MAX_INJECT_BYTES - 3)}...`;
|
|
47
|
+
}
|
|
48
|
+
function escXml(s) {
|
|
49
|
+
return s.replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' })[c] ?? c);
|
|
50
|
+
}
|
|
51
|
+
/** SessionStart 용 — 완료된 forge-loop 의 findings 또는 진행 중 stories 요약. */
|
|
52
|
+
export function renderForgeLoopForSession(state, now = Date.now()) {
|
|
53
|
+
if (!state)
|
|
54
|
+
return null;
|
|
55
|
+
const age = ageMs(state, now);
|
|
56
|
+
if (age > HARD_STALE_MS)
|
|
57
|
+
return null;
|
|
58
|
+
const stale = age > SOFT_STALE_MS;
|
|
59
|
+
const lines = [];
|
|
60
|
+
const task = String(state.task ?? '').trim();
|
|
61
|
+
if (task)
|
|
62
|
+
lines.push(`Task: ${escXml(task.slice(0, MAX_TASK_CHARS))}`);
|
|
63
|
+
if (state.active && Array.isArray(state.stories)) {
|
|
64
|
+
const total = state.stories.length;
|
|
65
|
+
const done = state.stories.filter(s => s?.passes).length;
|
|
66
|
+
lines.push(`Status: in-progress ${done}/${total}${stale ? ' (stale)' : ''}`);
|
|
67
|
+
const pending = state.stories
|
|
68
|
+
.filter(s => !s?.passes)
|
|
69
|
+
.slice(0, MAX_PENDING)
|
|
70
|
+
.map(s => `- ${escXml(String(s.id))}: ${escXml(String(s.title))}`);
|
|
71
|
+
if (pending.length) {
|
|
72
|
+
lines.push('Pending:');
|
|
73
|
+
lines.push(...pending);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (state.findings && typeof state.findings === 'object') {
|
|
77
|
+
lines.push(`Status: completed${stale ? ' (stale)' : ''}`);
|
|
78
|
+
for (const [id, val] of Object.entries(state.findings)) {
|
|
79
|
+
const text = String(val ?? '').slice(0, MAX_FINDING_CHARS);
|
|
80
|
+
if (text)
|
|
81
|
+
lines.push(`- ${escXml(id)}: ${escXml(text)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (lines.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
const tag = stale ? '<forge-loop-state stale="true">' : '<forge-loop-state>';
|
|
90
|
+
const body = lines.join('\n');
|
|
91
|
+
return clipBlock(`${tag}\n${body}\n</forge-loop-state>`);
|
|
92
|
+
}
|
|
93
|
+
/** UserPromptSubmit 용 — active=true 시에만 짧은 진행 상황 1~2줄. */
|
|
94
|
+
export function renderForgeLoopForPrompt(state, now = Date.now()) {
|
|
95
|
+
if (!state || !state.active || !Array.isArray(state.stories))
|
|
96
|
+
return null;
|
|
97
|
+
const age = ageMs(state, now);
|
|
98
|
+
if (age > HARD_STALE_MS)
|
|
99
|
+
return null;
|
|
100
|
+
const total = state.stories.length;
|
|
101
|
+
const done = state.stories.filter(s => s?.passes).length;
|
|
102
|
+
const next = state.stories.find(s => !s?.passes);
|
|
103
|
+
if (!next)
|
|
104
|
+
return null;
|
|
105
|
+
const stale = age > SOFT_STALE_MS;
|
|
106
|
+
const tag = stale ? '<forge-loop-active stale="true">' : '<forge-loop-active>';
|
|
107
|
+
const body = `Progress: ${done}/${total} | next: ${escXml(String(next.id))} ${escXml(String(next.title))}`;
|
|
108
|
+
return clipBlock(`${tag}\n${body}\n</forge-loop-active>`);
|
|
109
|
+
}
|
|
110
|
+
/** 테스트 노출용 상수 — 회귀 시 임계값 변경 즉시 감지. */
|
|
111
|
+
export const FORGE_LOOP_LIMITS = {
|
|
112
|
+
SOFT_STALE_MS,
|
|
113
|
+
HARD_STALE_MS,
|
|
114
|
+
MAX_INJECT_BYTES,
|
|
115
|
+
MAX_PENDING,
|
|
116
|
+
};
|
|
@@ -18,8 +18,13 @@ export declare function approve(): string;
|
|
|
18
18
|
/**
|
|
19
19
|
* 통과 + 모델에 컨텍스트 주입.
|
|
20
20
|
* UserPromptSubmit, SessionStart 이벤트에서만 모델에 도달함.
|
|
21
|
+
*
|
|
22
|
+
* H1 (v0.4.1): optional `userNotice` 로 사용자 UI (systemMessage) 에도 동시
|
|
23
|
+
* 1줄 노출. additionalContext 는 모델 전용이라 기존 recall hit 이 8,000+ 번
|
|
24
|
+
* 주입되었는데도 사용자는 0 건을 봤음. userNotice 로 같은 hit 을 사용자
|
|
25
|
+
* 에게 가시화한다.
|
|
21
26
|
*/
|
|
22
|
-
export declare function approveWithContext(context: string, eventName: string): string;
|
|
27
|
+
export declare function approveWithContext(context: string, eventName: string, userNotice?: string): string;
|
|
23
28
|
/**
|
|
24
29
|
* 통과 + UI 경고 표시 (모델에는 전달되지 않음).
|
|
25
30
|
* PostToolUse, PreToolUse 경고 등 모델 도달이 불필요한 경우 사용.
|
|
@@ -29,6 +34,24 @@ export declare function approveWithWarning(warning: string): string;
|
|
|
29
34
|
export declare function deny(reason: string): string;
|
|
30
35
|
/** 사용자 확인 요청 (PreToolUse 전용) */
|
|
31
36
|
export declare function ask(reason: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* P3' enforcement helper (2026-04-27)
|
|
39
|
+
*
|
|
40
|
+
* ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
|
|
41
|
+
*
|
|
42
|
+
* 사용:
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { canBlock } from './blocking-allowlist.js';
|
|
45
|
+
* if (someCondition) {
|
|
46
|
+
* console.log(blockOrObserve(hookName, 'reason', logCallback));
|
|
47
|
+
* return;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
|
|
52
|
+
* 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
|
|
53
|
+
*/
|
|
54
|
+
export declare function denyOrObserve(hookName: string, reason: string, observer?: (msg: string) => void): string;
|
|
32
55
|
/**
|
|
33
56
|
* Stop hook only — block the agent from stopping and feed a self-check
|
|
34
57
|
* question back to Claude so the current session resumes with new guidance.
|
|
@@ -44,6 +67,11 @@ export declare function blockStop(reason: string, systemMessage?: string): strin
|
|
|
44
67
|
* fail-open with error tracking: 에러 시 안전하게 통과하되, 실패 정보를 기록.
|
|
45
68
|
* forgen doctor의 Hook Health 섹션에서 실패 이력을 표시할 수 있도록 JSONL 로그에 기록.
|
|
46
69
|
*
|
|
70
|
+
* v0.4.1 (2026-04-24): optional `err` 매개변수 추가. 실 데이터상 106건의 hook 에러가
|
|
71
|
+
* 누적됐으나 전부 `{hook,at}` 만이라 근원 조사 불가했다. 이제 `error`/`stack` 을
|
|
72
|
+
* 함께 기록해 `forgen doctor` 가 원인 카테고리별로 빈도 surface 가능.
|
|
73
|
+
* payload 는 한 줄 cap(400자)로 잘라 JSONL 크기 폭주 방지.
|
|
74
|
+
*
|
|
47
75
|
* @fail-open: hook failure must never block the user's workflow
|
|
48
76
|
*/
|
|
49
|
-
export declare function failOpenWithTracking(hookName: string): string;
|
|
77
|
+
export declare function failOpenWithTracking(hookName: string, err?: unknown): string;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as fs from 'node:fs';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { STATE_DIR } from '../../core/paths.js';
|
|
19
|
+
import { canBlock } from './blocking-allowlist.js';
|
|
19
20
|
/** 통과 응답 (컨텍스트 없음, 모든 이벤트 공통) */
|
|
20
21
|
export function approve() {
|
|
21
22
|
return JSON.stringify({ continue: true });
|
|
@@ -23,11 +24,17 @@ export function approve() {
|
|
|
23
24
|
/**
|
|
24
25
|
* 통과 + 모델에 컨텍스트 주입.
|
|
25
26
|
* UserPromptSubmit, SessionStart 이벤트에서만 모델에 도달함.
|
|
27
|
+
*
|
|
28
|
+
* H1 (v0.4.1): optional `userNotice` 로 사용자 UI (systemMessage) 에도 동시
|
|
29
|
+
* 1줄 노출. additionalContext 는 모델 전용이라 기존 recall hit 이 8,000+ 번
|
|
30
|
+
* 주입되었는데도 사용자는 0 건을 봤음. userNotice 로 같은 hit 을 사용자
|
|
31
|
+
* 에게 가시화한다.
|
|
26
32
|
*/
|
|
27
|
-
export function approveWithContext(context, eventName) {
|
|
33
|
+
export function approveWithContext(context, eventName, userNotice) {
|
|
28
34
|
return JSON.stringify({
|
|
29
35
|
continue: true,
|
|
30
36
|
hookSpecificOutput: { hookEventName: eventName, additionalContext: context },
|
|
37
|
+
...(userNotice ? { systemMessage: userNotice } : {}),
|
|
31
38
|
});
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
@@ -59,6 +66,36 @@ export function ask(reason) {
|
|
|
59
66
|
},
|
|
60
67
|
});
|
|
61
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* P3' enforcement helper (2026-04-27)
|
|
71
|
+
*
|
|
72
|
+
* ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
|
|
73
|
+
*
|
|
74
|
+
* 사용:
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { canBlock } from './blocking-allowlist.js';
|
|
77
|
+
* if (someCondition) {
|
|
78
|
+
* console.log(blockOrObserve(hookName, 'reason', logCallback));
|
|
79
|
+
* return;
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
|
|
84
|
+
* 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
|
|
85
|
+
*/
|
|
86
|
+
export function denyOrObserve(hookName, reason, observer) {
|
|
87
|
+
if (canBlock(hookName)) {
|
|
88
|
+
return deny(reason);
|
|
89
|
+
}
|
|
90
|
+
// ALLOW-LIST 외 — log only, approve
|
|
91
|
+
if (observer) {
|
|
92
|
+
try {
|
|
93
|
+
observer(`[${hookName}] would-deny (observe-only): ${reason}`);
|
|
94
|
+
}
|
|
95
|
+
catch { /* fail-open */ }
|
|
96
|
+
}
|
|
97
|
+
return approve();
|
|
98
|
+
}
|
|
62
99
|
/**
|
|
63
100
|
* Stop hook only — block the agent from stopping and feed a self-check
|
|
64
101
|
* question back to Claude so the current session resumes with new guidance.
|
|
@@ -81,13 +118,34 @@ export function blockStop(reason, systemMessage) {
|
|
|
81
118
|
* fail-open with error tracking: 에러 시 안전하게 통과하되, 실패 정보를 기록.
|
|
82
119
|
* forgen doctor의 Hook Health 섹션에서 실패 이력을 표시할 수 있도록 JSONL 로그에 기록.
|
|
83
120
|
*
|
|
121
|
+
* v0.4.1 (2026-04-24): optional `err` 매개변수 추가. 실 데이터상 106건의 hook 에러가
|
|
122
|
+
* 누적됐으나 전부 `{hook,at}` 만이라 근원 조사 불가했다. 이제 `error`/`stack` 을
|
|
123
|
+
* 함께 기록해 `forgen doctor` 가 원인 카테고리별로 빈도 surface 가능.
|
|
124
|
+
* payload 는 한 줄 cap(400자)로 잘라 JSONL 크기 폭주 방지.
|
|
125
|
+
*
|
|
84
126
|
* @fail-open: hook failure must never block the user's workflow
|
|
85
127
|
*/
|
|
86
|
-
export function failOpenWithTracking(hookName) {
|
|
128
|
+
export function failOpenWithTracking(hookName, err) {
|
|
87
129
|
try {
|
|
88
130
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
89
131
|
const logPath = path.join(STATE_DIR, 'hook-errors.jsonl');
|
|
90
|
-
const
|
|
132
|
+
const payload = { hook: hookName, at: Date.now() };
|
|
133
|
+
if (err !== undefined && err !== null) {
|
|
134
|
+
if (err instanceof Error) {
|
|
135
|
+
payload.error = err.message.slice(0, 400);
|
|
136
|
+
if (err.stack) {
|
|
137
|
+
// 스택 첫 3줄만 — 어느 파일/라인에서 throw 됐는지만 알면 충분.
|
|
138
|
+
payload.stack = err.stack.split('\n').slice(0, 3).join(' | ').slice(0, 400);
|
|
139
|
+
}
|
|
140
|
+
const maybeCode = err.code;
|
|
141
|
+
if (typeof maybeCode === 'string')
|
|
142
|
+
payload.code = maybeCode;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
payload.error = String(err).slice(0, 400);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const entry = JSON.stringify(payload);
|
|
91
149
|
fs.appendFileSync(logPath, entry + '\n');
|
|
92
150
|
}
|
|
93
151
|
catch { /* fail-open: tracking itself must not throw */ }
|
|
@@ -181,7 +181,7 @@ function collectSkills() {
|
|
|
181
181
|
const skills = [];
|
|
182
182
|
const seen = new Map(); // name → source dir
|
|
183
183
|
// 패키지 내장 스킬 경로 (dist/../skills/)
|
|
184
|
-
const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
|
|
184
|
+
const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'claude', 'commands');
|
|
185
185
|
// 프로젝트 .forgen > 프로젝트 .compound > 개인 > 글로벌 > 패키지 내장
|
|
186
186
|
const dirs = [
|
|
187
187
|
path.join(process.cwd(), '.forgen', 'skills'),
|
|
@@ -312,5 +312,5 @@ async function main() {
|
|
|
312
312
|
}
|
|
313
313
|
main().catch((e) => {
|
|
314
314
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
315
|
-
console.log(failOpenWithTracking('skill-injector'));
|
|
315
|
+
console.log(failOpenWithTracking('skill-injector', e));
|
|
316
316
|
});
|
|
@@ -84,10 +84,10 @@ async function main() {
|
|
|
84
84
|
}
|
|
85
85
|
catch (e) {
|
|
86
86
|
log.debug('슬롭 감지 실패', e);
|
|
87
|
-
console.log(failOpenWithTracking('slop-detector'));
|
|
87
|
+
console.log(failOpenWithTracking('slop-detector', e));
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
main().catch((e) => {
|
|
91
91
|
process.stderr.write(`[ch-hook] ${e instanceof Error ? e.message : String(e)}\n`);
|
|
92
|
-
console.log(failOpenWithTracking('slop-detector'));
|
|
92
|
+
console.log(failOpenWithTracking('slop-detector', e));
|
|
93
93
|
});
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const MIN_INJECT_RELEVANCE = 0.3;
|
|
30
30
|
export declare const MIN_INJECT_RELEVANCE_TRUSTED = 0.25;
|
|
31
|
+
/**
|
|
32
|
+
* v0.4.1 — cold-start 사용자 threshold. outcomes 이벤트가 거의 없는 신규 사용자
|
|
33
|
+
* 는 starter-pack 이 champion/active 로 승격될 기회가 없어 0.3 gate 에 막혀 주입 0.
|
|
34
|
+
* 실측 (buyer-day1 v2): starter 15개 중 recall 5건 전부 relevance 0.08~0.25, 주입 0.
|
|
35
|
+
* 이 값으로 첫날부터 매칭 가능성 제공 + 누적 후엔 표준 threshold 로 자연 전환.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MIN_INJECT_RELEVANCE_COLD_START = 0.2;
|
|
38
|
+
/** cold-start 판정 임계 — fitness state 있는 솔루션이 이 수 미만이면 신규 사용자 간주. */
|
|
39
|
+
export declare const COLD_START_FITNESS_THRESHOLD = 5;
|
|
31
40
|
interface SessionCacheCommitResult {
|
|
32
41
|
/**
|
|
33
42
|
* commit 상태:
|
|
@@ -29,6 +29,7 @@ import { approve, approveWithContext, failOpenWithTracking } from './shared/hook
|
|
|
29
29
|
import { STATE_DIR } from '../core/paths.js';
|
|
30
30
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
31
31
|
import { appendPending, flushAccept } from '../engine/solution-outcomes.js';
|
|
32
|
+
import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
|
|
32
33
|
const MAX_SOLUTIONS_PER_SESSION = 10;
|
|
33
34
|
/**
|
|
34
35
|
* Minimum relevance thresholds by fitness state (2026-04-21 gate sweep).
|
|
@@ -51,6 +52,15 @@ const MAX_SOLUTIONS_PER_SESSION = 10;
|
|
|
51
52
|
*/
|
|
52
53
|
export const MIN_INJECT_RELEVANCE = 0.3;
|
|
53
54
|
export const MIN_INJECT_RELEVANCE_TRUSTED = 0.25;
|
|
55
|
+
/**
|
|
56
|
+
* v0.4.1 — cold-start 사용자 threshold. outcomes 이벤트가 거의 없는 신규 사용자
|
|
57
|
+
* 는 starter-pack 이 champion/active 로 승격될 기회가 없어 0.3 gate 에 막혀 주입 0.
|
|
58
|
+
* 실측 (buyer-day1 v2): starter 15개 중 recall 5건 전부 relevance 0.08~0.25, 주입 0.
|
|
59
|
+
* 이 값으로 첫날부터 매칭 가능성 제공 + 누적 후엔 표준 threshold 로 자연 전환.
|
|
60
|
+
*/
|
|
61
|
+
export const MIN_INJECT_RELEVANCE_COLD_START = 0.2;
|
|
62
|
+
/** cold-start 판정 임계 — fitness state 있는 솔루션이 이 수 미만이면 신규 사용자 간주. */
|
|
63
|
+
export const COLD_START_FITNESS_THRESHOLD = 5;
|
|
54
64
|
/** 세션별 이미 주입된 솔루션 추적 (중복 방지) */
|
|
55
65
|
function getSessionCachePath(sessionId) {
|
|
56
66
|
return path.join(STATE_DIR, `solution-cache-${sanitizeId(sessionId)}.json`);
|
|
@@ -347,11 +357,17 @@ async function main() {
|
|
|
347
357
|
catch (e) {
|
|
348
358
|
log.debug('fitness state load 실패 — default 0.3 적용', e);
|
|
349
359
|
}
|
|
360
|
+
// v0.4.1 cold-start boost: outcome 이벤트가 누적되지 않은 신규 사용자 (fitness
|
|
361
|
+
// state 있는 솔루션 < THRESHOLD) 는 champion/active 로 승격될 기회가 없으므로
|
|
362
|
+
// 보정된 낮은 threshold 적용. 누적되면 자동으로 표준 경로로 전환.
|
|
363
|
+
const isColdStart = fitnessStateMap.size < COLD_START_FITNESS_THRESHOLD;
|
|
350
364
|
function minRelevanceFor(name) {
|
|
351
365
|
const state = fitnessStateMap.get(name);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
366
|
+
if (state === 'champion' || state === 'active')
|
|
367
|
+
return MIN_INJECT_RELEVANCE_TRUSTED;
|
|
368
|
+
if (isColdStart)
|
|
369
|
+
return MIN_INJECT_RELEVANCE_COLD_START;
|
|
370
|
+
return MIN_INJECT_RELEVANCE;
|
|
355
371
|
}
|
|
356
372
|
let experimentCount = 0;
|
|
357
373
|
const toInject = [];
|
|
@@ -530,7 +546,34 @@ async function main() {
|
|
|
530
546
|
catch (e) {
|
|
531
547
|
log.debug('outcome appendPending 실패', e);
|
|
532
548
|
}
|
|
533
|
-
|
|
549
|
+
// H4: 양수 implicit-feedback — 솔루션이 실제로 사용자에게 surface 되었음을 기록.
|
|
550
|
+
// v0.4.0 의 enforcement 축은 block/violation 만 카운트했고 assist (solution 노출)
|
|
551
|
+
// 은 0건이었다. 이 emit 으로 forgen stats / session-quality-scorer 가 "오늘
|
|
552
|
+
// N개 surfaced" 를 계산할 수 있다.
|
|
553
|
+
try {
|
|
554
|
+
const now = new Date().toISOString();
|
|
555
|
+
for (const sol of effectiveToInject) {
|
|
556
|
+
appendImplicitFeedback({
|
|
557
|
+
type: 'recommendation_surfaced',
|
|
558
|
+
category: 'positive',
|
|
559
|
+
solution: sol.name,
|
|
560
|
+
match_score: sol.relevance,
|
|
561
|
+
at: now,
|
|
562
|
+
sessionId,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (e) {
|
|
567
|
+
log.debug('recommendation_surfaced emit 실패', e);
|
|
568
|
+
}
|
|
569
|
+
// H1: 사용자 UI 에 recall hit 1줄 노출. additionalContext 는 모델 전용이라
|
|
570
|
+
// v0.4.0 에서 8,000+ 주입이 발생했는데도 사용자는 0건을 봤다. systemMessage
|
|
571
|
+
// 로 "N개 솔루션 참조" 를 surface → 사용자가 어떤 축적 지식이 붙었는지 인식.
|
|
572
|
+
const topNames = effectiveToInject.slice(0, 3).map((s) => s.name);
|
|
573
|
+
const more = effectiveToInject.length - topNames.length;
|
|
574
|
+
const noticeNames = more > 0 ? `${topNames.join(', ')} (+${more})` : topNames.join(', ');
|
|
575
|
+
const userNotice = `[Forgen] 🔎 ${effectiveToInject.length} solution${effectiveToInject.length === 1 ? '' : 's'} recalled: ${noticeNames}`;
|
|
576
|
+
console.log(approveWithContext(fullInjection, 'UserPromptSubmit', userNotice));
|
|
534
577
|
}
|
|
535
578
|
finally {
|
|
536
579
|
recordHookTiming('solution-injector', Date.now() - _hookStart, 'UserPromptSubmit');
|
|
@@ -538,5 +581,5 @@ async function main() {
|
|
|
538
581
|
}
|
|
539
582
|
main().catch((e) => {
|
|
540
583
|
process.stderr.write(`[ch-hook] solution-injector: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
541
|
-
console.log(failOpenWithTracking('solution-injector'));
|
|
584
|
+
console.log(failOpenWithTracking('solution-injector', e));
|
|
542
585
|
});
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -22,7 +22,16 @@ import * as fs from 'node:fs';
|
|
|
22
22
|
import * as path from 'node:path';
|
|
23
23
|
import * as os from 'node:os';
|
|
24
24
|
import { readStdinJSON } from './shared/read-stdin.js';
|
|
25
|
-
import { approve, blockStop, failOpenWithTracking } from './shared/hook-response.js';
|
|
25
|
+
import { approve, approveWithWarning, blockStop, failOpenWithTracking } from './shared/hook-response.js';
|
|
26
|
+
import { takeLastExtractionNotice } from '../core/extraction-notice.js';
|
|
27
|
+
import { checkConclusionVerificationRatio } from '../checks/conclusion-verification-ratio.js';
|
|
28
|
+
import { checkSelfScoreInflation } from '../checks/self-score-deflation.js';
|
|
29
|
+
import { checkFactVsAgreement } from '../checks/fact-vs-agreement.js';
|
|
30
|
+
import { STATE_DIR } from '../core/paths.js';
|
|
31
|
+
import { sanitizeId } from './shared/sanitize-id.js';
|
|
32
|
+
import { detectRecallReferences } from '../core/recall-reference-detector.js';
|
|
33
|
+
import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
|
|
34
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
26
35
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
27
36
|
import { isHookEnabled } from './hook-config.js';
|
|
28
37
|
import { loadActiveRules } from '../store/rule-store.js';
|
|
@@ -38,9 +47,9 @@ import { DEFAULT_STOP_TRIGGER_RE, DEFAULT_STOP_EXCLUDE_RE } from './shared/stop-
|
|
|
38
47
|
* ADR-002 Meta 트리거(규칙 자동 강등)로 연결한다.
|
|
39
48
|
*/
|
|
40
49
|
const STUCK_LOOP_THRESHOLD = 3;
|
|
41
|
-
const BLOCK_COUNT_DIR = path.join(
|
|
42
|
-
const DRIFT_LOG = path.join(
|
|
43
|
-
const ACK_LOG = path.join(
|
|
50
|
+
const BLOCK_COUNT_DIR = path.join(STATE_DIR, 'enforcement', 'block-count');
|
|
51
|
+
const DRIFT_LOG = path.join(STATE_DIR, 'enforcement', 'drift.jsonl');
|
|
52
|
+
const ACK_LOG = path.join(STATE_DIR, 'enforcement', 'acknowledgments.jsonl');
|
|
44
53
|
/**
|
|
45
54
|
* Spike scenarios.json 로더 — FORGEN_SPIKE_RULES 명시 시에만 로드.
|
|
46
55
|
* H1 (2026-04-22): 이전에는 process.cwd()/tests/spike/... 를 기본 폴백했으나,
|
|
@@ -213,7 +222,7 @@ function evaluateVerifier(rule) {
|
|
|
213
222
|
* 루트 밖 경로는 존재 여부와 무관하게 false 반환.
|
|
214
223
|
*/
|
|
215
224
|
function artifactFresh(relOrAbs, maxAgeS) {
|
|
216
|
-
const homeBase =
|
|
225
|
+
const homeBase = STATE_DIR;
|
|
217
226
|
const projectBase = path.resolve(process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd(), '.forgen', 'state');
|
|
218
227
|
const allowedRoots = [homeBase, projectBase];
|
|
219
228
|
let p = relOrAbs;
|
|
@@ -397,22 +406,152 @@ export function getStuckLoopThreshold() {
|
|
|
397
406
|
return env;
|
|
398
407
|
return STUCK_LOOP_THRESHOLD;
|
|
399
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* H4 완결 (2026-04-24): recall_referenced emit 경로.
|
|
411
|
+
* Stop hook 에서 Claude 의 직전 응답 텍스트와 이 세션의 injection-cache 를 대조해,
|
|
412
|
+
* 주입된 솔루션 이름이 응답에 등장하면 `recall_referenced` 이벤트 기록. 동일
|
|
413
|
+
* 솔루션 중복 emit 방지용으로 injection-cache 의 해당 엔트리에 `_referenced: true`
|
|
414
|
+
* 플래그 세팅 후 atomic 재기록. fail-open — 어떤 단계든 throw 시 스킵.
|
|
415
|
+
*/
|
|
416
|
+
function emitRecallReferencesFailOpen(sessionId, lastMessage) {
|
|
417
|
+
try {
|
|
418
|
+
const cachePath = path.join(STATE_DIR, `injection-cache-${sanitizeId(sessionId)}.json`);
|
|
419
|
+
if (!fs.existsSync(cachePath))
|
|
420
|
+
return;
|
|
421
|
+
const raw = fs.readFileSync(cachePath, 'utf-8');
|
|
422
|
+
const cache = JSON.parse(raw);
|
|
423
|
+
const sols = Array.isArray(cache.solutions) ? cache.solutions : [];
|
|
424
|
+
if (sols.length === 0)
|
|
425
|
+
return;
|
|
426
|
+
const { newlyReferenced } = detectRecallReferences(lastMessage, sols);
|
|
427
|
+
if (newlyReferenced.length === 0)
|
|
428
|
+
return;
|
|
429
|
+
const now = new Date().toISOString();
|
|
430
|
+
for (const name of newlyReferenced) {
|
|
431
|
+
appendImplicitFeedback({
|
|
432
|
+
type: 'recall_referenced',
|
|
433
|
+
category: 'positive',
|
|
434
|
+
solution: name,
|
|
435
|
+
at: now,
|
|
436
|
+
sessionId,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// 중복 emit 방지 — cache 에 플래그 저장 후 재기록
|
|
440
|
+
const refSet = new Set(newlyReferenced);
|
|
441
|
+
const updated = {
|
|
442
|
+
...cache,
|
|
443
|
+
solutions: sols.map((s) => (refSet.has(s.name) ? { ...s, _referenced: true } : s)),
|
|
444
|
+
updatedAt: now,
|
|
445
|
+
};
|
|
446
|
+
atomicWriteJSON(cachePath, updated, { mode: 0o600, dirMode: 0o700 });
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
/* fail-open */
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* TEST-2 support: post-tool-use 가 저장한 modified-files-{sessionId}.json 에서
|
|
454
|
+
* recentToolNames 윈도우를 로드. 파일이 없거나 깨져도 빈 배열로 fail-open.
|
|
455
|
+
*/
|
|
456
|
+
function loadRecentToolNames(sessionId) {
|
|
457
|
+
try {
|
|
458
|
+
const p = path.join(STATE_DIR, `modified-files-${sanitizeId(sessionId)}.json`);
|
|
459
|
+
if (!fs.existsSync(p))
|
|
460
|
+
return [];
|
|
461
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
462
|
+
const data = JSON.parse(raw);
|
|
463
|
+
if (Array.isArray(data.recentToolNames)) {
|
|
464
|
+
return data.recentToolNames.filter((n) => typeof n === 'string');
|
|
465
|
+
}
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* H2: Stop hook approve 시 이전 세션의 auto-compound 추출 결과를 1회만 surface.
|
|
474
|
+
* takeLastExtractionNotice 가 null 이면 일반 approve, 아니면 systemMessage 포함.
|
|
475
|
+
*/
|
|
476
|
+
function approveWithOptionalExtractionNotice() {
|
|
477
|
+
const notice = takeLastExtractionNotice();
|
|
478
|
+
if (notice)
|
|
479
|
+
return approveWithWarning(notice);
|
|
480
|
+
return approve();
|
|
481
|
+
}
|
|
400
482
|
export async function main() {
|
|
401
483
|
const started = Date.now();
|
|
402
484
|
try {
|
|
403
485
|
if (!isHookEnabled(HOOK_NAME)) {
|
|
404
|
-
console.log(
|
|
486
|
+
console.log(approveWithOptionalExtractionNotice());
|
|
405
487
|
return;
|
|
406
488
|
}
|
|
407
489
|
const input = await readStdinJSON();
|
|
408
490
|
const lastMessage = readLastAssistantMessage(input);
|
|
409
491
|
if (!lastMessage) {
|
|
410
|
-
console.log(
|
|
492
|
+
console.log(approveWithOptionalExtractionNotice());
|
|
411
493
|
return;
|
|
412
494
|
}
|
|
495
|
+
// H4 완결: 응답 텍스트와 injection-cache 대조해 recall_referenced emit.
|
|
496
|
+
// block/approve 어느 경로이든 동일하게 기록 (참조는 응답 내용이 결정).
|
|
497
|
+
const sessionIdForRef = input?.session_id ?? 'unknown';
|
|
498
|
+
emitRecallReferencesFailOpen(sessionIdForRef, lastMessage);
|
|
499
|
+
// TEST-2/3: rule-free meta guards — FORGEN_USER_CONFIRMED=1 우회 공통.
|
|
500
|
+
if (process.env.FORGEN_USER_CONFIRMED !== '1') {
|
|
501
|
+
const sessionId = input?.session_id ?? 'unknown';
|
|
502
|
+
// TEST-2 (자가 점수 인플레이션): 숫자 점수 상승 선언 + 측정 도구 0회 → block.
|
|
503
|
+
// TEST-3 보다 강한 신호라 먼저 평가.
|
|
504
|
+
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',
|
|
511
|
+
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) {
|
|
524
|
+
recordViolation({
|
|
525
|
+
rule_id: 'builtin:conclusion-verification-ratio',
|
|
526
|
+
session_id: sessionId,
|
|
527
|
+
source: 'stop-guard',
|
|
528
|
+
kind: 'block',
|
|
529
|
+
message_preview: lastMessage.slice(0, 120),
|
|
530
|
+
});
|
|
531
|
+
const reasonText = `[forgen:stop-guard/conclusion-ratio] ${ratio.reason}
|
|
532
|
+
|
|
533
|
+
(Override this turn: set FORGEN_USER_CONFIRMED=1 (audited).)`;
|
|
534
|
+
console.log(blockStop(reasonText, 'rule:TEST-3 — conclusion/verification ratio'));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
// TEST-1: 사실 vs 합의 — fact assertion 키워드가 있으나 측정 도구 호출 0건.
|
|
538
|
+
// 원 design intent (per fact-vs-agreement.ts): alert level only — block 은 TEST-2/3.
|
|
539
|
+
// 여기서는 measurement 신호를 violations.jsonl 에 'alert' kind 로 기록만 (block 안 함).
|
|
540
|
+
// wiring gap 발견 (forgen-eval introspect) → 측정 가능하게 wired up.
|
|
541
|
+
const fva = checkFactVsAgreement({ text: lastMessage, recentTools, minMeasurements: 1 });
|
|
542
|
+
if (fva.alert) {
|
|
543
|
+
recordViolation({
|
|
544
|
+
rule_id: 'builtin:fact-vs-agreement',
|
|
545
|
+
session_id: sessionId,
|
|
546
|
+
source: 'stop-guard',
|
|
547
|
+
kind: 'correction', // alert-level signal (not block) per fact-vs-agreement.ts design
|
|
548
|
+
message_preview: lastMessage.slice(0, 120),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
413
552
|
const rules = loadStopRules();
|
|
414
553
|
if (rules.length === 0) {
|
|
415
|
-
console.log(
|
|
554
|
+
console.log(approveWithOptionalExtractionNotice());
|
|
416
555
|
return;
|
|
417
556
|
}
|
|
418
557
|
const result = evaluateStop(lastMessage, rules);
|
|
@@ -421,7 +560,7 @@ export async function main() {
|
|
|
421
560
|
// R9-PA2: 같은 session 에 pending block 이 있었다면 retract→pass 루프가
|
|
422
561
|
// 실제 작동한 것 — acknowledgment 이벤트로 기록. block-count 는 cleanup.
|
|
423
562
|
acknowledgeSessionBlocks(sessionId);
|
|
424
|
-
console.log(
|
|
563
|
+
console.log(approveWithOptionalExtractionNotice());
|
|
425
564
|
return;
|
|
426
565
|
}
|
|
427
566
|
const { hit, reason } = result;
|
|
@@ -433,7 +572,7 @@ export async function main() {
|
|
|
433
572
|
kind: 'correction',
|
|
434
573
|
message_preview: `[FORGEN_USER_CONFIRMED=1 bypass] ${lastMessage.slice(0, 100)}`,
|
|
435
574
|
});
|
|
436
|
-
console.log(
|
|
575
|
+
console.log(approveWithOptionalExtractionNotice());
|
|
437
576
|
return;
|
|
438
577
|
}
|
|
439
578
|
// T2 signal: block 은 rule 위반 증거 — violations.jsonl 에 기록.
|
|
@@ -464,13 +603,13 @@ export async function main() {
|
|
|
464
603
|
message_preview: lastMessage.slice(0, 120),
|
|
465
604
|
});
|
|
466
605
|
resetBlockCount(sessionId, hit.id);
|
|
467
|
-
console.log(
|
|
606
|
+
console.log(approveWithOptionalExtractionNotice());
|
|
468
607
|
return;
|
|
469
608
|
}
|
|
470
609
|
console.log(blockStop(reasonWithHint, hit.system_tag));
|
|
471
610
|
}
|
|
472
|
-
catch {
|
|
473
|
-
console.log(failOpenWithTracking(HOOK_NAME));
|
|
611
|
+
catch (e) {
|
|
612
|
+
console.log(failOpenWithTracking(HOOK_NAME, e));
|
|
474
613
|
}
|
|
475
614
|
finally {
|
|
476
615
|
recordHookTiming(HOOK_NAME, Date.now() - started, 'Stop');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude HostCapabilities — Multi-Host Core Design §9.0
|
|
3
|
+
*
|
|
4
|
+
* Claude 는 reference host. 모든 TrustLayerIntent 가 supported (identity binding).
|
|
5
|
+
* 본 선언은 *spec 정의 그 자체* 의 코드 표현 — 변경 시 spec §9.0 도 같이 갱신해야 한다.
|
|
6
|
+
*/
|
|
7
|
+
import type { HostCapabilities } from '../core/trust-layer-intent.js';
|
|
8
|
+
export declare const claudeCapabilities: HostCapabilities;
|