@wooojin/forgen 0.3.0 → 0.3.2
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/.claude-plugin/plugin.json +7 -2
- package/CHANGELOG.md +132 -0
- package/README.ja.md +29 -0
- package/README.ko.md +29 -0
- package/README.md +36 -3
- package/README.zh.md +29 -0
- package/agents/solution-evolver.md +115 -0
- package/dist/cli.js +11 -3
- package/dist/core/auto-compound-runner.js +6 -3
- package/dist/core/dashboard.js +57 -4
- package/dist/core/doctor.d.ts +6 -1
- package/dist/core/doctor.js +21 -1
- package/dist/core/global-config.d.ts +2 -2
- package/dist/core/global-config.js +6 -14
- package/dist/core/harness.d.ts +3 -5
- package/dist/core/harness.js +34 -338
- package/dist/core/installer.d.ts +10 -0
- package/dist/core/installer.js +185 -0
- package/dist/core/paths.d.ts +25 -34
- package/dist/core/paths.js +25 -35
- package/dist/core/settings-injector.d.ts +13 -0
- package/dist/core/settings-injector.js +167 -0
- package/dist/core/settings-lock.d.ts +35 -2
- package/dist/core/settings-lock.js +65 -7
- package/dist/core/spawn.js +100 -39
- package/dist/core/state-gc.d.ts +30 -0
- package/dist/core/state-gc.js +119 -0
- package/dist/core/uninstall.js +12 -4
- package/dist/core/v1-bootstrap.js +2 -2
- package/dist/engine/compound-cli.d.ts +27 -2
- package/dist/engine/compound-cli.js +69 -16
- package/dist/engine/compound-export.d.ts +15 -0
- package/dist/engine/compound-export.js +32 -5
- package/dist/engine/compound-loop.js +3 -2
- package/dist/engine/learn-cli.d.ts +1 -0
- package/dist/engine/learn-cli.js +234 -0
- package/dist/engine/match-eval-log.js +45 -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 +8 -2
- package/dist/engine/solution-format.js +38 -27
- package/dist/engine/solution-index.js +10 -0
- package/dist/engine/solution-matcher.d.ts +8 -0
- package/dist/engine/solution-matcher.js +27 -1
- package/dist/engine/solution-outcomes.d.ts +74 -0
- package/dist/engine/solution-outcomes.js +319 -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 +9 -1
- package/dist/engine/solution-writer.js +44 -2
- package/dist/fgx.js +9 -2
- package/dist/forge/cli.js +7 -7
- package/dist/hooks/context-guard.js +15 -1
- package/dist/hooks/hook-config.d.ts +9 -1
- package/dist/hooks/hook-config.js +25 -3
- package/dist/hooks/internal/run-lifecycle-check.d.ts +2 -0
- package/dist/hooks/internal/run-lifecycle-check.js +32 -0
- package/dist/hooks/notepad-injector.js +6 -3
- package/dist/hooks/permission-handler.d.ts +10 -2
- package/dist/hooks/permission-handler.js +31 -12
- package/dist/hooks/post-tool-failure.js +7 -0
- package/dist/hooks/pre-tool-use.js +10 -4
- package/dist/hooks/secret-filter.js +6 -0
- package/dist/hooks/session-recovery.js +15 -7
- package/dist/hooks/shared/hook-response.d.ts +0 -2
- package/dist/hooks/shared/hook-response.js +3 -8
- package/dist/hooks/shared/hook-timing.js +10 -1
- package/dist/hooks/solution-injector.d.ts +21 -0
- package/dist/hooks/solution-injector.js +80 -1
- package/dist/mcp/solution-reader.d.ts +2 -0
- package/dist/mcp/solution-reader.js +28 -1
- package/dist/mcp/tools.js +13 -2
- package/dist/preset/preset-manager.js +12 -2
- package/dist/store/evidence-store.js +5 -5
- package/dist/store/profile-store.d.ts +9 -0
- package/dist/store/profile-store.js +25 -4
- package/dist/store/rule-store.js +8 -8
- package/package.json +1 -1
- package/plugin.json +7 -2
- package/scripts/postinstall.js +52 -5
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { DEFAULT_EVIDENCE } from './solution-format.js';
|
|
5
|
+
import { diagnoseFromRawContent } from './solution-quarantine.js';
|
|
6
|
+
import { createLogger } from '../core/logger.js';
|
|
7
|
+
const log = createLogger('solution-fixup');
|
|
8
|
+
/**
|
|
9
|
+
* Attempt to repair known-safe frontmatter defects.
|
|
10
|
+
*
|
|
11
|
+
* Handled defects (pre-0.3.1 schema drift, observed on 5 auto-extracted
|
|
12
|
+
* solutions from 2026-04-10):
|
|
13
|
+
* - `extractedBy` missing → add `extractedBy: auto`
|
|
14
|
+
* - `evidence` block missing → add `DEFAULT_EVIDENCE`
|
|
15
|
+
*
|
|
16
|
+
* All other validation errors (bad scope, non-numeric confidence, etc.)
|
|
17
|
+
* are surfaced in `remaining_errors` and the file is left untouched —
|
|
18
|
+
* those require human judgement, not a mechanical default.
|
|
19
|
+
*
|
|
20
|
+
* `dryRun: true` (default) reports what would change without writing.
|
|
21
|
+
*/
|
|
22
|
+
export function fixupSolutions(solutionsDir, opts = {}) {
|
|
23
|
+
const dryRun = opts.dryRun !== false;
|
|
24
|
+
const result = { scanned: 0, fixed: 0, untouched: 0, unfixable: 0, reports: [] };
|
|
25
|
+
if (!fs.existsSync(solutionsDir))
|
|
26
|
+
return result;
|
|
27
|
+
const files = fs.readdirSync(solutionsDir).filter((f) => f.endsWith('.md'));
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const filePath = path.join(solutionsDir, file);
|
|
30
|
+
result.scanned++;
|
|
31
|
+
let content;
|
|
32
|
+
try {
|
|
33
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
result.unfixable++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const errors = diagnoseFromRawContent(content);
|
|
40
|
+
if (errors.length === 0) {
|
|
41
|
+
result.untouched++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const fix = tryFix(content, errors);
|
|
45
|
+
result.reports.push({
|
|
46
|
+
path: filePath,
|
|
47
|
+
changed: fix.changed,
|
|
48
|
+
added: fix.added,
|
|
49
|
+
remaining_errors: fix.remaining,
|
|
50
|
+
});
|
|
51
|
+
if (fix.changed && fix.remaining.length === 0) {
|
|
52
|
+
if (!dryRun) {
|
|
53
|
+
try {
|
|
54
|
+
fs.writeFileSync(filePath, fix.content);
|
|
55
|
+
log.debug(`fixed: ${filePath} (${fix.added.join(', ')})`);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
log.debug(`write failed: ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
59
|
+
result.unfixable++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
result.fixed++;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
result.unfixable++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function tryFix(content, initialErrors) {
|
|
72
|
+
const trimmed = content.trimStart();
|
|
73
|
+
const added = [];
|
|
74
|
+
if (!trimmed.startsWith('---')) {
|
|
75
|
+
return { changed: false, added, remaining: initialErrors, content };
|
|
76
|
+
}
|
|
77
|
+
const endIdx = trimmed.indexOf('---', 3);
|
|
78
|
+
if (endIdx === -1) {
|
|
79
|
+
return { changed: false, added, remaining: initialErrors, content };
|
|
80
|
+
}
|
|
81
|
+
const leadingWs = content.slice(0, content.length - trimmed.length);
|
|
82
|
+
const fmRaw = trimmed.slice(3, endIdx);
|
|
83
|
+
const body = trimmed.slice(endIdx + 3);
|
|
84
|
+
let fm;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = yaml.load(fmRaw, { schema: yaml.JSON_SCHEMA });
|
|
87
|
+
if (parsed == null || typeof parsed !== 'object') {
|
|
88
|
+
return { changed: false, added, remaining: initialErrors, content };
|
|
89
|
+
}
|
|
90
|
+
fm = parsed;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { changed: false, added, remaining: initialErrors, content };
|
|
94
|
+
}
|
|
95
|
+
if (fm.extractedBy !== 'auto' && fm.extractedBy !== 'manual') {
|
|
96
|
+
fm.extractedBy = 'auto';
|
|
97
|
+
added.push('extractedBy: auto');
|
|
98
|
+
}
|
|
99
|
+
if (fm.evidence == null || typeof fm.evidence !== 'object') {
|
|
100
|
+
fm.evidence = { ...DEFAULT_EVIDENCE };
|
|
101
|
+
added.push('evidence: default');
|
|
102
|
+
}
|
|
103
|
+
if (fm.supersedes === undefined) {
|
|
104
|
+
fm.supersedes = null;
|
|
105
|
+
added.push('supersedes: null');
|
|
106
|
+
}
|
|
107
|
+
const newFmRaw = yaml.dump(fm, { lineWidth: 120, noRefs: true, sortKeys: false });
|
|
108
|
+
const rebuilt = `${leadingWs}---\n${newFmRaw}---${body}`;
|
|
109
|
+
const remaining = diagnoseFromRawContent(rebuilt);
|
|
110
|
+
return {
|
|
111
|
+
changed: added.length > 0,
|
|
112
|
+
added,
|
|
113
|
+
remaining,
|
|
114
|
+
content: rebuilt,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -60,14 +60,20 @@ export declare const DEFAULT_EVIDENCE: SolutionEvidence;
|
|
|
60
60
|
export declare function slugify(text: string): string;
|
|
61
61
|
/** Runtime type guard for SolutionFrontmatter */
|
|
62
62
|
export declare function validateFrontmatter(fm: unknown): fm is SolutionFrontmatter;
|
|
63
|
+
/**
|
|
64
|
+
* Return a list of validation errors for a parsed frontmatter object.
|
|
65
|
+
*
|
|
66
|
+
* Empty array = valid. Non-empty = each entry describes one missing/wrong
|
|
67
|
+
* field. Callers that only need a boolean should use `validateFrontmatter`.
|
|
68
|
+
* Slow path (quarantine logging) uses this to produce actionable diagnostics.
|
|
69
|
+
*/
|
|
70
|
+
export declare function diagnoseFrontmatter(fm: unknown): string[];
|
|
63
71
|
/** Parse YAML frontmatter from solution file content */
|
|
64
72
|
export declare function parseFrontmatterOnly(content: string): SolutionFrontmatter | null;
|
|
65
73
|
/** Parse a full V3 solution file into its components */
|
|
66
74
|
export declare function parseSolutionV3(content: string): SolutionV3 | null;
|
|
67
75
|
/** Serialize a SolutionV3 to a markdown string with YAML frontmatter */
|
|
68
76
|
export declare function serializeSolutionV3(solution: SolutionV3): string;
|
|
69
|
-
/** Check if content is in V3 format (YAML frontmatter) */
|
|
70
|
-
export declare function isV3Format(content: string): boolean;
|
|
71
77
|
/** Check if content is in V1 format (# Title + > Type: pattern) */
|
|
72
78
|
export declare function isV1Format(content: string): boolean;
|
|
73
79
|
/** 한국어 일반 조사/어미 — strip 대상 (긴 것부터 매칭)
|
|
@@ -35,43 +35,58 @@ export function slugify(text) {
|
|
|
35
35
|
// ── Validation ──
|
|
36
36
|
/** Runtime type guard for SolutionFrontmatter */
|
|
37
37
|
export function validateFrontmatter(fm) {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
return diagnoseFrontmatter(fm).length === 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Return a list of validation errors for a parsed frontmatter object.
|
|
42
|
+
*
|
|
43
|
+
* Empty array = valid. Non-empty = each entry describes one missing/wrong
|
|
44
|
+
* field. Callers that only need a boolean should use `validateFrontmatter`.
|
|
45
|
+
* Slow path (quarantine logging) uses this to produce actionable diagnostics.
|
|
46
|
+
*/
|
|
47
|
+
export function diagnoseFrontmatter(fm) {
|
|
48
|
+
const errors = [];
|
|
49
|
+
if (fm == null || typeof fm !== 'object') {
|
|
50
|
+
errors.push('frontmatter is not an object');
|
|
51
|
+
return errors;
|
|
52
|
+
}
|
|
40
53
|
const o = fm;
|
|
41
54
|
if (typeof o.name !== 'string')
|
|
42
|
-
|
|
55
|
+
errors.push('name: must be string');
|
|
43
56
|
if (typeof o.version !== 'number' || o.version <= 0)
|
|
44
|
-
|
|
57
|
+
errors.push('version: must be positive number');
|
|
45
58
|
if (typeof o.status !== 'string' || !VALID_STATUSES.includes(o.status))
|
|
46
|
-
|
|
59
|
+
errors.push(`status: must be one of ${VALID_STATUSES.join('|')}`);
|
|
47
60
|
if (typeof o.confidence !== 'number' || o.confidence < 0 || o.confidence > 1)
|
|
48
|
-
|
|
61
|
+
errors.push('confidence: must be number in [0,1]');
|
|
49
62
|
if (typeof o.type !== 'string' || !VALID_TYPES.includes(o.type))
|
|
50
|
-
|
|
63
|
+
errors.push(`type: must be one of ${VALID_TYPES.join('|')}`);
|
|
51
64
|
if (o.scope !== 'me' && o.scope !== 'team' && o.scope !== 'project' && o.scope !== 'universal')
|
|
52
|
-
|
|
65
|
+
errors.push('scope: must be me|team|project|universal');
|
|
53
66
|
if (!Array.isArray(o.tags) || !o.tags.every((t) => typeof t === 'string'))
|
|
54
|
-
|
|
67
|
+
errors.push('tags: must be string[]');
|
|
55
68
|
if (!Array.isArray(o.identifiers) || !o.identifiers.every((t) => typeof t === 'string'))
|
|
56
|
-
|
|
69
|
+
errors.push('identifiers: must be string[]');
|
|
57
70
|
if (typeof o.created !== 'string')
|
|
58
|
-
|
|
71
|
+
errors.push('created: must be string');
|
|
59
72
|
if (typeof o.updated !== 'string')
|
|
60
|
-
|
|
73
|
+
errors.push('updated: must be string');
|
|
61
74
|
if (o.supersedes !== null && typeof o.supersedes !== 'string')
|
|
62
|
-
|
|
75
|
+
errors.push('supersedes: must be string or null');
|
|
63
76
|
if (o.extractedBy !== 'auto' && o.extractedBy !== 'manual')
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
errors.push('extractedBy: missing or not auto|manual');
|
|
78
|
+
if (o.evidence == null || typeof o.evidence !== 'object') {
|
|
79
|
+
errors.push('evidence: block missing');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const ev = o.evidence;
|
|
83
|
+
const evFields = ['injected', 'reflected', 'negative', 'sessions', 'reExtracted'];
|
|
84
|
+
for (const f of evFields) {
|
|
85
|
+
if (typeof ev[f] !== 'number')
|
|
86
|
+
errors.push(`evidence.${f}: must be number`);
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
|
-
return
|
|
89
|
+
return errors;
|
|
75
90
|
}
|
|
76
91
|
// ── Parsing ──
|
|
77
92
|
/** Parse YAML frontmatter from solution file content */
|
|
@@ -147,10 +162,6 @@ export function serializeSolutionV3(solution) {
|
|
|
147
162
|
return `---\n${yamlStr}---\n\n## Context\n${solution.context}\n\n## Content\n${solution.content}\n`;
|
|
148
163
|
}
|
|
149
164
|
// ── Format Detection ──
|
|
150
|
-
/** Check if content is in V3 format (YAML frontmatter) */
|
|
151
|
-
export function isV3Format(content) {
|
|
152
|
-
return content.trimStart().startsWith('---');
|
|
153
|
-
}
|
|
154
165
|
/** Check if content is in V1 format (# Title + > Type: pattern) */
|
|
155
166
|
export function isV1Format(content) {
|
|
156
167
|
const lines = content.split('\n');
|
|
@@ -5,6 +5,7 @@ import { defaultNormalizer } from './term-normalizer.js';
|
|
|
5
5
|
import { withFileLockSync } from '../hooks/shared/file-lock.js';
|
|
6
6
|
import { atomicWriteText } from '../hooks/shared/atomic-write.js';
|
|
7
7
|
import { createLogger } from '../core/logger.js';
|
|
8
|
+
import { recordQuarantine, diagnoseFromRawContent } from './solution-quarantine.js';
|
|
8
9
|
const log = createLogger('solution-index');
|
|
9
10
|
/**
|
|
10
11
|
* Cache keyed by an order-preserving directory signature.
|
|
@@ -155,6 +156,15 @@ function buildIndex(dirs) {
|
|
|
155
156
|
const fm = parseFrontmatterOnly(content);
|
|
156
157
|
if (!fm) {
|
|
157
158
|
droppedMalformed++;
|
|
159
|
+
// Slow-path diagnosis: re-parse YAML to produce actionable errors,
|
|
160
|
+
// then persist to ~/.forgen/state/solution-quarantine.jsonl so the
|
|
161
|
+
// file is visible to `forgen doctor` instead of silently dead.
|
|
162
|
+
// Best-effort: quarantine writes must never throw.
|
|
163
|
+
try {
|
|
164
|
+
const errors = diagnoseFromRawContent(content);
|
|
165
|
+
recordQuarantine(filePath, errors);
|
|
166
|
+
}
|
|
167
|
+
catch { /* ignore */ }
|
|
158
168
|
log.debug(`dropped (malformed frontmatter): ${filePath}`);
|
|
159
169
|
continue;
|
|
160
170
|
}
|
|
@@ -41,6 +41,14 @@ export interface SolutionMatch {
|
|
|
41
41
|
tags: string[];
|
|
42
42
|
identifiers: string[];
|
|
43
43
|
matchedTags: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Identifier substrings (function/file names) that appeared literally in the
|
|
46
|
+
* prompt. Added 2026-04-21 so solution-injector can enforce a precision gate
|
|
47
|
+
* distinguishing "user typed a specific identifier" (strong signal, survives
|
|
48
|
+
* 1-tag overlap) from "only 1 tag happens to overlap" (often noise — common
|
|
49
|
+
* nouns like 'type', 'file', 'forgen' trigger rare-tag BM25 boost).
|
|
50
|
+
*/
|
|
51
|
+
matchedIdentifiers: string[];
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
46
54
|
* Optional hints for the v3 `calculateRelevance` path. Used by hot-path
|
|
@@ -810,6 +810,31 @@ function loadTunedMatcherWeights() {
|
|
|
810
810
|
_weightsCacheTime = now;
|
|
811
811
|
return undefined;
|
|
812
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Cold-start exploration bonus for candidate solutions.
|
|
815
|
+
*
|
|
816
|
+
* Phase 4 evolution: newly proposed solutions enter at `status: candidate`.
|
|
817
|
+
* Without a nudge they compete head-to-head with mature verified/champion
|
|
818
|
+
* entries and almost always lose the first few rounds — not because
|
|
819
|
+
* they're worse, but because matchers favor solutions with richer tag
|
|
820
|
+
* histories. A small confidence multiplier lets candidates surface often
|
|
821
|
+
* enough to accumulate reflected/sessions evidence, after which the
|
|
822
|
+
* lifecycle loop decides their fate.
|
|
823
|
+
*
|
|
824
|
+
* The 1.3× factor is a starting point (Q1 in docs/design-solution-evolution.md).
|
|
825
|
+
* Bonus deactivation happens implicitly when compound-lifecycle.ts::
|
|
826
|
+
* runLifecycleCheck promotes the candidate to `verified` based on accumulated
|
|
827
|
+
* reflected/sessions evidence. There is no inject-count-based auto promotion
|
|
828
|
+
* (removed 2026-04-20 — see feedback_core_loop_invariant).
|
|
829
|
+
*/
|
|
830
|
+
const CANDIDATE_EXPLORATION_MULTIPLIER = 1.3;
|
|
831
|
+
function applyCandidateExplorationBonus(entries) {
|
|
832
|
+
return entries.map((e) => {
|
|
833
|
+
if (e.status !== 'candidate')
|
|
834
|
+
return e;
|
|
835
|
+
return { ...e, confidence: Math.min(1, e.confidence * CANDIDATE_EXPLORATION_MULTIPLIER) };
|
|
836
|
+
});
|
|
837
|
+
}
|
|
813
838
|
export function matchSolutions(prompt, scope, cwd) {
|
|
814
839
|
// Build solution dirs for index cache
|
|
815
840
|
const dirs = [{ dir: ME_SOLUTIONS, scope: 'me' }];
|
|
@@ -819,7 +844,7 @@ export function matchSolutions(prompt, scope, cwd) {
|
|
|
819
844
|
dirs.push({ dir: path.join(cwd, '.compound', 'solutions'), scope: 'project' });
|
|
820
845
|
// Use cached index (rebuilt only when dirs change)
|
|
821
846
|
const index = getOrBuildIndex(dirs);
|
|
822
|
-
const allSolutions = index.entries.map((e) => ({ ...e }));
|
|
847
|
+
const allSolutions = applyCandidateExplorationBonus(index.entries.map((e) => ({ ...e })));
|
|
823
848
|
const promptTags = extractTags(prompt);
|
|
824
849
|
const promptLower = prompt.toLowerCase();
|
|
825
850
|
// Meta-learning: load tuned weights if available
|
|
@@ -842,5 +867,6 @@ export function matchSolutions(prompt, scope, cwd) {
|
|
|
842
867
|
tags: c.solution.tags,
|
|
843
868
|
identifiers: c.solution.identifiers,
|
|
844
869
|
matchedTags: [...c.matchedTags, ...c.matchedIdentifiers],
|
|
870
|
+
matchedIdentifiers: c.matchedIdentifiers,
|
|
845
871
|
}));
|
|
846
872
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type Outcome = 'accept' | 'correct' | 'error' | 'unknown';
|
|
2
|
+
export type Attribution = 'explicit' | 'window' | 'session_end' | 'default';
|
|
3
|
+
/**
|
|
4
|
+
* One inject → outcome event. Written append-only to
|
|
5
|
+
* ~/.forgen/state/outcomes/{session_id}.jsonl. The pending state (inject
|
|
6
|
+
* happened, outcome not yet decided) is stored separately in
|
|
7
|
+
* ~/.forgen/state/outcome-pending-{session_id}.json.
|
|
8
|
+
*/
|
|
9
|
+
export interface OutcomeEvent {
|
|
10
|
+
ts: number;
|
|
11
|
+
session_id: string;
|
|
12
|
+
solution: string;
|
|
13
|
+
match_score: number;
|
|
14
|
+
injected_chars: number;
|
|
15
|
+
outcome: Outcome;
|
|
16
|
+
outcome_lag_ms: number;
|
|
17
|
+
attribution: Attribution;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Record that solutions were injected. Called from solution-injector right
|
|
21
|
+
* after `approveWithContext` is emitted. Fails silently — outcome tracking
|
|
22
|
+
* must never block the user's workflow.
|
|
23
|
+
*/
|
|
24
|
+
export declare function appendPending(sessionId: string, injections: Array<{
|
|
25
|
+
solution: string;
|
|
26
|
+
match_score: number;
|
|
27
|
+
injected_chars: number;
|
|
28
|
+
}>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Flush pending injections as `accept` events. Called when a new user
|
|
31
|
+
* prompt arrives without any intervening correction/error, signaling that
|
|
32
|
+
* the previous injections were silently accepted. "Silence = consent."
|
|
33
|
+
*
|
|
34
|
+
* If `excludeSolutions` is provided, those solutions are NOT flushed (e.g.
|
|
35
|
+
* because an earlier step already attributed them as `correct` or `error`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function flushAccept(sessionId: string, excludeSolutions?: Set<string>): number;
|
|
38
|
+
/**
|
|
39
|
+
* Attribute a correction to the most recent pending injection(s). Called
|
|
40
|
+
* from the correction-record MCP tool. Removes attributed entries from
|
|
41
|
+
* pending so subsequent `flushAccept` does not double-count them.
|
|
42
|
+
*
|
|
43
|
+
* Strategy: all currently-pending solutions in this session are marked as
|
|
44
|
+
* `correct`. This is conservative (the correction may target only one of
|
|
45
|
+
* them), but without semantic attribution we err on the side of the user's
|
|
46
|
+
* feedback signal being louder than acceptance.
|
|
47
|
+
*/
|
|
48
|
+
export declare function attributeCorrection(sessionId: string): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Attribute a tool error to pending solutions in this session. Called from
|
|
51
|
+
* post-tool-failure hook. Unlike corrections, errors do not clear pending
|
|
52
|
+
* — an error is a weaker signal and the next user prompt can still produce
|
|
53
|
+
* a correct/accept decision.
|
|
54
|
+
*
|
|
55
|
+
* Only the top-K most-relevant, recent, above-threshold pending solutions
|
|
56
|
+
* are attributed (see gates above). Below-threshold or stale pending
|
|
57
|
+
* entries are left untouched — they will resolve via accept/unknown later.
|
|
58
|
+
*
|
|
59
|
+
* To avoid flooding the log with duplicate errors for the same pending
|
|
60
|
+
* batch, we cap at one `error` event per (session, solution) pair per
|
|
61
|
+
* pending-cycle by tracking a `error_flagged` set in the pending state.
|
|
62
|
+
*/
|
|
63
|
+
export declare function attributeError(sessionId: string): string[];
|
|
64
|
+
/**
|
|
65
|
+
* At session end, any still-pending entries are logged as `unknown` (we
|
|
66
|
+
* can't tell if the user was happy or just stopped). Pending file is
|
|
67
|
+
* removed.
|
|
68
|
+
*/
|
|
69
|
+
export declare function finalizeSession(sessionId: string): number;
|
|
70
|
+
/**
|
|
71
|
+
* Read all outcome events across all sessions. Used by fitness
|
|
72
|
+
* calculation. Returns events sorted by timestamp ascending.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readAllOutcomes(): OutcomeEvent[];
|