@wooojin/forgen 0.2.1 → 0.3.1

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 (145) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.ko.md +25 -14
  3. package/README.md +61 -17
  4. package/agents/analyst.md +48 -4
  5. package/agents/architect.md +39 -4
  6. package/agents/code-reviewer.md +107 -77
  7. package/agents/critic.md +47 -4
  8. package/agents/debugger.md +46 -4
  9. package/agents/designer.md +40 -4
  10. package/agents/executor.md +112 -30
  11. package/agents/explore.md +45 -5
  12. package/agents/git-master.md +48 -4
  13. package/agents/planner.md +121 -18
  14. package/agents/solution-evolver.md +115 -0
  15. package/agents/test-engineer.md +58 -4
  16. package/agents/verifier.md +92 -77
  17. package/commands/architecture-decision.md +127 -258
  18. package/commands/calibrate.md +225 -0
  19. package/commands/code-review.md +163 -178
  20. package/commands/compound.md +127 -68
  21. package/commands/deep-interview.md +212 -110
  22. package/commands/docker.md +68 -178
  23. package/commands/forge-loop.md +215 -0
  24. package/commands/learn.md +231 -0
  25. package/commands/retro.md +215 -0
  26. package/commands/ship.md +277 -0
  27. package/dist/cli.js +25 -9
  28. package/dist/core/auto-compound-runner.js +14 -0
  29. package/dist/core/config-injector.d.ts +2 -1
  30. package/dist/core/config-injector.js +2 -1
  31. package/dist/core/dashboard.d.ts +17 -0
  32. package/dist/core/dashboard.js +158 -2
  33. package/dist/core/harness.d.ts +6 -1
  34. package/dist/core/harness.js +75 -19
  35. package/dist/core/paths.d.ts +31 -1
  36. package/dist/core/paths.js +43 -2
  37. package/dist/core/spawn.d.ts +3 -2
  38. package/dist/core/spawn.js +27 -8
  39. package/dist/core/types.d.ts +34 -0
  40. package/dist/engine/compound-lifecycle.d.ts +4 -3
  41. package/dist/engine/compound-lifecycle.js +91 -46
  42. package/dist/engine/learn-cli.d.ts +1 -0
  43. package/dist/engine/learn-cli.js +182 -0
  44. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  45. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  46. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  47. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  48. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  49. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  50. package/dist/engine/meta-learning/runner.d.ts +14 -0
  51. package/dist/engine/meta-learning/runner.js +90 -0
  52. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  53. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  54. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  55. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  56. package/dist/engine/meta-learning/types.d.ts +114 -0
  57. package/dist/engine/meta-learning/types.js +43 -0
  58. package/dist/engine/solution-candidate.d.ts +30 -0
  59. package/dist/engine/solution-candidate.js +124 -0
  60. package/dist/engine/solution-fitness.d.ts +52 -0
  61. package/dist/engine/solution-fitness.js +95 -0
  62. package/dist/engine/solution-fixup.d.ts +30 -0
  63. package/dist/engine/solution-fixup.js +116 -0
  64. package/dist/engine/solution-format.d.ts +10 -2
  65. package/dist/engine/solution-format.js +287 -57
  66. package/dist/engine/solution-index.d.ts +1 -1
  67. package/dist/engine/solution-index.js +10 -0
  68. package/dist/engine/solution-matcher.d.ts +7 -1
  69. package/dist/engine/solution-matcher.js +137 -37
  70. package/dist/engine/solution-outcomes.d.ts +70 -0
  71. package/dist/engine/solution-outcomes.js +242 -0
  72. package/dist/engine/solution-quarantine.d.ts +36 -0
  73. package/dist/engine/solution-quarantine.js +172 -0
  74. package/dist/engine/solution-weakness.d.ts +45 -0
  75. package/dist/engine/solution-weakness.js +225 -0
  76. package/dist/engine/solution-writer.d.ts +5 -0
  77. package/dist/engine/solution-writer.js +18 -0
  78. package/dist/fgx.js +12 -8
  79. package/dist/hooks/context-guard.d.ts +5 -0
  80. package/dist/hooks/context-guard.js +118 -2
  81. package/dist/hooks/hooks-generator.d.ts +3 -0
  82. package/dist/hooks/hooks-generator.js +23 -6
  83. package/dist/hooks/keyword-detector.js +16 -100
  84. package/dist/hooks/post-tool-failure.js +7 -0
  85. package/dist/hooks/skill-injector.d.ts +4 -3
  86. package/dist/hooks/skill-injector.js +6 -4
  87. package/dist/hooks/solution-injector.js +20 -0
  88. package/dist/host/codex-adapter.d.ts +10 -0
  89. package/dist/host/codex-adapter.js +154 -0
  90. package/dist/mcp/solution-reader.d.ts +5 -5
  91. package/dist/mcp/solution-reader.js +34 -24
  92. package/dist/mcp/tools.js +8 -0
  93. package/dist/services/session.d.ts +19 -0
  94. package/dist/services/session.js +62 -0
  95. package/hooks/hooks.json +2 -2
  96. package/package.json +2 -1
  97. package/skills/architecture-decision/SKILL.md +113 -257
  98. package/skills/calibrate/SKILL.md +207 -0
  99. package/skills/code-review/SKILL.md +151 -178
  100. package/skills/compound/SKILL.md +126 -68
  101. package/skills/deep-interview/SKILL.md +210 -110
  102. package/skills/docker/SKILL.md +57 -179
  103. package/skills/forge-loop/SKILL.md +198 -0
  104. package/skills/learn/SKILL.md +216 -0
  105. package/skills/retro/SKILL.md +199 -0
  106. package/skills/ship/SKILL.md +259 -0
  107. package/agents/code-simplifier.md +0 -197
  108. package/agents/performance-reviewer.md +0 -172
  109. package/agents/qa-tester.md +0 -158
  110. package/agents/refactoring-expert.md +0 -168
  111. package/agents/scientist.md +0 -144
  112. package/agents/security-reviewer.md +0 -137
  113. package/agents/writer.md +0 -184
  114. package/commands/api-design.md +0 -268
  115. package/commands/ci-cd.md +0 -270
  116. package/commands/database.md +0 -263
  117. package/commands/debug-detective.md +0 -99
  118. package/commands/documentation.md +0 -276
  119. package/commands/ecomode.md +0 -51
  120. package/commands/frontend.md +0 -271
  121. package/commands/git-master.md +0 -90
  122. package/commands/incident-response.md +0 -292
  123. package/commands/migrate.md +0 -101
  124. package/commands/performance.md +0 -288
  125. package/commands/refactor.md +0 -105
  126. package/commands/security-review.md +0 -288
  127. package/commands/specify.md +0 -128
  128. package/commands/tdd.md +0 -183
  129. package/commands/testing-strategy.md +0 -265
  130. package/skills/api-design/SKILL.md +0 -262
  131. package/skills/ci-cd/SKILL.md +0 -264
  132. package/skills/database/SKILL.md +0 -257
  133. package/skills/debug-detective/SKILL.md +0 -95
  134. package/skills/documentation/SKILL.md +0 -270
  135. package/skills/ecomode/SKILL.md +0 -46
  136. package/skills/frontend/SKILL.md +0 -265
  137. package/skills/git-master/SKILL.md +0 -86
  138. package/skills/incident-response/SKILL.md +0 -286
  139. package/skills/migrate/SKILL.md +0 -96
  140. package/skills/performance/SKILL.md +0 -282
  141. package/skills/refactor/SKILL.md +0 -100
  142. package/skills/security-review/SKILL.md +0 -282
  143. package/skills/specify/SKILL.md +0 -122
  144. package/skills/tdd/SKILL.md +0 -178
  145. package/skills/testing-strategy/SKILL.md +0 -260
