@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.
Files changed (125) hide show
  1. package/.claude-plugin/plugin.json +7 -2
  2. package/CHANGELOG.md +164 -0
  3. package/README.ja.md +90 -7
  4. package/README.ko.md +44 -1
  5. package/README.md +128 -9
  6. package/README.zh.md +90 -7
  7. package/dist/cli.js +140 -8
  8. package/dist/core/auto-compound-runner.js +16 -5
  9. package/dist/core/dashboard.js +11 -4
  10. package/dist/core/doctor.d.ts +6 -1
  11. package/dist/core/doctor.js +85 -11
  12. package/dist/core/global-config.d.ts +2 -2
  13. package/dist/core/global-config.js +6 -14
  14. package/dist/core/harness.d.ts +3 -5
  15. package/dist/core/harness.js +34 -338
  16. package/dist/core/inspect-cli.js +65 -5
  17. package/dist/core/installer.d.ts +10 -0
  18. package/dist/core/installer.js +185 -0
  19. package/dist/core/paths.d.ts +0 -34
  20. package/dist/core/paths.js +0 -35
  21. package/dist/core/settings-injector.d.ts +13 -0
  22. package/dist/core/settings-injector.js +167 -0
  23. package/dist/core/settings-lock.d.ts +35 -2
  24. package/dist/core/settings-lock.js +65 -7
  25. package/dist/core/spawn.js +100 -39
  26. package/dist/core/state-gc.d.ts +49 -0
  27. package/dist/core/state-gc.js +163 -0
  28. package/dist/core/stats-cli.d.ts +15 -0
  29. package/dist/core/stats-cli.js +143 -0
  30. package/dist/core/uninstall.d.ts +1 -0
  31. package/dist/core/uninstall.js +36 -5
  32. package/dist/core/v1-bootstrap.js +11 -3
  33. package/dist/engine/classify-enforce-cli.d.ts +8 -0
  34. package/dist/engine/classify-enforce-cli.js +61 -0
  35. package/dist/engine/compound-cli.d.ts +27 -2
  36. package/dist/engine/compound-cli.js +69 -16
  37. package/dist/engine/compound-export.d.ts +15 -0
  38. package/dist/engine/compound-export.js +32 -5
  39. package/dist/engine/compound-loop.js +3 -2
  40. package/dist/engine/enforce-classifier.d.ts +31 -0
  41. package/dist/engine/enforce-classifier.js +123 -0
  42. package/dist/engine/learn-cli.js +52 -0
  43. package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
  44. package/dist/engine/lifecycle/bypass-detector.js +82 -0
  45. package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
  46. package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
  47. package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
  48. package/dist/engine/lifecycle/meta-cli.js +7 -0
  49. package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
  50. package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
  51. package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
  52. package/dist/engine/lifecycle/orchestrator.js +131 -0
  53. package/dist/engine/lifecycle/signals.d.ts +30 -0
  54. package/dist/engine/lifecycle/signals.js +142 -0
  55. package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
  56. package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
  57. package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
  58. package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
  59. package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
  60. package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
  61. package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
  62. package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
  63. package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
  64. package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
  65. package/dist/engine/lifecycle/types.d.ts +52 -0
  66. package/dist/engine/lifecycle/types.js +7 -0
  67. package/dist/engine/match-eval-log.js +45 -0
  68. package/dist/engine/rule-toggle-cli.d.ts +13 -0
  69. package/dist/engine/rule-toggle-cli.js +76 -0
  70. package/dist/engine/solution-format.d.ts +0 -2
  71. package/dist/engine/solution-format.js +0 -4
  72. package/dist/engine/solution-matcher.d.ts +8 -0
  73. package/dist/engine/solution-matcher.js +7 -4
  74. package/dist/engine/solution-outcomes.d.ts +4 -0
  75. package/dist/engine/solution-outcomes.js +174 -97
  76. package/dist/engine/solution-writer.d.ts +8 -5
  77. package/dist/engine/solution-writer.js +43 -19
  78. package/dist/fgx.js +9 -2
  79. package/dist/forge/cli.js +7 -7
  80. package/dist/forge/evidence-processor.js +10 -2
  81. package/dist/hooks/context-guard.js +86 -1
  82. package/dist/hooks/hook-config.d.ts +9 -1
  83. package/dist/hooks/hook-config.js +25 -3
  84. package/dist/hooks/internal/run-lifecycle-check.d.ts +2 -0
  85. package/dist/hooks/internal/run-lifecycle-check.js +32 -0
  86. package/dist/hooks/notepad-injector.js +6 -3
  87. package/dist/hooks/permission-handler.d.ts +10 -2
  88. package/dist/hooks/permission-handler.js +31 -12
  89. package/dist/hooks/post-tool-use.js +62 -0
  90. package/dist/hooks/pre-tool-use.js +67 -5
  91. package/dist/hooks/secret-filter.d.ts +10 -0
  92. package/dist/hooks/secret-filter.js +26 -0
  93. package/dist/hooks/session-recovery.js +15 -7
  94. package/dist/hooks/shared/atomic-write.d.ts +8 -1
  95. package/dist/hooks/shared/atomic-write.js +17 -3
  96. package/dist/hooks/shared/hook-response.d.ts +11 -2
  97. package/dist/hooks/shared/hook-response.js +20 -7
  98. package/dist/hooks/shared/hook-timing.js +10 -1
  99. package/dist/hooks/shared/safe-regex.d.ts +25 -0
  100. package/dist/hooks/shared/safe-regex.js +50 -0
  101. package/dist/hooks/shared/stop-triggers.d.ts +19 -0
  102. package/dist/hooks/shared/stop-triggers.js +19 -0
  103. package/dist/hooks/solution-injector.d.ts +21 -0
  104. package/dist/hooks/solution-injector.js +60 -1
  105. package/dist/hooks/stop-guard.d.ts +84 -0
  106. package/dist/hooks/stop-guard.js +482 -0
  107. package/dist/mcp/solution-reader.d.ts +2 -0
  108. package/dist/mcp/solution-reader.js +28 -1
  109. package/dist/mcp/tools.js +24 -4
  110. package/dist/preset/preset-manager.js +12 -2
  111. package/dist/store/evidence-store.d.ts +15 -0
  112. package/dist/store/evidence-store.js +55 -6
  113. package/dist/store/profile-store.d.ts +9 -0
  114. package/dist/store/profile-store.js +25 -4
  115. package/dist/store/rule-lifecycle.d.ts +23 -0
  116. package/dist/store/rule-lifecycle.js +63 -0
  117. package/dist/store/rule-store.d.ts +21 -0
  118. package/dist/store/rule-store.js +133 -13
  119. package/dist/store/types.d.ts +83 -0
  120. package/dist/store/types.js +7 -1
  121. package/hooks/hook-registry.json +1 -0
  122. package/hooks/hooks.json +6 -1
  123. package/package.json +10 -2
  124. package/plugin.json +7 -2
  125. 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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * ADR-002 Lifecycle event model.
3
+ *
4
+ * 오케스트레이터가 발행하는 이벤트 — rule 상태 전이의 단위.
5
+ * 이 파일은 타입만 정의. 실제 이벤트 발행/소비 로직은 각 trigger-*.ts 참조.
6
+ */
7
+ export {};
@@ -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 outcome data, after which the fitness loop
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
- * Automatic deactivation after 5 accumulated injections is handled by a
826
- * separate promoter that flips `status` to `verified`.
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.