@wooojin/forgen 0.3.2 → 0.4.0
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 +1 -1
- package/CHANGELOG.md +64 -0
- package/README.ja.md +61 -7
- package/README.ko.md +15 -1
- package/README.md +92 -6
- package/README.zh.md +61 -7
- package/dist/cli.js +137 -5
- package/dist/core/auto-compound-runner.js +10 -2
- package/dist/core/doctor.js +64 -10
- package/dist/core/inspect-cli.js +65 -5
- package/dist/core/state-gc.d.ts +19 -0
- package/dist/core/state-gc.js +48 -4
- package/dist/core/stats-cli.d.ts +15 -0
- package/dist/core/stats-cli.js +143 -0
- package/dist/core/uninstall.d.ts +1 -0
- package/dist/core/uninstall.js +24 -1
- package/dist/core/v1-bootstrap.js +9 -1
- package/dist/engine/classify-enforce-cli.d.ts +8 -0
- package/dist/engine/classify-enforce-cli.js +61 -0
- package/dist/engine/enforce-classifier.d.ts +31 -0
- package/dist/engine/enforce-classifier.js +123 -0
- package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
- package/dist/engine/lifecycle/bypass-detector.js +82 -0
- package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
- package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
- package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
- package/dist/engine/lifecycle/meta-cli.js +7 -0
- package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
- package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
- package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
- package/dist/engine/lifecycle/orchestrator.js +131 -0
- package/dist/engine/lifecycle/signals.d.ts +30 -0
- package/dist/engine/lifecycle/signals.js +142 -0
- package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
- package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
- package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
- package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
- package/dist/engine/lifecycle/types.d.ts +52 -0
- package/dist/engine/lifecycle/types.js +7 -0
- package/dist/engine/rule-toggle-cli.d.ts +13 -0
- package/dist/engine/rule-toggle-cli.js +76 -0
- package/dist/forge/evidence-processor.js +10 -2
- package/dist/hooks/context-guard.js +71 -0
- package/dist/hooks/post-tool-use.js +62 -0
- package/dist/hooks/pre-tool-use.js +57 -1
- package/dist/hooks/secret-filter.d.ts +10 -0
- package/dist/hooks/secret-filter.js +20 -0
- package/dist/hooks/shared/atomic-write.d.ts +8 -1
- package/dist/hooks/shared/atomic-write.js +17 -3
- package/dist/hooks/shared/hook-response.d.ts +11 -0
- package/dist/hooks/shared/hook-response.js +18 -0
- package/dist/hooks/shared/safe-regex.d.ts +25 -0
- package/dist/hooks/shared/safe-regex.js +50 -0
- package/dist/hooks/shared/stop-triggers.d.ts +19 -0
- package/dist/hooks/shared/stop-triggers.js +19 -0
- package/dist/hooks/stop-guard.d.ts +84 -0
- package/dist/hooks/stop-guard.js +482 -0
- package/dist/mcp/tools.js +19 -2
- package/dist/store/evidence-store.d.ts +15 -0
- package/dist/store/evidence-store.js +50 -1
- package/dist/store/rule-lifecycle.d.ts +23 -0
- package/dist/store/rule-lifecycle.js +63 -0
- package/dist/store/rule-store.d.ts +21 -0
- package/dist/store/rule-store.js +128 -8
- package/dist/store/types.d.ts +83 -0
- package/dist/store/types.js +7 -1
- package/hooks/hook-registry.json +1 -0
- package/hooks/hooks.json +6 -1
- package/package.json +10 -2
- package/plugin.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule lifecycle factory + helpers — single source of truth for defaults/normalization.
|
|
3
|
+
*
|
|
4
|
+
* R6-F1 (2026-04-22): 이전에는 `rule.lifecycle ?? { phase: 'active', inject_count: 0, ... }`
|
|
5
|
+
* 리터럴이 rule-store, orchestrator, meta-reclassifier 등 5곳에 복제되어 필드 추가 시 동시
|
|
6
|
+
* 수정 필수였다. 한 곳에서 불변식을 걸고 모든 호출자가 이 함수를 통해 lifecycle 을 얻도록 통합.
|
|
7
|
+
*
|
|
8
|
+
* root-cause-analyst (R6) 분석: "Rule 이 data file + state machine 이중 정체성을 가지면서
|
|
9
|
+
* 기본값 재합성이 N 군데에 분산" 이 R4-B2(음수 corruption)/R5-B1(orphan)/R5-B2(mutex)/api-H1
|
|
10
|
+
* 등 버그 클러스터의 공통 뿌리. 이 factory 가 그 뿌리를 차단.
|
|
11
|
+
*/
|
|
12
|
+
/** safe non-negative integer normalization — 파일 corruption / 다중 writer race 방어. */
|
|
13
|
+
export function safeCount(n) {
|
|
14
|
+
return typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rule 에 대해 정규화된 LifecycleState 반환 (pure — rule 을 변경하지 않음).
|
|
18
|
+
* 기존 lifecycle 이 있으면 카운터를 safeCount 로 정규화한 사본, 없으면 초기 상태.
|
|
19
|
+
*/
|
|
20
|
+
export function initLifecycle(rule) {
|
|
21
|
+
const existing = rule.lifecycle;
|
|
22
|
+
if (existing) {
|
|
23
|
+
return {
|
|
24
|
+
phase: existing.phase,
|
|
25
|
+
first_active_at: existing.first_active_at,
|
|
26
|
+
last_inject_at: existing.last_inject_at,
|
|
27
|
+
last_violation_at: existing.last_violation_at,
|
|
28
|
+
inject_count: safeCount(existing.inject_count),
|
|
29
|
+
accept_count: safeCount(existing.accept_count),
|
|
30
|
+
violation_count: safeCount(existing.violation_count),
|
|
31
|
+
bypass_count: safeCount(existing.bypass_count),
|
|
32
|
+
conflict_refs: Array.isArray(existing.conflict_refs) ? [...existing.conflict_refs] : [],
|
|
33
|
+
merged_into: existing.merged_into,
|
|
34
|
+
superseded_by: existing.superseded_by,
|
|
35
|
+
meta_promotions: Array.isArray(existing.meta_promotions) ? [...existing.meta_promotions] : [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
phase: 'active',
|
|
40
|
+
first_active_at: rule.created_at,
|
|
41
|
+
inject_count: 0,
|
|
42
|
+
accept_count: 0,
|
|
43
|
+
violation_count: 0,
|
|
44
|
+
bypass_count: 0,
|
|
45
|
+
conflict_refs: [],
|
|
46
|
+
meta_promotions: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** inject count + last_inject_at 을 한 단계 증가 — markRulesInjected 의 공통 로직. */
|
|
50
|
+
export function bumpInject(lifecycle, nowIso) {
|
|
51
|
+
return {
|
|
52
|
+
...lifecycle,
|
|
53
|
+
inject_count: lifecycle.inject_count + 1,
|
|
54
|
+
last_inject_at: nowIso,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** meta_promotions 에 새 entry append (immutable). */
|
|
58
|
+
export function appendMetaPromotion(lifecycle, promotion) {
|
|
59
|
+
return {
|
|
60
|
+
...lifecycle,
|
|
61
|
+
meta_promotions: [...lifecycle.meta_promotions, promotion],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -16,9 +16,30 @@ export declare function createRule(params: {
|
|
|
16
16
|
render_key: string;
|
|
17
17
|
}): Rule;
|
|
18
18
|
export declare function saveRule(rule: Rule): void;
|
|
19
|
+
/**
|
|
20
|
+
* ADR-002 T5 — rule 저장 + 기존 active rules 와 자연어 충돌 감지 + 양쪽 conflict_refs 기록.
|
|
21
|
+
*
|
|
22
|
+
* saveRule 과의 차이:
|
|
23
|
+
* - 저장 직후 T5 detect 실행 → 충돌 발견 시 신규 rule + 반대편 rule 모두 conflict_refs 업데이트.
|
|
24
|
+
* - auto-merge 안 함 (ADR-002 §Risks — 사용자 수동 해소).
|
|
25
|
+
* - T5 감지 실패는 저장 자체를 막지 않음 (fail-open).
|
|
26
|
+
*
|
|
27
|
+
* 반환: 저장된 rule + 감지된 충돌 rule_id 목록.
|
|
28
|
+
*/
|
|
29
|
+
export declare function appendRule(rule: Rule): Promise<{
|
|
30
|
+
saved: true;
|
|
31
|
+
conflicts_with: string[];
|
|
32
|
+
}>;
|
|
19
33
|
export declare function loadRule(ruleId: string): Rule | null;
|
|
20
34
|
export declare function loadAllRules(): Rule[];
|
|
21
35
|
export declare function loadActiveRules(): Rule[];
|
|
36
|
+
/**
|
|
37
|
+
* ADR-002 Meta signal — rule 들이 프롬프트에 inject 되었음을 기록.
|
|
38
|
+
* rule.lifecycle.inject_count +1, last_inject_at = now.
|
|
39
|
+
* lifecycle 없던 rule 은 auto-init 하고 phase='active'.
|
|
40
|
+
* Meta promotion (B→A) 의 rolling window 집계가 이 카운터를 소비한다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function markRulesInjected(ruleIds: string[], nowIso?: string): void;
|
|
22
43
|
export declare function updateRuleStatus(ruleId: string, status: RuleStatus): boolean;
|
|
23
44
|
/**
|
|
24
45
|
* 현재 세션 ID와 다른 scope:'session' 규칙을 비활성화.
|
package/dist/store/rule-store.js
CHANGED
|
@@ -9,6 +9,8 @@ import * as path from 'node:path';
|
|
|
9
9
|
import * as crypto from 'node:crypto';
|
|
10
10
|
import { ME_RULES } from '../core/paths.js';
|
|
11
11
|
import { atomicWriteJSON, safeReadJSON } from '../hooks/shared/atomic-write.js';
|
|
12
|
+
import { CURRENT_RULE_SCHEMA_VERSION } from './types.js';
|
|
13
|
+
import { initLifecycle, bumpInject } from './rule-lifecycle.js';
|
|
12
14
|
function rulePath(ruleId) {
|
|
13
15
|
return path.join(ME_RULES, `${ruleId}.json`);
|
|
14
16
|
}
|
|
@@ -33,25 +35,143 @@ export function saveRule(rule) {
|
|
|
33
35
|
rule.updated_at = new Date().toISOString();
|
|
34
36
|
atomicWriteJSON(rulePath(rule.rule_id), rule, { pretty: true });
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* ADR-002 T5 — rule 저장 + 기존 active rules 와 자연어 충돌 감지 + 양쪽 conflict_refs 기록.
|
|
40
|
+
*
|
|
41
|
+
* saveRule 과의 차이:
|
|
42
|
+
* - 저장 직후 T5 detect 실행 → 충돌 발견 시 신규 rule + 반대편 rule 모두 conflict_refs 업데이트.
|
|
43
|
+
* - auto-merge 안 함 (ADR-002 §Risks — 사용자 수동 해소).
|
|
44
|
+
* - T5 감지 실패는 저장 자체를 막지 않음 (fail-open).
|
|
45
|
+
*
|
|
46
|
+
* 반환: 저장된 rule + 감지된 충돌 rule_id 목록.
|
|
47
|
+
*/
|
|
48
|
+
export async function appendRule(rule) {
|
|
49
|
+
saveRule(rule);
|
|
50
|
+
try {
|
|
51
|
+
const [{ detect: detectT5 }, { appendLifecycleEvents }] = await Promise.all([
|
|
52
|
+
import('../engine/lifecycle/trigger-t5-conflict.js'),
|
|
53
|
+
import('../engine/lifecycle/meta-reclassifier.js'),
|
|
54
|
+
]);
|
|
55
|
+
const all = loadAllRules();
|
|
56
|
+
const events = detectT5({ rules: all });
|
|
57
|
+
const relevant = events.filter((e) => e.evidence?.refs?.includes(rule.rule_id));
|
|
58
|
+
if (relevant.length === 0)
|
|
59
|
+
return { saved: true, conflicts_with: [] };
|
|
60
|
+
// conflict_refs 양방향 업데이트
|
|
61
|
+
const affected = new Set();
|
|
62
|
+
for (const ev of relevant)
|
|
63
|
+
affected.add(ev.rule_id);
|
|
64
|
+
for (const id of affected) {
|
|
65
|
+
const target = all.find((r) => r.rule_id === id);
|
|
66
|
+
if (!target)
|
|
67
|
+
continue;
|
|
68
|
+
const refs = relevant
|
|
69
|
+
.filter((ev) => ev.rule_id === id)
|
|
70
|
+
.flatMap((ev) => (ev.evidence?.refs ?? []).filter((r) => r !== id));
|
|
71
|
+
const currentConflicts = target.lifecycle?.conflict_refs ?? [];
|
|
72
|
+
const merged = [...new Set([...currentConflicts, ...refs])];
|
|
73
|
+
const lifecycle = target.lifecycle ?? {
|
|
74
|
+
phase: 'active',
|
|
75
|
+
first_active_at: target.created_at,
|
|
76
|
+
inject_count: 0, accept_count: 0, violation_count: 0, bypass_count: 0,
|
|
77
|
+
conflict_refs: [], meta_promotions: [],
|
|
78
|
+
};
|
|
79
|
+
saveRule({ ...target, lifecycle: { ...lifecycle, conflict_refs: merged } });
|
|
80
|
+
}
|
|
81
|
+
appendLifecycleEvents(relevant);
|
|
82
|
+
const conflicts_with = [
|
|
83
|
+
...new Set(relevant
|
|
84
|
+
.filter((e) => e.rule_id === rule.rule_id)
|
|
85
|
+
.flatMap((e) => (e.evidence?.refs ?? []).filter((r) => r !== rule.rule_id))),
|
|
86
|
+
];
|
|
87
|
+
return { saved: true, conflicts_with };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { saved: true, conflicts_with: [] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
36
93
|
export function loadRule(ruleId) {
|
|
37
94
|
return safeReadJSON(rulePath(ruleId), null);
|
|
38
95
|
}
|
|
39
96
|
export function loadAllRules() {
|
|
40
|
-
if (!fs.existsSync(ME_RULES))
|
|
41
|
-
return [];
|
|
42
97
|
const rules = [];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
98
|
+
// 1) 사용자 개인 rules: ~/.forgen/me/rules
|
|
99
|
+
if (fs.existsSync(ME_RULES)) {
|
|
100
|
+
for (const file of fs.readdirSync(ME_RULES)) {
|
|
101
|
+
if (!file.endsWith('.json'))
|
|
102
|
+
continue;
|
|
103
|
+
const rule = safeReadJSON(path.join(ME_RULES, file), null);
|
|
104
|
+
if (rule && isCompatibleSchema(rule, file))
|
|
105
|
+
rules.push(rule);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 2) 프로젝트 로컬 rules: <cwd>/.forgen/rules
|
|
109
|
+
// ADR-003 Phase 1 Dogfood — 팀/프로젝트가 git 에 committed 한 L1 정책을 자동 로드.
|
|
110
|
+
// 같은 rule_id 가 me 와 project 양쪽에 있으면 project 가 우선 (git 소스가 정책 진실).
|
|
111
|
+
const projectRulesDir = resolveProjectRulesDir();
|
|
112
|
+
if (projectRulesDir && fs.existsSync(projectRulesDir)) {
|
|
113
|
+
for (const file of fs.readdirSync(projectRulesDir)) {
|
|
114
|
+
if (!file.endsWith('.json'))
|
|
115
|
+
continue;
|
|
116
|
+
const rule = safeReadJSON(path.join(projectRulesDir, file), null);
|
|
117
|
+
if (!rule || !isCompatibleSchema(rule, file))
|
|
118
|
+
continue;
|
|
119
|
+
const existingIdx = rules.findIndex((r) => r.rule_id === rule.rule_id);
|
|
120
|
+
if (existingIdx >= 0)
|
|
121
|
+
rules[existingIdx] = rule; // project override
|
|
122
|
+
else
|
|
123
|
+
rules.push(rule);
|
|
124
|
+
}
|
|
49
125
|
}
|
|
50
126
|
return rules;
|
|
51
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* R5-B3: schema_version 호환성 체크.
|
|
130
|
+
* - undefined / 0 / CURRENT_RULE_SCHEMA_VERSION: OK
|
|
131
|
+
* - > CURRENT: 상위 버전 → graceful skip (downgrade 시 silent corruption 방지)
|
|
132
|
+
*/
|
|
133
|
+
function isCompatibleSchema(rule, filename) {
|
|
134
|
+
const v = rule.schema_version;
|
|
135
|
+
if (v == null || v <= CURRENT_RULE_SCHEMA_VERSION)
|
|
136
|
+
return true;
|
|
137
|
+
if (process.env.FORGEN_DEBUG_SIGNALS === '1') {
|
|
138
|
+
process.stderr.write(`[forgen:rule-store] ${filename} schema_version=${v} > supported ${CURRENT_RULE_SCHEMA_VERSION} — skipped\n`);
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 현재 프로젝트 cwd 의 `.forgen/rules/` 경로. FORGEN_CWD/COMPOUND_CWD 우선.
|
|
144
|
+
* 테스트 / CI 에서 프로젝트 스코프 로딩을 비활성화하려면 FORGEN_DISABLE_PROJECT_RULES=1.
|
|
145
|
+
*/
|
|
146
|
+
function resolveProjectRulesDir() {
|
|
147
|
+
if (process.env.FORGEN_DISABLE_PROJECT_RULES === '1')
|
|
148
|
+
return null;
|
|
149
|
+
const cwd = process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
150
|
+
return path.join(cwd, '.forgen', 'rules');
|
|
151
|
+
}
|
|
52
152
|
export function loadActiveRules() {
|
|
53
153
|
return loadAllRules().filter(r => r.status === 'active');
|
|
54
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* ADR-002 Meta signal — rule 들이 프롬프트에 inject 되었음을 기록.
|
|
157
|
+
* rule.lifecycle.inject_count +1, last_inject_at = now.
|
|
158
|
+
* lifecycle 없던 rule 은 auto-init 하고 phase='active'.
|
|
159
|
+
* Meta promotion (B→A) 의 rolling window 집계가 이 카운터를 소비한다.
|
|
160
|
+
*/
|
|
161
|
+
export function markRulesInjected(ruleIds, nowIso = new Date().toISOString()) {
|
|
162
|
+
for (const id of ruleIds) {
|
|
163
|
+
const rule = loadRule(id);
|
|
164
|
+
if (!rule)
|
|
165
|
+
continue;
|
|
166
|
+
// R6-F1: initLifecycle 이 정규화(카운터 ≥0, 배열 보장) 까지 포함.
|
|
167
|
+
const lifecycle = initLifecycle(rule);
|
|
168
|
+
const updated = {
|
|
169
|
+
...rule,
|
|
170
|
+
lifecycle: bumpInject(lifecycle, nowIso),
|
|
171
|
+
};
|
|
172
|
+
atomicWriteJSON(rulePath(rule.rule_id), updated, { pretty: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
55
175
|
export function updateRuleStatus(ruleId, status) {
|
|
56
176
|
const rule = loadRule(ruleId);
|
|
57
177
|
if (!rule)
|
package/dist/store/types.d.ts
CHANGED
|
@@ -14,7 +14,79 @@ export type RuleScope = 'me' | 'session';
|
|
|
14
14
|
export type RuleStrength = 'soft' | 'default' | 'strong' | 'hard';
|
|
15
15
|
export type RuleSource = 'onboarding' | 'explicit_correction' | 'behavior_inference' | 'pack_overlay';
|
|
16
16
|
export type RuleStatus = 'active' | 'suppressed' | 'removed' | 'superseded';
|
|
17
|
+
export type EnforcementMech = 'A' | 'B' | 'C';
|
|
18
|
+
export type HookPoint = 'PreToolUse' | 'PostToolUse' | 'Stop' | 'UserPromptSubmit';
|
|
19
|
+
export type VerifierKind = 'file_exists' | 'pattern_match' | 'tool_arg_regex' | 'artifact_check' | 'self_check_prompt';
|
|
20
|
+
export interface VerifierSpec {
|
|
21
|
+
kind: VerifierKind;
|
|
22
|
+
/** 각 kind 별로 의미가 다름 — 예: self_check_prompt 는 `question`, artifact_check 는 `path`+`max_age_s`. */
|
|
23
|
+
params: Record<string, string | number | boolean>;
|
|
24
|
+
}
|
|
25
|
+
export interface EnforceSpec {
|
|
26
|
+
mech: EnforcementMech;
|
|
27
|
+
hook: HookPoint;
|
|
28
|
+
/** Mech-A/B 에서 필수, Mech-C 에서는 미사용. */
|
|
29
|
+
verifier?: VerifierSpec;
|
|
30
|
+
/** Mech-A BLOCK / Mech-B self-check 시 Claude 에게 전달할 reason. */
|
|
31
|
+
block_message?: string;
|
|
32
|
+
/** Mech-C drift-score.ts 키 — 정량 판정 불가 규칙의 장기 누적 편향 축. */
|
|
33
|
+
drift_key?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Stop hook 전용: 어시스턴트 응답 텍스트에서 이 규칙을 발화시킬 정규식.
|
|
36
|
+
* 미지정 시 shared default (완료 선언 키워드 regex) 사용.
|
|
37
|
+
*/
|
|
38
|
+
trigger_keywords_regex?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Stop hook 전용: trigger 가 매칭되더라도 이 regex 가 매칭되면 발화 안 함.
|
|
41
|
+
* retraction/meta/테스트-맥락 등 false-positive 컨텍스트 차단용.
|
|
42
|
+
*/
|
|
43
|
+
trigger_exclude_regex?: string;
|
|
44
|
+
/**
|
|
45
|
+
* UI 표시용 한 줄 태그 (Stop hook 의 `systemMessage` 로 전달).
|
|
46
|
+
* 예: "rule:R-B1 — e2e-before-done"
|
|
47
|
+
*/
|
|
48
|
+
system_tag?: string;
|
|
49
|
+
}
|
|
50
|
+
export type LifecyclePhase = 'active' | 'flagged' | 'suppressed' | 'retired' | 'merged' | 'superseded';
|
|
51
|
+
export interface MetaPromotion {
|
|
52
|
+
at: string;
|
|
53
|
+
from_mech: EnforcementMech;
|
|
54
|
+
to_mech: EnforcementMech;
|
|
55
|
+
reason: 'consistent_adherence' | 'repeated_violation' | 'user_override' | 'stuck_loop_force_approve';
|
|
56
|
+
trigger_stats: {
|
|
57
|
+
window_n: number;
|
|
58
|
+
adherence_rate?: number;
|
|
59
|
+
violation_count?: number;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export interface LifecycleState {
|
|
63
|
+
phase: LifecyclePhase;
|
|
64
|
+
first_active_at: string;
|
|
65
|
+
last_inject_at?: string;
|
|
66
|
+
last_violation_at?: string;
|
|
67
|
+
inject_count: number;
|
|
68
|
+
accept_count: number;
|
|
69
|
+
violation_count: number;
|
|
70
|
+
/** T3: 사용자가 rule 과 반대로 행동한 횟수 */
|
|
71
|
+
bypass_count: number;
|
|
72
|
+
/** T5: 충돌하는 rule_id 목록 */
|
|
73
|
+
conflict_refs: string[];
|
|
74
|
+
/** T5: 이 rule 이 흡수된 대상 rule_id */
|
|
75
|
+
merged_into?: string;
|
|
76
|
+
/** T1: 이 rule 을 교체한 rule_id */
|
|
77
|
+
superseded_by?: string;
|
|
78
|
+
/** Meta: mech 변경 이력 */
|
|
79
|
+
meta_promotions: MetaPromotion[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Rule JSON schema version. v0.4.0 introduces `enforce_via` + `lifecycle` — 이들을
|
|
83
|
+
* 포함하는 schema 의 공식 버전은 1. 누락된 rule 파일은 pre-v0.4.0 으로 취급 (optional fields
|
|
84
|
+
* 만 비어있을 뿐 로드 가능). 미래 breaking change 시 이 값을 증가시키고 `migrate()` 체인으로 흡수.
|
|
85
|
+
*/
|
|
86
|
+
export declare const CURRENT_RULE_SCHEMA_VERSION = 1;
|
|
17
87
|
export interface Rule {
|
|
88
|
+
/** R5-B3: 미래 breaking schema change 를 위한 version 필드. 없으면 v0 (pre-0.4.0) 으로 취급. */
|
|
89
|
+
schema_version?: number;
|
|
18
90
|
rule_id: string;
|
|
19
91
|
category: RuleCategory;
|
|
20
92
|
scope: RuleScope;
|
|
@@ -27,6 +99,17 @@ export interface Rule {
|
|
|
27
99
|
render_key: string;
|
|
28
100
|
created_at: string;
|
|
29
101
|
updated_at: string;
|
|
102
|
+
/**
|
|
103
|
+
* 이 rule 이 어떤 hook/verifier 로 강제되는가. optional — 기존 rule 은 null.
|
|
104
|
+
* `forgen classify-enforce` 명령이 기존 rule 을 자동 분류하여 채운다.
|
|
105
|
+
* ADR-001 §Data Model.
|
|
106
|
+
*/
|
|
107
|
+
enforce_via?: EnforceSpec[];
|
|
108
|
+
/**
|
|
109
|
+
* Lifecycle 상태. optional — 기존 rule 은 load 시 phase='active' 로 auto-initialize.
|
|
110
|
+
* ADR-002 §Data Model.
|
|
111
|
+
*/
|
|
112
|
+
lifecycle?: LifecycleState;
|
|
30
113
|
}
|
|
31
114
|
export type EvidenceType = 'explicit_correction' | 'behavior_observation' | 'session_summary';
|
|
32
115
|
export interface Evidence {
|
package/dist/store/types.js
CHANGED
|
@@ -4,4 +4,10 @@
|
|
|
4
4
|
* Authoritative source: docs/plans/2026-04-03-forgen-data-model-storage-spec.md
|
|
5
5
|
* Runtime contracts: docs/plans/2026-04-03-forgen-component-interface-design.md
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
// ── Rule ────────────────────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Rule JSON schema version. v0.4.0 introduces `enforce_via` + `lifecycle` — 이들을
|
|
10
|
+
* 포함하는 schema 의 공식 버전은 1. 누락된 rule 파일은 pre-v0.4.0 으로 취급 (optional fields
|
|
11
|
+
* 만 비어있을 뿐 로드 가능). 미래 breaking change 시 이 값을 증가시키고 `migrate()` 체인으로 흡수.
|
|
12
|
+
*/
|
|
13
|
+
export const CURRENT_RULE_SCHEMA_VERSION = 1;
|
package/hooks/hook-registry.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
{ "name": "post-tool-use", "tier": "compound-core", "event": "PostToolUse", "matcher": "*", "script": "hooks/post-tool-use.js", "timeout": 3, "compoundCritical": true },
|
|
6
6
|
{ "name": "pre-compact", "tier": "compound-core", "event": "PreCompact", "matcher": "*", "script": "hooks/pre-compact.js", "timeout": 3, "compoundCritical": false },
|
|
7
7
|
{ "name": "context-guard-stop", "tier": "compound-core", "event": "Stop", "matcher": "*", "script": "hooks/context-guard.js", "timeout": 5, "compoundCritical": false },
|
|
8
|
+
{ "name": "stop-guard", "tier": "compound-core", "event": "Stop", "matcher": "*", "script": "hooks/stop-guard.js", "timeout": 10, "compoundCritical": true },
|
|
8
9
|
{ "name": "pre-tool-use", "tier": "compound-core", "event": "PreToolUse", "matcher": "*", "script": "hooks/pre-tool-use.js", "timeout": 3, "compoundCritical": true },
|
|
9
10
|
{ "name": "secret-filter", "tier": "safety", "event": "PostToolUse", "matcher": "Write|Edit|Bash", "script": "hooks/secret-filter.js", "timeout": 3, "compoundCritical": false },
|
|
10
11
|
{ "name": "slop-detector", "tier": "safety", "event": "PostToolUse", "matcher": "Write|Edit", "script": "hooks/slop-detector.js", "timeout": 3, "compoundCritical": false },
|
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Forgen harness hooks (auto-generated,
|
|
2
|
+
"description": "Forgen harness hooks (auto-generated, 20/20 active)",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"UserPromptSubmit": [
|
|
5
5
|
{
|
|
@@ -102,6 +102,11 @@
|
|
|
102
102
|
"type": "command",
|
|
103
103
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hooks/context-guard.js\"",
|
|
104
104
|
"timeout": 5
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"type": "command",
|
|
108
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hooks/stop-guard.js\"",
|
|
109
|
+
"timeout": 10
|
|
105
110
|
}
|
|
106
111
|
]
|
|
107
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooojin/forgen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"preferGlobal": true,
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"types": "./dist/lib.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"author": "jang-ujin",
|
|
36
36
|
"license": "MIT",
|
|
37
|
-
"description": "
|
|
37
|
+
"description": "When Claude says 'done', forgen makes it prove it — turn-level self-verification + personalized rules, at $0 extra API cost.",
|
|
38
38
|
"keywords": [
|
|
39
39
|
"claude-code",
|
|
40
40
|
"forgen",
|
|
@@ -86,5 +86,13 @@
|
|
|
86
86
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
87
87
|
"js-yaml": "^4.1.1",
|
|
88
88
|
"zod": "^4.3.6"
|
|
89
|
+
},
|
|
90
|
+
"peerDependencies": {
|
|
91
|
+
"@anthropic-ai/claude-code": ">=2.0.0"
|
|
92
|
+
},
|
|
93
|
+
"peerDependenciesMeta": {
|
|
94
|
+
"@anthropic-ai/claude-code": {
|
|
95
|
+
"optional": true
|
|
96
|
+
}
|
|
89
97
|
}
|
|
90
98
|
}
|
package/plugin.json
CHANGED