@wooojin/forgen 0.3.1 → 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 +7 -2
- package/CHANGELOG.md +164 -0
- package/README.ja.md +90 -7
- package/README.ko.md +44 -1
- package/README.md +128 -9
- package/README.zh.md +90 -7
- package/dist/cli.js +140 -8
- package/dist/core/auto-compound-runner.js +16 -5
- package/dist/core/dashboard.js +11 -4
- package/dist/core/doctor.d.ts +6 -1
- package/dist/core/doctor.js +85 -11
- package/dist/core/global-config.d.ts +2 -2
- package/dist/core/global-config.js +6 -14
- package/dist/core/harness.d.ts +3 -5
- package/dist/core/harness.js +34 -338
- package/dist/core/inspect-cli.js +65 -5
- package/dist/core/installer.d.ts +10 -0
- package/dist/core/installer.js +185 -0
- package/dist/core/paths.d.ts +0 -34
- package/dist/core/paths.js +0 -35
- package/dist/core/settings-injector.d.ts +13 -0
- package/dist/core/settings-injector.js +167 -0
- package/dist/core/settings-lock.d.ts +35 -2
- package/dist/core/settings-lock.js +65 -7
- package/dist/core/spawn.js +100 -39
- package/dist/core/state-gc.d.ts +49 -0
- package/dist/core/state-gc.js +163 -0
- 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 +36 -5
- package/dist/core/v1-bootstrap.js +11 -3
- package/dist/engine/classify-enforce-cli.d.ts +8 -0
- package/dist/engine/classify-enforce-cli.js +61 -0
- package/dist/engine/compound-cli.d.ts +27 -2
- package/dist/engine/compound-cli.js +69 -16
- package/dist/engine/compound-export.d.ts +15 -0
- package/dist/engine/compound-export.js +32 -5
- package/dist/engine/compound-loop.js +3 -2
- package/dist/engine/enforce-classifier.d.ts +31 -0
- package/dist/engine/enforce-classifier.js +123 -0
- package/dist/engine/learn-cli.js +52 -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/match-eval-log.js +45 -0
- package/dist/engine/rule-toggle-cli.d.ts +13 -0
- package/dist/engine/rule-toggle-cli.js +76 -0
- package/dist/engine/solution-format.d.ts +0 -2
- package/dist/engine/solution-format.js +0 -4
- package/dist/engine/solution-matcher.d.ts +8 -0
- package/dist/engine/solution-matcher.js +7 -4
- package/dist/engine/solution-outcomes.d.ts +4 -0
- package/dist/engine/solution-outcomes.js +174 -97
- package/dist/engine/solution-writer.d.ts +8 -5
- package/dist/engine/solution-writer.js +43 -19
- package/dist/fgx.js +9 -2
- package/dist/forge/cli.js +7 -7
- package/dist/forge/evidence-processor.js +10 -2
- package/dist/hooks/context-guard.js +86 -1
- package/dist/hooks/hook-config.d.ts +9 -1
- package/dist/hooks/hook-config.js +25 -3
- package/dist/hooks/internal/run-lifecycle-check.d.ts +2 -0
- package/dist/hooks/internal/run-lifecycle-check.js +32 -0
- package/dist/hooks/notepad-injector.js +6 -3
- package/dist/hooks/permission-handler.d.ts +10 -2
- package/dist/hooks/permission-handler.js +31 -12
- package/dist/hooks/post-tool-use.js +62 -0
- package/dist/hooks/pre-tool-use.js +67 -5
- package/dist/hooks/secret-filter.d.ts +10 -0
- package/dist/hooks/secret-filter.js +26 -0
- package/dist/hooks/session-recovery.js +15 -7
- 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 -2
- package/dist/hooks/shared/hook-response.js +20 -7
- package/dist/hooks/shared/hook-timing.js +10 -1
- 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/solution-injector.d.ts +21 -0
- package/dist/hooks/solution-injector.js +60 -1
- package/dist/hooks/stop-guard.d.ts +84 -0
- package/dist/hooks/stop-guard.js +482 -0
- package/dist/mcp/solution-reader.d.ts +2 -0
- package/dist/mcp/solution-reader.js +28 -1
- package/dist/mcp/tools.js +24 -4
- package/dist/preset/preset-manager.js +12 -2
- package/dist/store/evidence-store.d.ts +15 -0
- package/dist/store/evidence-store.js +55 -6
- package/dist/store/profile-store.d.ts +9 -0
- package/dist/store/profile-store.js +25 -4
- 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 +133 -13
- 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 +7 -2
- package/scripts/postinstall.js +52 -5
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T5 — 규칙 충돌 (conflict_detected).
|
|
3
|
+
*
|
|
4
|
+
* 트리거 조건:
|
|
5
|
+
* 같은 category 인 rule pair 중, policy 자연어가 상반 (negation + 공통 키워드 ≥ 2).
|
|
6
|
+
*
|
|
7
|
+
* auto-merge 안 함. conflict_refs 플래그만 설정 → 사용자 수동 해소.
|
|
8
|
+
*/
|
|
9
|
+
const NEGATION_RE = /\b(없|금지|마라|말라|하지\s*않|don'?t|never|not\s+|no\s+|avoid)\b/i;
|
|
10
|
+
function tokens(policy) {
|
|
11
|
+
return new Set(policy
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[.,;:!?()[\]{}"'`~]/g, ' ')
|
|
14
|
+
.split(/\s+/)
|
|
15
|
+
.filter((w) => w.length >= 3));
|
|
16
|
+
}
|
|
17
|
+
function sharedTokens(a, b, min) {
|
|
18
|
+
let count = 0;
|
|
19
|
+
for (const t of a)
|
|
20
|
+
if (b.has(t)) {
|
|
21
|
+
count += 1;
|
|
22
|
+
if (count >= min)
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export function detect(input) {
|
|
28
|
+
const minShared = input.min_shared_tokens ?? 2;
|
|
29
|
+
const ts = input.ts ?? Date.now();
|
|
30
|
+
const events = [];
|
|
31
|
+
const reported = new Set(); // 'a|b' 쌍 중복 방지
|
|
32
|
+
// M/T5 fix: 짧은 policy 는 토큰 overlap 이 우연히 발생하기 쉬우므로 20자 이상만.
|
|
33
|
+
// scope 도 같아야 — session-scoped 임시 규칙과 me-scope 영구 규칙이 서로 충돌로 잡히면 노이즈.
|
|
34
|
+
const active = input.rules.filter((r) => r.status === 'active' && r.policy.length >= 20);
|
|
35
|
+
for (let i = 0; i < active.length; i++) {
|
|
36
|
+
const a = active[i];
|
|
37
|
+
const aTokens = tokens(a.policy);
|
|
38
|
+
const aNeg = NEGATION_RE.test(a.policy);
|
|
39
|
+
for (let j = i + 1; j < active.length; j++) {
|
|
40
|
+
const b = active[j];
|
|
41
|
+
if (a.category !== b.category)
|
|
42
|
+
continue;
|
|
43
|
+
if (a.scope !== b.scope)
|
|
44
|
+
continue; // M/T5: scope 불일치 시 pair 아님
|
|
45
|
+
const bTokens = tokens(b.policy);
|
|
46
|
+
const bNeg = NEGATION_RE.test(b.policy);
|
|
47
|
+
if (aNeg === bNeg)
|
|
48
|
+
continue; // 같은 어조 — 충돌 아님
|
|
49
|
+
if (!sharedTokens(aTokens, bTokens, minShared))
|
|
50
|
+
continue;
|
|
51
|
+
const key = [a.rule_id, b.rule_id].sort().join('|');
|
|
52
|
+
if (reported.has(key))
|
|
53
|
+
continue;
|
|
54
|
+
reported.add(key);
|
|
55
|
+
events.push({
|
|
56
|
+
kind: 't5_conflict_detected',
|
|
57
|
+
rule_id: a.rule_id,
|
|
58
|
+
evidence: {
|
|
59
|
+
source: 'rule-pairing',
|
|
60
|
+
refs: [a.rule_id, b.rule_id],
|
|
61
|
+
},
|
|
62
|
+
suggested_action: 'flag',
|
|
63
|
+
ts,
|
|
64
|
+
});
|
|
65
|
+
events.push({
|
|
66
|
+
kind: 't5_conflict_detected',
|
|
67
|
+
rule_id: b.rule_id,
|
|
68
|
+
evidence: {
|
|
69
|
+
source: 'rule-pairing',
|
|
70
|
+
refs: [a.rule_id, b.rule_id],
|
|
71
|
+
},
|
|
72
|
+
suggested_action: 'flag',
|
|
73
|
+
ts,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return events;
|
|
78
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-002 Lifecycle event model.
|
|
3
|
+
*
|
|
4
|
+
* 오케스트레이터가 발행하는 이벤트 — rule 상태 전이의 단위.
|
|
5
|
+
* 이 파일은 타입만 정의. 실제 이벤트 발행/소비 로직은 각 trigger-*.ts 참조.
|
|
6
|
+
*/
|
|
7
|
+
export type LifecycleEventKind = 't1_explicit_correction' | 't2_repeated_violation' | 't3_user_bypass' | 't4_time_decay' | 't5_conflict_detected' | 'meta_promote_to_a' | 'meta_demote_to_b';
|
|
8
|
+
export type LifecycleSuggestedAction = 'flag' | 'suppress' | 'retire' | 'merge' | 'supersede' | 'promote_mech' | 'demote_mech';
|
|
9
|
+
export interface LifecycleEvent {
|
|
10
|
+
kind: LifecycleEventKind;
|
|
11
|
+
rule_id: string;
|
|
12
|
+
session_id?: string;
|
|
13
|
+
evidence?: {
|
|
14
|
+
source: string;
|
|
15
|
+
refs: string[];
|
|
16
|
+
metrics?: Record<string, number>;
|
|
17
|
+
};
|
|
18
|
+
suggested_action: LifecycleSuggestedAction;
|
|
19
|
+
/** T5 merge 전용: 흡수 대상 rule_id */
|
|
20
|
+
merged_into?: string;
|
|
21
|
+
/** T1 supersede 전용: 교체 rule_id */
|
|
22
|
+
superseded_by?: string;
|
|
23
|
+
ts: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 트리거들이 공유하는 rule-level 시그널 집계.
|
|
27
|
+
* RuleState 는 Rule + signals (pure data). 각 detect() 는 이 상태 배열을 입력으로 받는다.
|
|
28
|
+
*/
|
|
29
|
+
export interface RuleSignals {
|
|
30
|
+
violations_30d: number;
|
|
31
|
+
violation_rate_30d: number;
|
|
32
|
+
bypass_7d: number;
|
|
33
|
+
last_inject_days_ago: number;
|
|
34
|
+
injects_rolling_n: number;
|
|
35
|
+
violations_rolling_n: number;
|
|
36
|
+
last_updated_days_ago: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ViolationEntry {
|
|
39
|
+
at: string;
|
|
40
|
+
rule_id: string;
|
|
41
|
+
session_id: string;
|
|
42
|
+
source: 'stop-guard' | 'pre-tool-guard' | 'post-tool-guard' | 'evidence-store' | 'manual';
|
|
43
|
+
kind: 'block' | 'deny' | 'correction';
|
|
44
|
+
message_preview?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface BypassEntry {
|
|
47
|
+
at: string;
|
|
48
|
+
rule_id: string;
|
|
49
|
+
session_id: string;
|
|
50
|
+
tool: string;
|
|
51
|
+
pattern_preview: string;
|
|
52
|
+
}
|
|
@@ -69,6 +69,47 @@ const MAX_NORMALIZED_QUERY_LOGGED = 64;
|
|
|
69
69
|
const MAX_MATCHED_TERMS_PER_CANDIDATE = 16;
|
|
70
70
|
/** Read-side DoS guard: refuse to load if the JSONL file is larger than this. */
|
|
71
71
|
const MAX_LOG_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
72
|
+
/**
|
|
73
|
+
* Write-side rotation threshold (2026-04-21). When the active log reaches
|
|
74
|
+
* this size, it is renamed to `<path>.1` (clobbering any previous rotation)
|
|
75
|
+
* and a fresh empty file is opened. Keeps one generation of history for
|
|
76
|
+
* offline forensics without letting the log grow unbounded. Chosen so
|
|
77
|
+
* typical installs retain ~10-20k records — enough to spot recurrent
|
|
78
|
+
* matcher surprises, not enough to silently fill disk.
|
|
79
|
+
*/
|
|
80
|
+
const ROTATION_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
81
|
+
/**
|
|
82
|
+
* Internal: rotate the log if it exceeds ROTATION_SIZE_BYTES. Called from
|
|
83
|
+
* inside the file lock so no concurrent writer observes a torn state. A
|
|
84
|
+
* rotation failure is swallowed — the caller will still attempt to append
|
|
85
|
+
* and either succeed (no-op rotation) or the append itself will surface
|
|
86
|
+
* the underlying fs error via the outer catch.
|
|
87
|
+
*/
|
|
88
|
+
function maybeRotate(logPath) {
|
|
89
|
+
let size = 0;
|
|
90
|
+
try {
|
|
91
|
+
const st = fs.statSync(logPath);
|
|
92
|
+
if (!st.isFile())
|
|
93
|
+
return;
|
|
94
|
+
size = st.size;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return; // missing file is fine, append will create it
|
|
98
|
+
}
|
|
99
|
+
if (size < ROTATION_SIZE_BYTES)
|
|
100
|
+
return;
|
|
101
|
+
const rotated = `${logPath}.1`;
|
|
102
|
+
try {
|
|
103
|
+
// rename is atomic on POSIX within the same directory; overwrites any
|
|
104
|
+
// previous rotation. We intentionally keep only one generation.
|
|
105
|
+
fs.renameSync(logPath, rotated);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Best effort. If rotation fails (permissions, cross-device link)
|
|
109
|
+
// we leave the original file alone and the next append just continues
|
|
110
|
+
// growing. The 50 MB read-side cap still protects offline tools.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
72
113
|
/**
|
|
73
114
|
* Check whether logging is disabled via environment variable.
|
|
74
115
|
* Accepts `off`, `disabled`, `0`, `false`, `no` (case-insensitive).
|
|
@@ -150,6 +191,10 @@ export function logMatchDecision(input) {
|
|
|
150
191
|
// so concurrent writers could interleave without this lock. The lock
|
|
151
192
|
// is taken on the log file itself, and cleaned up by withFileLockSync.
|
|
152
193
|
withFileLockSync(MATCH_EVAL_LOG_PATH, () => {
|
|
194
|
+
// Rotate BEFORE opening the fd so the new fd points at the fresh
|
|
195
|
+
// file. Doing this after open would append to the file that is
|
|
196
|
+
// about to be renamed into the previous-generation slot.
|
|
197
|
+
maybeRotate(MATCH_EVAL_LOG_PATH);
|
|
153
198
|
// O_NOFOLLOW: refuse to follow a symlink at the target path. This
|
|
154
199
|
// blocks a local-attacker symlink swap attack where the log file
|
|
155
200
|
// is replaced with a link to e.g. ~/.ssh/authorized_keys.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handler for `forgen suppress-rule <id>` / `forgen activate-rule <id>`.
|
|
3
|
+
*
|
|
4
|
+
* R7-U2: Day-1 탈출구 — 사용자가 차단 메시지를 보고 JSON 을 손으로 편집하지 않아도
|
|
5
|
+
* 한 명령으로 규칙을 끄거나 되살릴 수 있도록.
|
|
6
|
+
*
|
|
7
|
+
* 구현:
|
|
8
|
+
* - prefix-match 지원 (첫 8자만 쳐도 OK).
|
|
9
|
+
* - multiple match 이면 목록 출력하고 중단.
|
|
10
|
+
* - hard strength rule 은 cli 로도 suppress 불가 (ADR-002 불변 원칙).
|
|
11
|
+
*/
|
|
12
|
+
export declare function handleSuppressRule(args: string[]): Promise<void>;
|
|
13
|
+
export declare function handleActivateRule(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handler for `forgen suppress-rule <id>` / `forgen activate-rule <id>`.
|
|
3
|
+
*
|
|
4
|
+
* R7-U2: Day-1 탈출구 — 사용자가 차단 메시지를 보고 JSON 을 손으로 편집하지 않아도
|
|
5
|
+
* 한 명령으로 규칙을 끄거나 되살릴 수 있도록.
|
|
6
|
+
*
|
|
7
|
+
* 구현:
|
|
8
|
+
* - prefix-match 지원 (첫 8자만 쳐도 OK).
|
|
9
|
+
* - multiple match 이면 목록 출력하고 중단.
|
|
10
|
+
* - hard strength rule 은 cli 로도 suppress 불가 (ADR-002 불변 원칙).
|
|
11
|
+
*/
|
|
12
|
+
import { loadAllRules, saveRule } from '../store/rule-store.js';
|
|
13
|
+
export async function handleSuppressRule(args) {
|
|
14
|
+
const partial = args[0];
|
|
15
|
+
if (!partial) {
|
|
16
|
+
console.error('Usage: forgen suppress-rule <rule_id | prefix>');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
const all = loadAllRules();
|
|
20
|
+
const matches = all.filter((r) => r.rule_id === partial || r.rule_id.startsWith(partial));
|
|
21
|
+
if (matches.length === 0) {
|
|
22
|
+
console.error(`No rule found matching "${partial}". Try \`forgen inspect rules\`.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (matches.length > 1) {
|
|
26
|
+
console.error(`Ambiguous prefix "${partial}" — ${matches.length} rules match:`);
|
|
27
|
+
for (const r of matches) {
|
|
28
|
+
console.error(` ${r.rule_id} [${r.strength}] ${r.policy.slice(0, 60)}`);
|
|
29
|
+
}
|
|
30
|
+
console.error('Use a longer prefix or the full rule_id.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const rule = matches[0];
|
|
34
|
+
if (rule.strength === 'hard') {
|
|
35
|
+
console.error(`Refusing to suppress hard rule "${rule.rule_id}" (ADR-002 immutability).`);
|
|
36
|
+
console.error(' Hard rules require explicit removal from the rule source file.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (rule.status === 'suppressed') {
|
|
40
|
+
console.log(`Rule ${rule.rule_id} is already suppressed.`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
saveRule({ ...rule, status: 'suppressed' });
|
|
44
|
+
console.log(`✓ Suppressed rule ${rule.rule_id}`);
|
|
45
|
+
console.log(` Policy: ${rule.policy.slice(0, 80)}`);
|
|
46
|
+
console.log(` Re-activate with: forgen activate-rule ${rule.rule_id}`);
|
|
47
|
+
}
|
|
48
|
+
export async function handleActivateRule(args) {
|
|
49
|
+
const partial = args[0];
|
|
50
|
+
if (!partial) {
|
|
51
|
+
console.error('Usage: forgen activate-rule <rule_id | prefix>');
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
const all = loadAllRules();
|
|
55
|
+
const matches = all.filter((r) => r.rule_id === partial || r.rule_id.startsWith(partial));
|
|
56
|
+
if (matches.length === 0) {
|
|
57
|
+
console.error(`No rule found matching "${partial}".`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (matches.length > 1) {
|
|
61
|
+
console.error(`Ambiguous prefix "${partial}" — ${matches.length} rules. Use longer prefix.`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const rule = matches[0];
|
|
65
|
+
if (rule.status === 'active') {
|
|
66
|
+
console.log(`Rule ${rule.rule_id} is already active.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (rule.status === 'removed' || rule.status === 'superseded') {
|
|
70
|
+
console.error(`Cannot activate rule with status=${rule.status}. Edit the rule file directly.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
saveRule({ ...rule, status: 'active' });
|
|
74
|
+
console.log(`✓ Activated rule ${rule.rule_id}`);
|
|
75
|
+
console.log(` Policy: ${rule.policy.slice(0, 80)}`);
|
|
76
|
+
}
|
|
@@ -74,8 +74,6 @@ export declare function parseFrontmatterOnly(content: string): SolutionFrontmatt
|
|
|
74
74
|
export declare function parseSolutionV3(content: string): SolutionV3 | null;
|
|
75
75
|
/** Serialize a SolutionV3 to a markdown string with YAML frontmatter */
|
|
76
76
|
export declare function serializeSolutionV3(solution: SolutionV3): string;
|
|
77
|
-
/** Check if content is in V3 format (YAML frontmatter) */
|
|
78
|
-
export declare function isV3Format(content: string): boolean;
|
|
79
77
|
/** Check if content is in V1 format (# Title + > Type: pattern) */
|
|
80
78
|
export declare function isV1Format(content: string): boolean;
|
|
81
79
|
/** 한국어 일반 조사/어미 — strip 대상 (긴 것부터 매칭)
|
|
@@ -162,10 +162,6 @@ export function serializeSolutionV3(solution) {
|
|
|
162
162
|
return `---\n${yamlStr}---\n\n## Context\n${solution.context}\n\n## Content\n${solution.content}\n`;
|
|
163
163
|
}
|
|
164
164
|
// ── Format Detection ──
|
|
165
|
-
/** Check if content is in V3 format (YAML frontmatter) */
|
|
166
|
-
export function isV3Format(content) {
|
|
167
|
-
return content.trimStart().startsWith('---');
|
|
168
|
-
}
|
|
169
165
|
/** Check if content is in V1 format (# Title + > Type: pattern) */
|
|
170
166
|
export function isV1Format(content) {
|
|
171
167
|
const lines = content.split('\n');
|
|
@@ -41,6 +41,14 @@ export interface SolutionMatch {
|
|
|
41
41
|
tags: string[];
|
|
42
42
|
identifiers: string[];
|
|
43
43
|
matchedTags: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Identifier substrings (function/file names) that appeared literally in the
|
|
46
|
+
* prompt. Added 2026-04-21 so solution-injector can enforce a precision gate
|
|
47
|
+
* distinguishing "user typed a specific identifier" (strong signal, survives
|
|
48
|
+
* 1-tag overlap) from "only 1 tag happens to overlap" (often noise — common
|
|
49
|
+
* nouns like 'type', 'file', 'forgen' trigger rare-tag BM25 boost).
|
|
50
|
+
*/
|
|
51
|
+
matchedIdentifiers: string[];
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
46
54
|
* Optional hints for the v3 `calculateRelevance` path. Used by hot-path
|
|
@@ -818,12 +818,14 @@ function loadTunedMatcherWeights() {
|
|
|
818
818
|
* entries and almost always lose the first few rounds — not because
|
|
819
819
|
* they're worse, but because matchers favor solutions with richer tag
|
|
820
820
|
* histories. A small confidence multiplier lets candidates surface often
|
|
821
|
-
* enough to accumulate
|
|
822
|
-
* decides their fate.
|
|
821
|
+
* enough to accumulate reflected/sessions evidence, after which the
|
|
822
|
+
* lifecycle loop decides their fate.
|
|
823
823
|
*
|
|
824
824
|
* The 1.3× factor is a starting point (Q1 in docs/design-solution-evolution.md).
|
|
825
|
-
*
|
|
826
|
-
*
|
|
825
|
+
* Bonus deactivation happens implicitly when compound-lifecycle.ts::
|
|
826
|
+
* runLifecycleCheck promotes the candidate to `verified` based on accumulated
|
|
827
|
+
* reflected/sessions evidence. There is no inject-count-based auto promotion
|
|
828
|
+
* (removed 2026-04-20 — see feedback_core_loop_invariant).
|
|
827
829
|
*/
|
|
828
830
|
const CANDIDATE_EXPLORATION_MULTIPLIER = 1.3;
|
|
829
831
|
function applyCandidateExplorationBonus(entries) {
|
|
@@ -865,5 +867,6 @@ export function matchSolutions(prompt, scope, cwd) {
|
|
|
865
867
|
tags: c.solution.tags,
|
|
866
868
|
identifiers: c.solution.identifiers,
|
|
867
869
|
matchedTags: [...c.matchedTags, ...c.matchedIdentifiers],
|
|
870
|
+
matchedIdentifiers: c.matchedIdentifiers,
|
|
868
871
|
}));
|
|
869
872
|
}
|
|
@@ -52,6 +52,10 @@ export declare function attributeCorrection(sessionId: string): string[];
|
|
|
52
52
|
* — an error is a weaker signal and the next user prompt can still produce
|
|
53
53
|
* a correct/accept decision.
|
|
54
54
|
*
|
|
55
|
+
* Only the top-K most-relevant, recent, above-threshold pending solutions
|
|
56
|
+
* are attributed (see gates above). Below-threshold or stale pending
|
|
57
|
+
* entries are left untouched — they will resolve via accept/unknown later.
|
|
58
|
+
*
|
|
55
59
|
* To avoid flooding the log with duplicate errors for the same pending
|
|
56
60
|
* batch, we cap at one `error` event per (session, solution) pair per
|
|
57
61
|
* pending-cycle by tracking a `error_flagged` set in the pending state.
|