@wooojin/forgen 0.2.1 → 0.3.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 (124) hide show
  1. package/CHANGELOG.md +44 -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/test-engineer.md +58 -4
  15. package/agents/verifier.md +92 -77
  16. package/commands/architecture-decision.md +127 -258
  17. package/commands/calibrate.md +225 -0
  18. package/commands/code-review.md +163 -178
  19. package/commands/compound.md +127 -68
  20. package/commands/deep-interview.md +212 -110
  21. package/commands/docker.md +68 -178
  22. package/commands/forge-loop.md +215 -0
  23. package/commands/learn.md +231 -0
  24. package/commands/retro.md +215 -0
  25. package/commands/ship.md +277 -0
  26. package/dist/cli.js +17 -9
  27. package/dist/core/auto-compound-runner.js +14 -0
  28. package/dist/core/config-injector.d.ts +2 -1
  29. package/dist/core/config-injector.js +2 -1
  30. package/dist/core/dashboard.d.ts +17 -0
  31. package/dist/core/dashboard.js +112 -2
  32. package/dist/core/harness.d.ts +6 -1
  33. package/dist/core/harness.js +75 -19
  34. package/dist/core/paths.d.ts +6 -1
  35. package/dist/core/paths.js +18 -2
  36. package/dist/core/spawn.d.ts +3 -2
  37. package/dist/core/spawn.js +27 -8
  38. package/dist/core/types.d.ts +34 -0
  39. package/dist/engine/compound-lifecycle.d.ts +4 -3
  40. package/dist/engine/compound-lifecycle.js +91 -46
  41. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  42. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  43. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  44. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  45. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  46. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  47. package/dist/engine/meta-learning/runner.d.ts +14 -0
  48. package/dist/engine/meta-learning/runner.js +90 -0
  49. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  50. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  51. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  52. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  53. package/dist/engine/meta-learning/types.d.ts +114 -0
  54. package/dist/engine/meta-learning/types.js +43 -0
  55. package/dist/engine/solution-format.d.ts +2 -2
  56. package/dist/engine/solution-format.js +249 -34
  57. package/dist/engine/solution-index.d.ts +1 -1
  58. package/dist/engine/solution-matcher.d.ts +7 -1
  59. package/dist/engine/solution-matcher.js +114 -37
  60. package/dist/fgx.js +12 -8
  61. package/dist/hooks/context-guard.d.ts +5 -0
  62. package/dist/hooks/context-guard.js +118 -2
  63. package/dist/hooks/hooks-generator.d.ts +3 -0
  64. package/dist/hooks/hooks-generator.js +23 -6
  65. package/dist/hooks/keyword-detector.js +16 -100
  66. package/dist/hooks/skill-injector.d.ts +4 -3
  67. package/dist/hooks/skill-injector.js +6 -4
  68. package/dist/host/codex-adapter.d.ts +10 -0
  69. package/dist/host/codex-adapter.js +154 -0
  70. package/dist/mcp/solution-reader.d.ts +5 -5
  71. package/dist/mcp/solution-reader.js +34 -24
  72. package/dist/services/session.d.ts +19 -0
  73. package/dist/services/session.js +62 -0
  74. package/hooks/hooks.json +2 -2
  75. package/package.json +2 -1
  76. package/skills/architecture-decision/SKILL.md +113 -257
  77. package/skills/calibrate/SKILL.md +207 -0
  78. package/skills/code-review/SKILL.md +151 -178
  79. package/skills/compound/SKILL.md +126 -68
  80. package/skills/deep-interview/SKILL.md +210 -110
  81. package/skills/docker/SKILL.md +57 -179
  82. package/skills/forge-loop/SKILL.md +198 -0
  83. package/skills/learn/SKILL.md +216 -0
  84. package/skills/retro/SKILL.md +199 -0
  85. package/skills/ship/SKILL.md +259 -0
  86. package/agents/code-simplifier.md +0 -197
  87. package/agents/performance-reviewer.md +0 -172
  88. package/agents/qa-tester.md +0 -158
  89. package/agents/refactoring-expert.md +0 -168
  90. package/agents/scientist.md +0 -144
  91. package/agents/security-reviewer.md +0 -137
  92. package/agents/writer.md +0 -184
  93. package/commands/api-design.md +0 -268
  94. package/commands/ci-cd.md +0 -270
  95. package/commands/database.md +0 -263
  96. package/commands/debug-detective.md +0 -99
  97. package/commands/documentation.md +0 -276
  98. package/commands/ecomode.md +0 -51
  99. package/commands/frontend.md +0 -271
  100. package/commands/git-master.md +0 -90
  101. package/commands/incident-response.md +0 -292
  102. package/commands/migrate.md +0 -101
  103. package/commands/performance.md +0 -288
  104. package/commands/refactor.md +0 -105
  105. package/commands/security-review.md +0 -288
  106. package/commands/specify.md +0 -128
  107. package/commands/tdd.md +0 -183
  108. package/commands/testing-strategy.md +0 -265
  109. package/skills/api-design/SKILL.md +0 -262
  110. package/skills/ci-cd/SKILL.md +0 -264
  111. package/skills/database/SKILL.md +0 -257
  112. package/skills/debug-detective/SKILL.md +0 -95
  113. package/skills/documentation/SKILL.md +0 -270
  114. package/skills/ecomode/SKILL.md +0 -46
  115. package/skills/frontend/SKILL.md +0 -265
  116. package/skills/git-master/SKILL.md +0 -86
  117. package/skills/incident-response/SKILL.md +0 -286
  118. package/skills/migrate/SKILL.md +0 -96
  119. package/skills/performance/SKILL.md +0 -282
  120. package/skills/refactor/SKILL.md +0 -100
  121. package/skills/security-review/SKILL.md +0 -282
  122. package/skills/specify/SKILL.md +0 -122
  123. package/skills/tdd/SKILL.md +0 -178
  124. package/skills/testing-strategy/SKILL.md +0 -260
@@ -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;
@@ -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
+ }