@@ -1,11 +1,12 @@
1
+ import { execFileSync } from 'node:child_process';
1
2
  import * as fs from 'node:fs';
2
3
  import * as path from 'node:path';
3
- import { execFileSync } from 'node:child_process';
4
+ import { createLogger } from '../core/logger.js';
4
5
  import { parseFrontmatterOnly } from './solution-format.js';
5
6
  import { mutateSolutionFile } from './solution-writer.js';
6
- import { createLogger } from '../core/logger.js';
7
7
  const log = createLogger('compound-lifecycle');
8
- import { ME_SOLUTIONS, ME_RULES } from '../core/paths.js';
8
+ import { ME_RULES, ME_SOLUTIONS, META_LEARNING_DIR } from '../core/paths.js';
9
+ import { safeReadJSON } from '../hooks/shared/atomic-write.js';
9
10
  /** Circuit breaker negative thresholds by status */
10
11
  const CIRCUIT_BREAKER_THRESHOLDS = {
11
12
  experiment: 2,
@@ -29,10 +30,14 @@ const STATUS_CONFIDENCE_MIN = {
29
30
  /** Get the next promotion status */
30
31
  export function nextStatus(current) {
31
32
  switch (current) {
32
- case 'experiment': return 'candidate';
33
- case 'candidate': return 'verified';
34
- case 'verified': return 'mature';
35
- default: return null;
33
+ case 'experiment':
34
+ return 'candidate';
35
+ case 'candidate':
36
+ return 'verified';
37
+ case 'verified':
38
+ return 'mature';
39
+ default:
40
+ return null;
36
41
  }
37
42
  }
38
43
  /** Get confidence for a status level.
@@ -40,30 +45,54 @@ export function nextStatus(current) {
40
45
  * Previous: 0.3/0.6/0.8/0.85 had only 0.05 gap between verified and mature. */
41
46
  export function statusConfidence(status) {
42
47
  switch (status) {
43
- case 'experiment': return 0.3;
44
- case 'candidate': return 0.55;
45
- case 'verified': return 0.75;
46
- case 'mature': return 0.90;
47
- case 'retired': return 0;
48
+ case 'experiment':
49
+ return 0.3;
50
+ case 'candidate':
51
+ return 0.55;
52
+ case 'verified':
53
+ return 0.75;
54
+ case 'mature':
55
+ return 0.9;
56
+ case 'retired':
57
+ return 0;
48
58
  }
49
59
  }
50
- /** Check promotion eligibility */
51
- export function checkPromotion(fm) {
60
+ /** Load adaptive thresholds from meta-learning state (returns null if not tuned) */
61
+ function loadAdaptiveThresholds() {
62
+ const thresholdsPath = path.join(META_LEARNING_DIR, 'lifecycle-thresholds.json');
63
+ return safeReadJSON(thresholdsPath, null);
64
+ }
65
+ /** Check promotion eligibility (with optional adaptive thresholds) */
66
+ export function checkPromotion(fm, thresholds) {
52
67
  const ev = fm.evidence;
68
+ const t = thresholds ?? null;
53
69
  switch (fm.status) {
54
- case 'experiment':
55
- // A: reflected >= 3 AND negative == 0 AND sessions >= 3 (Beta(4,1) → P(rate>0.5)=0.94)
56
- // B: reExtracted >= 2 AND negative == 0 AND reflected >= 1 (prevents trivial re-extraction)
57
- return (ev.negative === 0) && ((ev.reflected >= 3 && ev.sessions >= 3) ||
58
- (ev.reExtracted >= 2 && ev.reflected >= 1));
59
- case 'candidate':
60
- // A: reflected >= 4 AND negative == 0 AND sessions >= 3
61
- // B: reExtracted >= 2 AND negative == 0
62
- return (ev.negative === 0) && ((ev.reflected >= 4 && ev.sessions >= 3) ||
63
- (ev.reExtracted >= 2));
64
- case 'verified':
65
- // reflected >= 8, negative <= 1, sessions >= 5
66
- return ev.reflected >= 8 && ev.negative <= 1 && ev.sessions >= 5;
70
+ case 'experiment': {
71
+ const minReflected = t?.experiment.reflected ?? 3;
72
+ const minSessions = t?.experiment.sessions ?? 3;
73
+ const minReExtracted = t?.experiment.reExtracted ?? 2;
74
+ // A: reflected >= threshold AND negative == 0 AND sessions >= threshold
75
+ // B: reExtracted >= threshold AND negative == 0 AND reflected >= 1
76
+ return (ev.negative === 0 &&
77
+ ((ev.reflected >= minReflected && ev.sessions >= minSessions) ||
78
+ (ev.reExtracted >= minReExtracted && ev.reflected >= 1)));
79
+ }
80
+ case 'candidate': {
81
+ const minReflected = t?.candidate.reflected ?? 4;
82
+ const minSessions = t?.candidate.sessions ?? 3;
83
+ const minReExtracted = t?.candidate.reExtracted ?? 2;
84
+ // A: reflected >= threshold AND negative == 0 AND sessions >= threshold
85
+ // B: reExtracted >= threshold AND negative == 0
86
+ return (ev.negative === 0 &&
87
+ ((ev.reflected >= minReflected && ev.sessions >= minSessions) ||
88
+ ev.reExtracted >= minReExtracted));
89
+ }
90
+ case 'verified': {
91
+ const minReflected = t?.verified.reflected ?? 8;
92
+ const minSessions = t?.verified.sessions ?? 5;
93
+ const maxNegative = t?.verified.negative ?? 1;
94
+ return (ev.reflected >= minReflected && ev.negative <= maxNegative && ev.sessions >= minSessions);
95
+ }
67
96
  default:
68
97
  return false;
69
98
  }
@@ -88,21 +117,28 @@ export function checkIdentifierStaleness(fm, cwd) {
88
117
  if (fm.identifiers.length === 0)
89
118
  return false; // no identifiers to check
90
119
  try {
91
- const validIds = fm.identifiers.slice(0, 5).filter(id => id.length >= 6);
120
+ const validIds = fm.identifiers.slice(0, 5).filter((id) => id.length >= 6);
92
121
  // All identifiers were too short — nothing to grep, treat as stale (matches original behavior)
93
122
  if (validIds.length === 0)
94
123
  return true;
95
124
  // Escape regex metacharacters and join with OR for a single grep call
96
125
  // (previously: one execFileSync per identifier — up to 15s worst case)
97
- const pattern = validIds.map(id => id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
126
+ const pattern = validIds.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
98
127
  execFileSync('grep', [
99
- '-r', '-E',
100
- '--include=*.ts', '--include=*.tsx',
101
- '--include=*.js', '--include=*.jsx',
128
+ '-r',
129
+ '-E',
130
+ '--include=*.ts',
131
+ '--include=*.tsx',
132
+ '--include=*.js',
133
+ '--include=*.jsx',
102
134
  '--exclude-dir=node_modules',
103
135
  '--exclude-dir=dist',
104
136
  '--exclude-dir=.git',
105
- '-l', '-m', '1', pattern, '.',
137
+ '-l',
138
+ '-m',
139
+ '1',
140
+ pattern,
141
+ '.',
106
142
  ], { cwd, encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
107
143
  return false; // grep exit 0 = at least one identifier found = not stale
108
144
  }
@@ -143,7 +179,7 @@ export function isStale(fm) {
143
179
  * PR2b: solution-writer.mutateSolutionFile로 통합. lock + fresh re-read + atomic write.
144
180
  */
145
181
  export function updateSolutionFile(filePath, updates) {
146
- return mutateSolutionFile(filePath, sol => {
182
+ return mutateSolutionFile(filePath, (sol) => {
147
183
  sol.frontmatter = {
148
184
  ...sol.frontmatter,
149
185
  ...updates,
@@ -152,15 +188,17 @@ export function updateSolutionFile(filePath, updates) {
152
188
  });
153
189
  }
154
190
  /** Run lifecycle check on all solutions */
155
- export function runLifecycleCheck(sessionId = 'system') {
191
+ export function runLifecycleCheck(_sessionId = 'system') {
156
192
  const result = { promoted: [], demoted: [], retired: [], contradictions: [] };
193
+ // Meta-learning: load adaptive thresholds if available
194
+ const adaptiveThresholds = loadAdaptiveThresholds();
157
195
  const dirs = [ME_SOLUTIONS, ME_RULES];
158
196
  for (const dir of dirs) {
159
197
  if (!fs.existsSync(dir))
160
198
  continue;
161
199
  let files;
162
200
  try {
163
- files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
201
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
164
202
  }
165
203
  catch {
166
204
  continue;
@@ -182,7 +220,10 @@ export function runLifecycleCheck(sessionId = 'system') {
182
220
  // 2. Check confidence-status consistency
183
221
  const demoteTo = checkConfidenceDemotion(fm);
184
222
  if (demoteTo) {
185
- if (updateSolutionFile(filePath, { status: demoteTo, confidence: statusConfidence(demoteTo) })) {
223
+ if (updateSolutionFile(filePath, {
224
+ status: demoteTo,
225
+ confidence: statusConfidence(demoteTo),
226
+ })) {
186
227
  result.demoted.push(`${fm.name}: ${fm.status} → ${demoteTo}`);
187
228
  }
188
229
  continue;
@@ -198,7 +239,7 @@ export function runLifecycleCheck(sessionId = 'system') {
198
239
  // 4. Check promotion FIRST (with minimum age gate based on updated timestamp)
199
240
  // Promotion must run before identifier staleness to give solutions a chance
200
241
  // to be promoted before being penalized for stale identifiers.
201
- if (checkPromotion(fm)) {
242
+ if (checkPromotion(fm, adaptiveThresholds)) {
202
243
  const minAgeMs = MIN_AGE_FOR_PROMOTION[fm.status] ?? 0;
203
244
  const ageMs = Date.now() - new Date(fm.updated || fm.created).getTime();
204
245
  if (ageMs >= minAgeMs) {
@@ -215,7 +256,7 @@ export function runLifecycleCheck(sessionId = 'system') {
215
256
  if (fm.identifiers.length > 0) {
216
257
  const effectiveCwd = process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
217
258
  if (checkIdentifierStaleness(fm, effectiveCwd)) {
218
- const newConf = Math.max(0, fm.confidence - 0.20);
259
+ const newConf = Math.max(0, fm.confidence - 0.2);
219
260
  if (updateSolutionFile(filePath, { confidence: newConf })) {
220
261
  result.demoted.push(`${fm.name}: identifier-stale (confidence → ${newConf})`);
221
262
  }
@@ -239,7 +280,7 @@ export function detectContradictions(dirs) {
239
280
  if (!fs.existsSync(dir))
240
281
  continue;
241
282
  try {
242
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
283
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
243
284
  for (const file of files) {
244
285
  const content = fs.readFileSync(path.join(dir, file), 'utf-8');
245
286
  const fm = parseFrontmatterOnly(content);
@@ -248,22 +289,24 @@ export function detectContradictions(dirs) {
248
289
  solutions.push({ name: fm.name, tags: fm.tags, identifiers: fm.identifiers });
249
290
  }
250
291
  }
251
- catch { /* 솔루션 파일 파싱 실패 무시 — 중복 감지는 best-effort */ }
292
+ catch {
293
+ /* 솔루션 파일 파싱 실패 무시 — 중복 감지는 best-effort */
294
+ }
252
295
  }
253
296
  // Pre-build tag Sets for O(1) lookup — avoids O(m²) per pair
254
- const tagSets = solutions.map(s => new Set(s.tags));
297
+ const tagSets = solutions.map((s) => new Set(s.tags));
255
298
  // Pairwise comparison
256
299
  for (let i = 0; i < solutions.length; i++) {
257
300
  for (let j = i + 1; j < solutions.length; j++) {
258
301
  const a = solutions[i];
259
302
  const b = solutions[j];
260
303
  // Tags overlap > 70%
261
- const overlap = a.tags.filter(t => tagSets[j].has(t));
304
+ const overlap = a.tags.filter((t) => tagSets[j].has(t));
262
305
  const overlapRatio = overlap.length / Math.max(a.tags.length, b.tags.length, 1);
263
306
  if (overlapRatio < 0.7)
264
307
  continue;
265
308
  // Identifiers completely different
266
- const idOverlap = a.identifiers.filter(id => b.identifiers.includes(id));
309
+ const idOverlap = a.identifiers.filter((id) => b.identifiers.includes(id));
267
310
  if (idOverlap.length === 0 && a.identifiers.length > 0 && b.identifiers.length > 0) {
268
311
  contradictions.push(`${a.name} vs ${b.name} (tags ${(overlapRatio * 100).toFixed(0)}% overlap, identifiers disjoint)`);
269
312
  }
@@ -278,7 +321,7 @@ export function verifySolution(solutionName) {
278
321
  if (!fs.existsSync(dir))
279
322
  continue;
280
323
  try {
281
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
324
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
282
325
  for (const file of files) {
283
326
  const filePath = path.join(dir, file);
284
327
  // PR2c-4 (security L-1): symlink을 통한 임의 파일 read 차단.
@@ -299,7 +342,9 @@ export function verifySolution(solutionName) {
299
342
  return updateSolutionFile(filePath, { status: 'verified', confidence: 0.8 });
300
343
  }
301
344
  }
302
- catch { /* 솔루션 파일 읽기/업데이트 실패 무시 — false 반환으로 재시도 가능 */ }
345
+ catch {
346
+ /* 솔루션 파일 읽기/업데이트 실패 무시 — false 반환으로 재시도 가능 */
347
+ }
303
348
  }
304
349
  return false;
305
350
  }
@@ -0,0 +1 @@
1
+ export declare function handleLearn(args: string[]): Promise<void>;
@@ -0,0 +1,182 @@
1
+ import * as path from 'node:path';
2
+ import * as os from 'node:os';
3
+ import { fixupSolutions } from './solution-fixup.js';
4
+ import { listQuarantined, pruneQuarantine } from './solution-quarantine.js';
5
+ import { computeFitness } from './solution-fitness.js';
6
+ import { buildWeaknessReport, saveWeaknessReport } from './solution-weakness.js';
7
+ import { listCandidates, promoteCandidate, rollbackSince } from './solution-candidate.js';
8
+ const ME_SOLUTIONS = path.join(os.homedir(), '.forgen', 'me', 'solutions');
9
+ export async function handleLearn(args) {
10
+ const sub = args[0];
11
+ if (sub === 'fix-up')
12
+ return runFixUp(args.slice(1));
13
+ if (sub === 'quarantine')
14
+ return runQuarantine(args.slice(1));
15
+ if (sub === 'fitness')
16
+ return runFitness(args.slice(1));
17
+ if (sub === 'evolve')
18
+ return runEvolve(args.slice(1));
19
+ printUsage();
20
+ }
21
+ function printUsage() {
22
+ console.log(`
23
+ forgen learn — solution index maintenance and fitness
24
+
25
+ Usage:
26
+ forgen learn fix-up [--apply] Repair malformed solution frontmatter (dry-run by default)
27
+ forgen learn quarantine [--prune] Show files dropped by the index; --prune removes fixed/deleted
28
+ forgen learn fitness [--json] Show per-solution fitness (accept/correct/error ratios)
29
+ forgen learn evolve [--save|--rollback <ts>|--promote <name>]
30
+ Phase 4 evolution: weakness report + candidate lifecycle
31
+ `);
32
+ }
33
+ function runFixUp(args) {
34
+ const apply = args.includes('--apply');
35
+ const result = fixupSolutions(ME_SOLUTIONS, { dryRun: !apply });
36
+ console.log(`\n ${apply ? 'Applied' : 'Dry-run'}: scanned=${result.scanned} fixed=${result.fixed} untouched=${result.untouched} unfixable=${result.unfixable}`);
37
+ for (const rep of result.reports) {
38
+ const rel = path.basename(rep.path);
39
+ if (rep.changed && rep.remaining_errors.length === 0) {
40
+ console.log(` ✓ ${rel} — add: ${rep.added.join(', ')}`);
41
+ }
42
+ else {
43
+ console.log(` ✗ ${rel} — remaining: ${rep.remaining_errors.join('; ')}`);
44
+ }
45
+ }
46
+ if (!apply && result.fixed > 0) {
47
+ console.log(`\n Re-run with --apply to write changes.\n`);
48
+ }
49
+ else if (apply && result.fixed > 0) {
50
+ console.log(`\n Consider: forgen learn quarantine --prune\n`);
51
+ }
52
+ else {
53
+ console.log('');
54
+ }
55
+ }
56
+ function runQuarantine(args) {
57
+ if (args.includes('--prune')) {
58
+ const result = pruneQuarantine();
59
+ console.log(`\n Pruned: removed=${result.removed} kept=${result.kept}\n`);
60
+ return;
61
+ }
62
+ const entries = listQuarantined();
63
+ if (entries.length === 0) {
64
+ console.log(`\n No quarantined solutions. ✓\n`);
65
+ return;
66
+ }
67
+ console.log(`\n Quarantined solutions (${entries.length}):\n`);
68
+ for (const e of entries) {
69
+ const rel = path.basename(e.path);
70
+ console.log(` ${rel} (${e.at})`);
71
+ for (const err of e.errors)
72
+ console.log(` - ${err}`);
73
+ }
74
+ console.log(`\n Fix: forgen learn fix-up --apply → then: forgen learn quarantine --prune\n`);
75
+ }
76
+ function runFitness(args) {
77
+ const records = computeFitness();
78
+ if (args.includes('--json')) {
79
+ console.log(JSON.stringify(records, null, 2));
80
+ return;
81
+ }
82
+ if (records.length === 0) {
83
+ console.log(`\n No outcome events yet. Fitness becomes available after solution injections accumulate.\n`);
84
+ return;
85
+ }
86
+ console.log(`\n Solution Fitness (${records.length} tracked):\n`);
87
+ console.log(` ${'name'.padEnd(48)} ${'state'.padEnd(14)} ${'inj'.padStart(4)} ${'acc/cor/err'.padStart(11)} ${'fit'.padStart(6)}`);
88
+ console.log(` ${'-'.repeat(48)} ${'-'.repeat(14)} ${'-'.repeat(4)} ${'-'.repeat(11)} ${'-'.repeat(6)}`);
89
+ for (const r of records) {
90
+ const name = r.solution.length > 47 ? r.solution.slice(0, 45) + '..' : r.solution;
91
+ const acr = `${r.accepted}/${r.corrected}/${r.errored}`;
92
+ console.log(` ${name.padEnd(48)} ${r.state.padEnd(14)} ${String(r.injected).padStart(4)} ${acr.padStart(11)} ${r.fitness.toFixed(2).padStart(6)}`);
93
+ }
94
+ console.log('');
95
+ }
96
+ function runEvolve(args) {
97
+ const save = args.includes('--save');
98
+ const rollbackIdx = args.indexOf('--rollback');
99
+ const promoteIdx = args.indexOf('--promote');
100
+ if (rollbackIdx >= 0 && args[rollbackIdx + 1]) {
101
+ return runEvolveRollback(args[rollbackIdx + 1]);
102
+ }
103
+ if (promoteIdx >= 0 && args[promoteIdx + 1]) {
104
+ return runEvolvePromote(args[promoteIdx + 1]);
105
+ }
106
+ // Default: generate + optionally save weakness report, print proposer
107
+ // brief so the user can hand it to the ch-solution-evolver agent.
108
+ const report = buildWeaknessReport();
109
+ console.log(`\n Weakness Report @ ${report.generated_at}\n`);
110
+ console.log(` Population: ${report.population.total} solutions`);
111
+ console.log(` champion=${report.population.champion} active=${report.population.active} underperform=${report.population.underperform} draft=${report.population.draft}\n`);
112
+ renderTagRow('Under-served tags', report.under_served_tags.map((t) => `${t.tag} (×${t.correction_mentions})`));
113
+ renderTagRow('Conflict clusters', report.conflict_clusters.map((c) => `${c.shared_tags.slice(0, 2).join('+')}: ${c.champion.name} vs ${c.underperform.name}`));
114
+ renderTagRow('Dead corners', report.dead_corners.map((d) => `${d.solution}: [${d.unique_tags.slice(0, 2).join(',')}]`));
115
+ renderTagRow('Volatile', report.volatile.map((v) => `${v.solution} Δ${v.delta}`));
116
+ if (save) {
117
+ const p = saveWeaknessReport(report);
118
+ console.log(`\n Saved: ${p}`);
119
+ console.log(` Next: invoke the ch-solution-evolver agent with this report, then run:`);
120
+ console.log(` forgen learn evolve --promote <candidate-name> # accept one of the 3 proposals`);
121
+ console.log(` forgen learn evolve --rollback ${Date.now()} # undo this week's candidates`);
122
+ console.log('');
123
+ }
124
+ else {
125
+ console.log(`\n Dry-run. Re-run with --save to persist this report and proceed to proposer.\n`);
126
+ }
127
+ }
128
+ function renderTagRow(label, items) {
129
+ if (items.length === 0) {
130
+ console.log(` ${label}: (none)`);
131
+ return;
132
+ }
133
+ console.log(` ${label}:`);
134
+ for (const item of items.slice(0, 5))
135
+ console.log(` - ${item}`);
136
+ }
137
+ function runEvolveRollback(ts) {
138
+ const epochMs = /^\d+$/.test(ts) ? Number(ts) : Date.parse(ts);
139
+ if (!Number.isFinite(epochMs)) {
140
+ console.log(`\n Invalid timestamp: ${ts}. Use epoch ms or ISO-8601.\n`);
141
+ return;
142
+ }
143
+ const result = rollbackSince(epochMs);
144
+ console.log(`\n Rollback since ${new Date(epochMs).toISOString()}:`);
145
+ if (result.archived.length === 0) {
146
+ console.log(` (no evolved solutions newer than cutoff)\n`);
147
+ return;
148
+ }
149
+ console.log(` Archived ${result.archived.length} file(s) → ${result.archive_dir}`);
150
+ for (const p of result.archived)
151
+ console.log(` - ${path.basename(p)}`);
152
+ if (result.errors.length > 0) {
153
+ console.log(` Errors:`);
154
+ for (const e of result.errors)
155
+ console.log(` ! ${e}`);
156
+ }
157
+ console.log('');
158
+ }
159
+ function runEvolvePromote(candidateNameOrList) {
160
+ if (candidateNameOrList === '--list' || candidateNameOrList === 'list') {
161
+ const found = listCandidates();
162
+ if (found.length === 0) {
163
+ console.log(`\n No pending candidates in ~/.forgen/lab/candidates/\n`);
164
+ return;
165
+ }
166
+ console.log(`\n Pending candidates (${found.length}):`);
167
+ for (const p of found)
168
+ console.log(` - ${path.basename(p, '.md')}`);
169
+ console.log(`\n Promote one: forgen learn evolve --promote <name>\n`);
170
+ return;
171
+ }
172
+ const result = promoteCandidate(candidateNameOrList);
173
+ if (result.ok) {
174
+ console.log(`\n ✓ Promoted: ${path.basename(result.dest)}`);
175
+ console.log(` from: ${result.source}`);
176
+ console.log(` to: ${result.dest}`);
177
+ console.log(` Cold-start bonus active until 5 injections accumulate (auto-promotes to verified).\n`);
178
+ }
179
+ else {
180
+ console.log(`\n ✗ Promotion refused: ${result.reason}\n`);
181
+ }
182
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Forgen Meta-Learning — Adaptive Thresholds (Feature 4)
3
+ *
4
+ * Computes learning velocity and adapts promotion thresholds
5
+ * based on the user's solution accumulation rate.
6
+ *
7
+ * Learning velocity = solutions created in last 30 days / 4.3 weeks
8
+ *
9
+ * > 3/week (high volume) → thresholds +1 (more evidence needed)
10
+ * 0.5~3/week (normal) → no change
11
+ * < 0.5/week (slow pace) → thresholds -1 (lower the bar)
12
+ *
13
+ * Guardrails: [thresholdFloor, thresholdCeiling], max ±1 per cycle.
14
+ */
15
+ import type { AdaptiveLifecycleThresholds, MetaLearningConfig } from './types.js';
16
+ /**
17
+ * Compute adaptive promotion thresholds based on learning velocity.
18
+ * Returns null if cold-start conditions are not met.
19
+ */
20
+ export declare function computeAdaptiveThresholds(config: MetaLearningConfig): AdaptiveLifecycleThresholds | null;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Forgen Meta-Learning — Adaptive Thresholds (Feature 4)
3
+ *
4
+ * Computes learning velocity and adapts promotion thresholds
5
+ * based on the user's solution accumulation rate.
6
+ *
7
+ * Learning velocity = solutions created in last 30 days / 4.3 weeks
8
+ *
9
+ * > 3/week (high volume) → thresholds +1 (more evidence needed)
10
+ * 0.5~3/week (normal) → no change
11
+ * < 0.5/week (slow pace) → thresholds -1 (lower the bar)
12
+ *
13
+ * Guardrails: [thresholdFloor, thresholdCeiling], max ±1 per cycle.
14
+ */
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { ME_SOLUTIONS, META_LEARNING_DIR } from '../../core/paths.js';
18
+ import { atomicWriteJSON, safeReadJSON } from '../../hooks/shared/atomic-write.js';
19
+ import { parseFrontmatterOnly } from '../solution-format.js';
20
+ import { DEFAULT_PROMOTION_THRESHOLDS } from './types.js';
21
+ const THRESHOLDS_PATH = path.join(META_LEARNING_DIR, 'lifecycle-thresholds.json');
22
+ const VELOCITY_WINDOW_DAYS = 30;
23
+ const WEEKS_IN_WINDOW = VELOCITY_WINDOW_DAYS / 7;
24
+ function loadCurrentThresholds() {
25
+ return safeReadJSON(THRESHOLDS_PATH, null);
26
+ }
27
+ function saveThresholds(t) {
28
+ atomicWriteJSON(THRESHOLDS_PATH, t, { pretty: true });
29
+ }
30
+ function clampThreshold(value, floor, ceiling) {
31
+ return Math.max(floor, Math.min(ceiling, Math.round(value)));
32
+ }
33
+ function computeLearningVelocity() {
34
+ try {
35
+ if (!fs.existsSync(ME_SOLUTIONS))
36
+ return { velocity: 0, totalSolutions: 0 };
37
+ const files = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md'));
38
+ const now = Date.now();
39
+ const windowMs = VELOCITY_WINDOW_DAYS * 24 * 60 * 60 * 1000;
40
+ let recentCount = 0;
41
+ let totalSolutions = 0;
42
+ for (const file of files) {
43
+ try {
44
+ const content = fs.readFileSync(path.join(ME_SOLUTIONS, file), 'utf-8');
45
+ const fm = parseFrontmatterOnly(content);
46
+ if (!fm || fm.status === 'retired')
47
+ continue;
48
+ totalSolutions++;
49
+ const created = fm.created ? new Date(fm.created).getTime() : 0;
50
+ if (created > 0 && now - created <= windowMs) {
51
+ recentCount++;
52
+ }
53
+ }
54
+ catch { }
55
+ }
56
+ return {
57
+ velocity: recentCount / WEEKS_IN_WINDOW,
58
+ totalSolutions,
59
+ };
60
+ }
61
+ catch {
62
+ return { velocity: 0, totalSolutions: 0 };
63
+ }
64
+ }
65
+ /**
66
+ * Compute adaptive promotion thresholds based on learning velocity.
67
+ * Returns null if cold-start conditions are not met.
68
+ */
69
+ export function computeAdaptiveThresholds(config) {
70
+ const { velocity, totalSolutions } = computeLearningVelocity();
71
+ // Cold-start check
72
+ if (totalSolutions < config.coldStart.minSolutionsForThresholds) {
73
+ return null;
74
+ }
75
+ const current = loadCurrentThresholds();
76
+ const base = current ?? {
77
+ experiment: { ...DEFAULT_PROMOTION_THRESHOLDS.experiment },
78
+ candidate: { ...DEFAULT_PROMOTION_THRESHOLDS.candidate },
79
+ verified: { ...DEFAULT_PROMOTION_THRESHOLDS.verified },
80
+ learningVelocity: velocity,
81
+ updatedAt: new Date().toISOString(),
82
+ sampleSize: totalSolutions,
83
+ defaults: { ...DEFAULT_PROMOTION_THRESHOLDS },
84
+ };
85
+ // Determine adjustment direction
86
+ const { maxThresholdDelta, thresholdFloor, thresholdCeiling } = config.guardrails;
87
+ let delta = 0;
88
+ if (velocity > 3) {
89
+ delta = maxThresholdDelta; // high volume → stricter
90
+ }
91
+ else if (velocity < 0.5) {
92
+ delta = -maxThresholdDelta; // slow pace → more lenient
93
+ }
94
+ if (delta === 0 && current) {
95
+ // No change needed, but update metadata
96
+ current.learningVelocity = velocity;
97
+ current.sampleSize = totalSolutions;
98
+ current.updatedAt = new Date().toISOString();
99
+ saveThresholds(current);
100
+ return current;
101
+ }
102
+ const result = {
103
+ experiment: {
104
+ reflected: clampThreshold(base.experiment.reflected + delta, thresholdFloor, thresholdCeiling),
105
+ sessions: clampThreshold(base.experiment.sessions + delta, thresholdFloor, thresholdCeiling),
106
+ reExtracted: clampThreshold(base.experiment.reExtracted + delta, thresholdFloor, thresholdCeiling),
107
+ },
108
+ candidate: {
109
+ reflected: clampThreshold(base.candidate.reflected + delta, thresholdFloor, thresholdCeiling),
110
+ sessions: clampThreshold(base.candidate.sessions + delta, thresholdFloor, thresholdCeiling),
111
+ reExtracted: clampThreshold(base.candidate.reExtracted + delta, thresholdFloor, thresholdCeiling),
112
+ },
113
+ verified: {
114
+ reflected: clampThreshold(base.verified.reflected + delta, thresholdFloor, thresholdCeiling),
115
+ sessions: clampThreshold(base.verified.sessions + delta, thresholdFloor, thresholdCeiling),
116
+ reExtracted: clampThreshold(base.verified.reExtracted + delta, thresholdFloor, thresholdCeiling),
117
+ negative: base.verified.negative, // negative threshold does not adapt
118
+ },
119
+ learningVelocity: velocity,
120
+ updatedAt: new Date().toISOString(),
121
+ sampleSize: totalSolutions,
122
+ defaults: { ...DEFAULT_PROMOTION_THRESHOLDS },
123
+ };
124
+ saveThresholds(result);
125
+ return result;
126
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Forgen Meta-Learning — Extraction Tuner (Feature 5)
3
+ *
4
+ * Tracks which solution types (pattern, solution, decision, etc.) have the
5
+ * best reflected/injected ratio and biases future extraction toward those types.
6
+ *
7
+ * Uses Laplace smoothing (pseudo-count +1) to prevent zero weights
8
+ * for underrepresented types.
9
+ */
10
+ import type { ExtractionBias, MetaLearningConfig } from './types.js';
11
+ /**
12
+ * Compute extraction bias based on type effectiveness.
13
+ * Returns null if cold-start conditions are not met.
14
+ */
15
+ export declare function computeExtractionBias(config: MetaLearningConfig): ExtractionBias | null;