@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.
- package/CHANGELOG.md +76 -0
- package/README.ko.md +25 -14
- package/README.md +61 -17
- package/agents/analyst.md +48 -4
- package/agents/architect.md +39 -4
- package/agents/code-reviewer.md +107 -77
- package/agents/critic.md +47 -4
- package/agents/debugger.md +46 -4
- package/agents/designer.md +40 -4
- package/agents/executor.md +112 -30
- package/agents/explore.md +45 -5
- package/agents/git-master.md +48 -4
- package/agents/planner.md +121 -18
- package/agents/solution-evolver.md +115 -0
- package/agents/test-engineer.md +58 -4
- package/agents/verifier.md +92 -77
- package/commands/architecture-decision.md +127 -258
- package/commands/calibrate.md +225 -0
- package/commands/code-review.md +163 -178
- package/commands/compound.md +127 -68
- package/commands/deep-interview.md +212 -110
- package/commands/docker.md +68 -178
- package/commands/forge-loop.md +215 -0
- package/commands/learn.md +231 -0
- package/commands/retro.md +215 -0
- package/commands/ship.md +277 -0
- package/dist/cli.js +25 -9
- package/dist/core/auto-compound-runner.js +14 -0
- package/dist/core/config-injector.d.ts +2 -1
- package/dist/core/config-injector.js +2 -1
- package/dist/core/dashboard.d.ts +17 -0
- package/dist/core/dashboard.js +158 -2
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/paths.d.ts +31 -1
- package/dist/core/paths.js +43 -2
- package/dist/core/spawn.d.ts +3 -2
- package/dist/core/spawn.js +27 -8
- package/dist/core/types.d.ts +34 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/learn-cli.d.ts +1 -0
- package/dist/engine/learn-cli.js +182 -0
- package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
- package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
- package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
- package/dist/engine/meta-learning/extraction-tuner.js +99 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
- package/dist/engine/meta-learning/runner.d.ts +14 -0
- package/dist/engine/meta-learning/runner.js +90 -0
- package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
- package/dist/engine/meta-learning/scope-promoter.js +84 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
- package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
- package/dist/engine/meta-learning/types.d.ts +114 -0
- package/dist/engine/meta-learning/types.js +43 -0
- package/dist/engine/solution-candidate.d.ts +30 -0
- package/dist/engine/solution-candidate.js +124 -0
- package/dist/engine/solution-fitness.d.ts +52 -0
- package/dist/engine/solution-fitness.js +95 -0
- package/dist/engine/solution-fixup.d.ts +30 -0
- package/dist/engine/solution-fixup.js +116 -0
- package/dist/engine/solution-format.d.ts +10 -2
- package/dist/engine/solution-format.js +287 -57
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-index.js +10 -0
- package/dist/engine/solution-matcher.d.ts +7 -1
- package/dist/engine/solution-matcher.js +137 -37
- package/dist/engine/solution-outcomes.d.ts +70 -0
- package/dist/engine/solution-outcomes.js +242 -0
- package/dist/engine/solution-quarantine.d.ts +36 -0
- package/dist/engine/solution-quarantine.js +172 -0
- package/dist/engine/solution-weakness.d.ts +45 -0
- package/dist/engine/solution-weakness.js +225 -0
- package/dist/engine/solution-writer.d.ts +5 -0
- package/dist/engine/solution-writer.js +18 -0
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +5 -0
- package/dist/hooks/context-guard.js +118 -2
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/keyword-detector.js +16 -100
- package/dist/hooks/post-tool-failure.js +7 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +6 -4
- package/dist/hooks/solution-injector.js +20 -0
- package/dist/host/codex-adapter.d.ts +10 -0
- package/dist/host/codex-adapter.js +154 -0
- package/dist/mcp/solution-reader.d.ts +5 -5
- package/dist/mcp/solution-reader.js +34 -24
- package/dist/mcp/tools.js +8 -0
- package/dist/services/session.d.ts +19 -0
- package/dist/services/session.js +62 -0
- package/hooks/hooks.json +2 -2
- package/package.json +2 -1
- package/skills/architecture-decision/SKILL.md +113 -257
- package/skills/calibrate/SKILL.md +207 -0
- package/skills/code-review/SKILL.md +151 -178
- package/skills/compound/SKILL.md +126 -68
- package/skills/deep-interview/SKILL.md +210 -110
- package/skills/docker/SKILL.md +57 -179
- package/skills/forge-loop/SKILL.md +198 -0
- package/skills/learn/SKILL.md +216 -0
- package/skills/retro/SKILL.md +199 -0
- package/skills/ship/SKILL.md +259 -0
- package/agents/code-simplifier.md +0 -197
- package/agents/performance-reviewer.md +0 -172
- package/agents/qa-tester.md +0 -158
- package/agents/refactoring-expert.md +0 -168
- package/agents/scientist.md +0 -144
- package/agents/security-reviewer.md +0 -137
- package/agents/writer.md +0 -184
- package/commands/api-design.md +0 -268
- package/commands/ci-cd.md +0 -270
- package/commands/database.md +0 -263
- package/commands/debug-detective.md +0 -99
- package/commands/documentation.md +0 -276
- package/commands/ecomode.md +0 -51
- package/commands/frontend.md +0 -271
- package/commands/git-master.md +0 -90
- package/commands/incident-response.md +0 -292
- package/commands/migrate.md +0 -101
- package/commands/performance.md +0 -288
- package/commands/refactor.md +0 -105
- package/commands/security-review.md +0 -288
- package/commands/specify.md +0 -128
- package/commands/tdd.md +0 -183
- package/commands/testing-strategy.md +0 -265
- package/skills/api-design/SKILL.md +0 -262
- package/skills/ci-cd/SKILL.md +0 -264
- package/skills/database/SKILL.md +0 -257
- package/skills/debug-detective/SKILL.md +0 -95
- package/skills/documentation/SKILL.md +0 -270
- package/skills/ecomode/SKILL.md +0 -46
- package/skills/frontend/SKILL.md +0 -265
- package/skills/git-master/SKILL.md +0 -86
- package/skills/incident-response/SKILL.md +0 -286
- package/skills/migrate/SKILL.md +0 -96
- package/skills/performance/SKILL.md +0 -282
- package/skills/refactor/SKILL.md +0 -100
- package/skills/security-review/SKILL.md +0 -282
- package/skills/specify/SKILL.md +0 -122
- package/skills/tdd/SKILL.md +0 -178
- 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 {
|
|
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,
|
|
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':
|
|
33
|
-
|
|
34
|
-
case '
|
|
35
|
-
|
|
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':
|
|
44
|
-
|
|
45
|
-
case '
|
|
46
|
-
|
|
47
|
-
case '
|
|
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
|
-
/**
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
case '
|
|
65
|
-
|
|
66
|
-
|
|
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',
|
|
100
|
-
'
|
|
101
|
-
'--include=*.
|
|
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',
|
|
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(
|
|
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, {
|
|
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.
|
|
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 {
|
|
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 {
|
|
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;
|