@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
@@ -0,0 +1,99 @@
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 * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import { ME_SOLUTIONS, META_LEARNING_DIR } from '../../core/paths.js';
13
+ import { atomicWriteJSON } from '../../hooks/shared/atomic-write.js';
14
+ import { parseFrontmatterOnly } from '../solution-format.js';
15
+ const BIAS_PATH = path.join(META_LEARNING_DIR, 'extraction-bias.json');
16
+ const ALL_TYPES = [
17
+ 'pattern',
18
+ 'solution',
19
+ 'decision',
20
+ 'troubleshoot',
21
+ 'anti-pattern',
22
+ 'convention',
23
+ ];
24
+ function computeTypeEffectiveness() {
25
+ const stats = {};
26
+ for (const t of ALL_TYPES) {
27
+ stats[t] = { injected: 0, reflected: 0, ratio: 0 };
28
+ }
29
+ let totalSolutions = 0;
30
+ const typesWithData = new Set();
31
+ try {
32
+ if (!fs.existsSync(ME_SOLUTIONS))
33
+ return { stats, totalSolutions: 0, typeCount: 0 };
34
+ const files = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md'));
35
+ for (const file of files) {
36
+ try {
37
+ const content = fs.readFileSync(path.join(ME_SOLUTIONS, file), 'utf-8');
38
+ const fm = parseFrontmatterOnly(content);
39
+ if (!fm || fm.status === 'retired')
40
+ continue;
41
+ totalSolutions++;
42
+ const type = fm.type;
43
+ if (!stats[type])
44
+ stats[type] = { injected: 0, reflected: 0, ratio: 0 };
45
+ stats[type].injected += fm.evidence.injected;
46
+ stats[type].reflected += fm.evidence.reflected;
47
+ if (fm.evidence.injected > 0)
48
+ typesWithData.add(type);
49
+ }
50
+ catch { }
51
+ }
52
+ }
53
+ catch {
54
+ /* empty */
55
+ }
56
+ // Compute ratios
57
+ for (const type of Object.keys(stats)) {
58
+ stats[type].ratio = stats[type].injected > 0 ? stats[type].reflected / stats[type].injected : 0;
59
+ }
60
+ return { stats, totalSolutions, typeCount: typesWithData.size };
61
+ }
62
+ /**
63
+ * Compute extraction bias based on type effectiveness.
64
+ * Returns null if cold-start conditions are not met.
65
+ */
66
+ export function computeExtractionBias(config) {
67
+ const { stats, totalSolutions, typeCount } = computeTypeEffectiveness();
68
+ // Cold-start check
69
+ if (totalSolutions < config.coldStart.minSolutionsForExtraction || typeCount < 3) {
70
+ return null;
71
+ }
72
+ // Laplace-smoothed weights: ratio + 1 pseudo-count per type
73
+ const rawWeights = {};
74
+ let sum = 0;
75
+ for (const type of ALL_TYPES) {
76
+ const w = stats[type].ratio + 1 / ALL_TYPES.length; // Laplace smoothing
77
+ rawWeights[type] = w;
78
+ sum += w;
79
+ }
80
+ // Normalize to sum = 1.0, cap individual type at 0.5
81
+ const typeWeights = {};
82
+ for (const type of ALL_TYPES) {
83
+ typeWeights[type] = Math.min(0.5, Math.round((rawWeights[type] / sum) * 1000) / 1000);
84
+ }
85
+ // Re-normalize after capping
86
+ const cappedSum = Object.values(typeWeights).reduce((s, v) => s + v, 0);
87
+ if (cappedSum > 0) {
88
+ for (const type of ALL_TYPES) {
89
+ typeWeights[type] = Math.round((typeWeights[type] / cappedSum) * 1000) / 1000;
90
+ }
91
+ }
92
+ const result = {
93
+ typeWeights,
94
+ updatedAt: new Date().toISOString(),
95
+ sampleSize: totalSolutions,
96
+ };
97
+ atomicWriteJSON(BIAS_PATH, result, { pretty: true });
98
+ return result;
99
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Forgen Meta-Learning — Matcher Weight Tuner (Feature 2)
3
+ *
4
+ * Analyzes which scoring component (TF-IDF, BM25, Bigram) best discriminates
5
+ * reflected vs. non-reflected solutions and adjusts ensemble weights.
6
+ *
7
+ * Algorithm:
8
+ * 1. Load all non-retired solutions with evidence.injected > 0
9
+ * 2. Partition into "effective" (reflected/injected > median) vs "ineffective"
10
+ * 3. For each component: compute discrimination ratio (effective_mean / ineffective_mean)
11
+ * 4. Shift weights toward the component with highest discrimination
12
+ * 5. Apply guardrails: clamp [floor, ceiling], max delta per cycle, normalize to 1.0
13
+ *
14
+ * Cold-start: requires 10+ solutions with injected > 0, 3+ with reflected > 0.
15
+ */
16
+ import type { MatcherWeights, MetaLearningConfig } from './types.js';
17
+ /**
18
+ * Tune matcher ensemble weights based on solution effectiveness data.
19
+ * Returns null if cold-start conditions are not met.
20
+ */
21
+ export declare function tuneMatcherWeights(config: MetaLearningConfig): MatcherWeights | null;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Forgen Meta-Learning — Matcher Weight Tuner (Feature 2)
3
+ *
4
+ * Analyzes which scoring component (TF-IDF, BM25, Bigram) best discriminates
5
+ * reflected vs. non-reflected solutions and adjusts ensemble weights.
6
+ *
7
+ * Algorithm:
8
+ * 1. Load all non-retired solutions with evidence.injected > 0
9
+ * 2. Partition into "effective" (reflected/injected > median) vs "ineffective"
10
+ * 3. For each component: compute discrimination ratio (effective_mean / ineffective_mean)
11
+ * 4. Shift weights toward the component with highest discrimination
12
+ * 5. Apply guardrails: clamp [floor, ceiling], max delta per cycle, normalize to 1.0
13
+ *
14
+ * Cold-start: requires 10+ solutions with injected > 0, 3+ with reflected > 0.
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import { ME_SOLUTIONS, META_LEARNING_DIR } from '../../core/paths.js';
19
+ import { atomicWriteJSON, safeReadJSON } from '../../hooks/shared/atomic-write.js';
20
+ import { parseFrontmatterOnly } from '../solution-format.js';
21
+ import { DEFAULT_MATCHER_WEIGHTS } from './types.js';
22
+ const WEIGHTS_PATH = path.join(META_LEARNING_DIR, 'matcher-weights.json');
23
+ function loadSolutionEffectivenessData() {
24
+ try {
25
+ if (!fs.existsSync(ME_SOLUTIONS))
26
+ return [];
27
+ const files = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md'));
28
+ const data = [];
29
+ for (const file of files) {
30
+ try {
31
+ const content = fs.readFileSync(path.join(ME_SOLUTIONS, file), 'utf-8');
32
+ const fm = parseFrontmatterOnly(content);
33
+ if (!fm || fm.status === 'retired')
34
+ continue;
35
+ if (!fm.evidence || fm.evidence.injected <= 0)
36
+ continue;
37
+ data.push({
38
+ name: fm.name,
39
+ injected: fm.evidence.injected,
40
+ reflected: fm.evidence.reflected,
41
+ ratio: fm.evidence.reflected / fm.evidence.injected,
42
+ tags: fm.tags ?? [],
43
+ });
44
+ }
45
+ catch { }
46
+ }
47
+ return data;
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ function loadCurrentWeights() {
54
+ return safeReadJSON(WEIGHTS_PATH, null);
55
+ }
56
+ function saveWeights(weights) {
57
+ atomicWriteJSON(WEIGHTS_PATH, weights, { pretty: true });
58
+ }
59
+ function median(values) {
60
+ if (values.length === 0)
61
+ return 0;
62
+ const sorted = [...values].sort((a, b) => a - b);
63
+ const mid = Math.floor(sorted.length / 2);
64
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
65
+ }
66
+ function clampWeight(value, floor, ceiling) {
67
+ return Math.max(floor, Math.min(ceiling, value));
68
+ }
69
+ function normalizeWeights(w) {
70
+ const sum = w.tfidf + w.bm25 + w.bigram;
71
+ if (sum <= 0)
72
+ return { ...DEFAULT_MATCHER_WEIGHTS };
73
+ return {
74
+ tfidf: Math.round((w.tfidf / sum) * 1000) / 1000,
75
+ bm25: Math.round((w.bm25 / sum) * 1000) / 1000,
76
+ bigram: Math.round((w.bigram / sum) * 1000) / 1000,
77
+ };
78
+ }
79
+ /**
80
+ * Tune matcher ensemble weights based on solution effectiveness data.
81
+ * Returns null if cold-start conditions are not met.
82
+ */
83
+ export function tuneMatcherWeights(config) {
84
+ const data = loadSolutionEffectivenessData();
85
+ // Cold-start check
86
+ const injectedCount = data.length;
87
+ const reflectedCount = data.filter((d) => d.reflected > 0).length;
88
+ if (injectedCount < config.coldStart.minSolutionsForMatcher || reflectedCount < 3) {
89
+ return null;
90
+ }
91
+ // Partition by median effectiveness ratio
92
+ const ratios = data.map((d) => d.ratio);
93
+ const medianRatio = median(ratios);
94
+ const effective = data.filter((d) => d.ratio > medianRatio);
95
+ const ineffective = data.filter((d) => d.ratio <= medianRatio);
96
+ if (effective.length === 0 || ineffective.length === 0)
97
+ return null;
98
+ // Compute discrimination signals per component.
99
+ // We use tag count as a proxy for component contribution:
100
+ // - TF-IDF benefits from more exact tag matches
101
+ // - BM25 benefits from longer documents (more tags)
102
+ // - Bigram benefits from partial/fuzzy matches (shorter tags)
103
+ //
104
+ // Since we can't replay the exact scoring without the original queries,
105
+ // we use statistical proxies from the solution characteristics.
106
+ const avgTagsEffective = effective.reduce((s, d) => s + d.tags.length, 0) / effective.length;
107
+ const avgTagsIneffective = ineffective.reduce((s, d) => s + d.tags.length, 0) / ineffective.length;
108
+ // Discrimination signals:
109
+ // - If effective solutions have more tags → BM25 (length normalization) discriminates well
110
+ // - If effective solutions have fewer tags → TF-IDF (exact match) discriminates well
111
+ // - Bigram gets a boost proportional to how many effective solutions have short tags
112
+ const tagRatio = avgTagsEffective / Math.max(avgTagsIneffective, 1);
113
+ const shortTagEffective = effective.filter((d) => d.tags.some((t) => t.length <= 5)).length / effective.length;
114
+ // Raw discrimination scores (higher = more discriminating)
115
+ const tfidfSignal = tagRatio < 1 ? 1.2 : 1.0; // exact match helps when effective have fewer tags
116
+ const bm25Signal = tagRatio > 1 ? 1.2 : 1.0; // length normalization helps when effective have more tags
117
+ const bigramSignal = shortTagEffective > 0.5 ? 1.15 : 0.95; // fuzzy match helps with short tags
118
+ // Load current weights or defaults
119
+ const current = loadCurrentWeights();
120
+ const currentW = current
121
+ ? { tfidf: current.tfidf, bm25: current.bm25, bigram: current.bigram }
122
+ : { ...DEFAULT_MATCHER_WEIGHTS };
123
+ // Compute target weights from discrimination signals
124
+ const signalSum = tfidfSignal + bm25Signal + bigramSignal;
125
+ const targetW = {
126
+ tfidf: tfidfSignal / signalSum,
127
+ bm25: bm25Signal / signalSum,
128
+ bigram: bigramSignal / signalSum,
129
+ };
130
+ // Apply max delta per cycle guardrail
131
+ const { maxWeightDelta, weightFloor, weightCeiling } = config.guardrails;
132
+ const newW = {
133
+ tfidf: clampWeight(currentW.tfidf +
134
+ Math.max(-maxWeightDelta, Math.min(maxWeightDelta, targetW.tfidf - currentW.tfidf)), weightFloor, weightCeiling),
135
+ bm25: clampWeight(currentW.bm25 +
136
+ Math.max(-maxWeightDelta, Math.min(maxWeightDelta, targetW.bm25 - currentW.bm25)), weightFloor, weightCeiling),
137
+ bigram: clampWeight(currentW.bigram +
138
+ Math.max(-maxWeightDelta, Math.min(maxWeightDelta, targetW.bigram - currentW.bigram)), weightFloor, weightCeiling),
139
+ };
140
+ // Normalize to sum = 1.0
141
+ const normalized = normalizeWeights(newW);
142
+ const result = {
143
+ ...normalized,
144
+ updatedAt: new Date().toISOString(),
145
+ sampleSize: data.length,
146
+ version: (current?.version ?? 0) + 1,
147
+ defaults: { ...DEFAULT_MATCHER_WEIGHTS },
148
+ };
149
+ saveWeights(result);
150
+ return result;
151
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Forgen Meta-Learning — Runner (Orchestrator)
3
+ *
4
+ * Coordinates execution of all meta-learning features at session end.
5
+ * Called from auto-compound-runner.ts as Step 5.
6
+ *
7
+ * Design:
8
+ * - Each feature is independently gated by config + cold-start check
9
+ * - Failures in one feature do not block others (fail-open per feature)
10
+ * - Session quality scoring always runs first (feeds other features)
11
+ */
12
+ import { type MetaLearningConfig, type MetaLearningResult } from './types.js';
13
+ export declare function loadMetaLearningConfig(): MetaLearningConfig;
14
+ export declare function runMetaLearning(sessionId: string, cwd: string): MetaLearningResult;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Forgen Meta-Learning — Runner (Orchestrator)
3
+ *
4
+ * Coordinates execution of all meta-learning features at session end.
5
+ * Called from auto-compound-runner.ts as Step 5.
6
+ *
7
+ * Design:
8
+ * - Each feature is independently gated by config + cold-start check
9
+ * - Failures in one feature do not block others (fail-open per feature)
10
+ * - Session quality scoring always runs first (feeds other features)
11
+ */
12
+ import * as path from 'node:path';
13
+ import { safeReadJSON } from '../../hooks/shared/atomic-write.js';
14
+ import { computeAdaptiveThresholds } from './adaptive-thresholds.js';
15
+ import { computeExtractionBias } from './extraction-tuner.js';
16
+ import { tuneMatcherWeights } from './matcher-weight-tuner.js';
17
+ import { checkScopePromotions, updateProjectUsageMap } from './scope-promoter.js';
18
+ import { saveSessionQuality, scoreSession } from './session-quality-scorer.js';
19
+ import { DEFAULT_CONFIG } from './types.js';
20
+ export function loadMetaLearningConfig() {
21
+ const hookConfigPath = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '', '.forgen', 'hook-config.json');
22
+ const hookConfig = safeReadJSON(hookConfigPath, {});
23
+ const metaSection = hookConfig?.['meta-learning'];
24
+ if (!metaSection)
25
+ return DEFAULT_CONFIG;
26
+ return {
27
+ enabled: metaSection.enabled ?? DEFAULT_CONFIG.enabled,
28
+ features: { ...DEFAULT_CONFIG.features, ...metaSection.features },
29
+ coldStart: { ...DEFAULT_CONFIG.coldStart, ...metaSection.coldStart },
30
+ guardrails: { ...DEFAULT_CONFIG.guardrails, ...metaSection.guardrails },
31
+ };
32
+ }
33
+ export function runMetaLearning(sessionId, cwd) {
34
+ const config = loadMetaLearningConfig();
35
+ if (!config.enabled) {
36
+ return { skipped: true, reason: 'meta-learning disabled in config' };
37
+ }
38
+ const result = {};
39
+ // Feature 1: Session Quality Scorer (always first — feeds other features)
40
+ if (config.features.sessionQualityScorer) {
41
+ try {
42
+ const score = scoreSession(sessionId);
43
+ if (score) {
44
+ saveSessionQuality(score);
45
+ result.qualityScore = score;
46
+ }
47
+ }
48
+ catch (e) {
49
+ process.stderr.write(`[forgen-meta] quality scorer: ${e instanceof Error ? e.message : String(e)}\n`);
50
+ }
51
+ }
52
+ // Feature 2: Matcher Weight Tuning
53
+ if (config.features.matcherWeightTuning) {
54
+ try {
55
+ result.matcherWeights = tuneMatcherWeights(config);
56
+ }
57
+ catch (e) {
58
+ process.stderr.write(`[forgen-meta] matcher tuning: ${e instanceof Error ? e.message : String(e)}\n`);
59
+ }
60
+ }
61
+ // Feature 3: Scope Auto-Promotion
62
+ if (config.features.scopeAutoPromotion) {
63
+ try {
64
+ updateProjectUsageMap(sessionId, cwd, config);
65
+ result.scopePromotions = checkScopePromotions(config);
66
+ }
67
+ catch (e) {
68
+ process.stderr.write(`[forgen-meta] scope promotion: ${e instanceof Error ? e.message : String(e)}\n`);
69
+ }
70
+ }
71
+ // Feature 4: Adaptive Thresholds
72
+ if (config.features.adaptiveThresholds) {
73
+ try {
74
+ result.thresholds = computeAdaptiveThresholds(config);
75
+ }
76
+ catch (e) {
77
+ process.stderr.write(`[forgen-meta] adaptive thresholds: ${e instanceof Error ? e.message : String(e)}\n`);
78
+ }
79
+ }
80
+ // Feature 5: Extraction Tuning
81
+ if (config.features.extractionTuning) {
82
+ try {
83
+ result.extractionBias = computeExtractionBias(config);
84
+ }
85
+ catch (e) {
86
+ process.stderr.write(`[forgen-meta] extraction tuning: ${e instanceof Error ? e.message : String(e)}\n`);
87
+ }
88
+ }
89
+ return result;
90
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Forgen Meta-Learning — Scope Promoter (Feature 3)
3
+ *
4
+ * Tracks cross-project solution usage and auto-promotes solutions
5
+ * from scope:'me' to scope:'universal' when used in 3+ distinct projects.
6
+ *
7
+ * Data flow:
8
+ * 1. At session end: read injection-cache for injected solutions + session cwd
9
+ * 2. Record (solution, project) pair in project-usage-map.json
10
+ * 3. Check if any solution has 3+ distinct projects → mutate frontmatter
11
+ */
12
+ import type { MetaLearningConfig } from './types.js';
13
+ /**
14
+ * Record which project (cwd) each injected solution was used in.
15
+ */
16
+ export declare function updateProjectUsageMap(sessionId: string, cwd: string, _config: MetaLearningConfig): void;
17
+ /**
18
+ * Check for solutions that should be promoted to universal scope.
19
+ * Returns names of promoted solutions.
20
+ */
21
+ export declare function checkScopePromotions(config: MetaLearningConfig): string[];
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Forgen Meta-Learning — Scope Promoter (Feature 3)
3
+ *
4
+ * Tracks cross-project solution usage and auto-promotes solutions
5
+ * from scope:'me' to scope:'universal' when used in 3+ distinct projects.
6
+ *
7
+ * Data flow:
8
+ * 1. At session end: read injection-cache for injected solutions + session cwd
9
+ * 2. Record (solution, project) pair in project-usage-map.json
10
+ * 3. Check if any solution has 3+ distinct projects → mutate frontmatter
11
+ */
12
+ import * as path from 'node:path';
13
+ import { META_LEARNING_DIR, STATE_DIR } from '../../core/paths.js';
14
+ import { atomicWriteJSON, safeReadJSON } from '../../hooks/shared/atomic-write.js';
15
+ import { mutateSolutionByName } from '../solution-writer.js';
16
+ const USAGE_MAP_PATH = path.join(META_LEARNING_DIR, 'project-usage-map.json');
17
+ function sanitizeId(id) {
18
+ return id.replace(/[^a-zA-Z0-9_-]/g, '_');
19
+ }
20
+ function loadUsageMap() {
21
+ return safeReadJSON(USAGE_MAP_PATH, { solutions: {} });
22
+ }
23
+ function saveUsageMap(map) {
24
+ atomicWriteJSON(USAGE_MAP_PATH, map, { pretty: true });
25
+ }
26
+ function loadInjectedSolutions(sessionId) {
27
+ // Try solution-cache (primary) and injection-cache (fallback)
28
+ for (const prefix of ['solution-cache', 'injection-cache']) {
29
+ const cachePath = path.join(STATE_DIR, `${prefix}-${sanitizeId(sessionId)}.json`);
30
+ const data = safeReadJSON(cachePath, {});
31
+ if (data.injected && data.injected.length > 0)
32
+ return data.injected;
33
+ }
34
+ return [];
35
+ }
36
+ /**
37
+ * Record which project (cwd) each injected solution was used in.
38
+ */
39
+ export function updateProjectUsageMap(sessionId, cwd, _config) {
40
+ const injected = loadInjectedSolutions(sessionId);
41
+ if (injected.length === 0)
42
+ return;
43
+ // Normalize cwd to project root name for privacy
44
+ const projectKey = path.basename(cwd);
45
+ const map = loadUsageMap();
46
+ let changed = false;
47
+ for (const name of injected) {
48
+ if (!map.solutions[name]) {
49
+ map.solutions[name] = { projects: [projectKey], updatedAt: new Date().toISOString() };
50
+ changed = true;
51
+ }
52
+ else if (!map.solutions[name].projects.includes(projectKey)) {
53
+ map.solutions[name].projects.push(projectKey);
54
+ map.solutions[name].updatedAt = new Date().toISOString();
55
+ changed = true;
56
+ }
57
+ }
58
+ if (changed)
59
+ saveUsageMap(map);
60
+ }
61
+ /**
62
+ * Check for solutions that should be promoted to universal scope.
63
+ * Returns names of promoted solutions.
64
+ */
65
+ export function checkScopePromotions(config) {
66
+ const map = loadUsageMap();
67
+ const minProjects = config.coldStart.minProjectsForScope;
68
+ const promoted = [];
69
+ for (const [name, entry] of Object.entries(map.solutions)) {
70
+ if (entry.projects.length < minProjects)
71
+ continue;
72
+ const success = mutateSolutionByName(name, (sol) => {
73
+ if (sol.frontmatter.scope === 'universal')
74
+ return false; // already promoted
75
+ if (sol.frontmatter.scope !== 'me')
76
+ return false; // only promote from 'me'
77
+ sol.frontmatter.scope = 'universal';
78
+ return true;
79
+ });
80
+ if (success)
81
+ promoted.push(name);
82
+ }
83
+ return promoted;
84
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Forgen Meta-Learning — Session Quality Scorer (Feature 1)
3
+ *
4
+ * Joins existing data sources to compute a per-session quality score.
5
+ * This score feeds other meta-learning features (matcher tuning, thresholds).
6
+ *
7
+ * Data sources:
8
+ * - injection-cache-{sessionId}.json → injected solutions
9
+ * - modified-files-{sessionId}.json → drift state
10
+ * - implicit-feedback.jsonl → revert/drift events
11
+ * - me/behavior/*.json → correction evidence
12
+ * - state/sessions/{sessionId}.json → session metadata
13
+ */
14
+ import type { SessionQualityScore } from './types.js';
15
+ interface InjectionCacheData {
16
+ injected: string[];
17
+ totalInjectedChars: number;
18
+ updatedAt: string;
19
+ /** Tag snapshot per solution (backfill feature) */
20
+ tags?: Record<string, string[]>;
21
+ }
22
+ interface DriftState {
23
+ sessionId: string;
24
+ totalEdits: number;
25
+ totalReverts: number;
26
+ ewmaEditRate: number;
27
+ ewmaRevertRate: number;
28
+ lastWarningAt: number | null;
29
+ lastCriticalAt: number | null;
30
+ hardCapReached: boolean;
31
+ }
32
+ interface ImplicitFeedbackEntry {
33
+ type: string;
34
+ sessionId?: string;
35
+ at: string;
36
+ [key: string]: unknown;
37
+ }
38
+ export declare function loadInjectionCache(sessionId: string): InjectionCacheData | null;
39
+ export declare function loadDriftState(sessionId: string): DriftState | null;
40
+ export declare function loadImplicitFeedback(sessionId: string): ImplicitFeedbackEntry[];
41
+ export declare function loadSessionCorrections(sessionId: string): number;
42
+ /**
43
+ * Compute overall session quality score (0-100, higher = better).
44
+ *
45
+ * Formula:
46
+ * 100
47
+ * - (correctionRate × 15) // each correction/prompt penalizes 15pts
48
+ * - (driftScore × 0.3) // drift 0-100 maps to 0-30 penalty
49
+ * - (revertCount × 5) // each revert penalizes 5pts
50
+ * + (solutionEffectiveness × 20) // good solution usage boosts 0-20pts
51
+ */
52
+ export declare function computeOverallScore(correctionRate: number, driftScore: number, revertCount: number, solutionEffectiveness: number): number;
53
+ /**
54
+ * Score a session's quality by joining all available data sources.
55
+ * Returns null if insufficient data (no session state found).
56
+ */
57
+ export declare function scoreSession(sessionId: string): SessionQualityScore | null;
58
+ export declare function saveSessionQuality(score: SessionQualityScore, baseDir?: string): void;
59
+ export declare function loadSessionQuality(sessionId: string, baseDir?: string): SessionQualityScore | null;
60
+ export declare function loadRecentQualityScores(limit?: number, baseDir?: string): SessionQualityScore[];
61
+ export {};