Skip to content

Commit d4f32de

Browse files
rootroot
authored andcommitted
feat(history): record evidence-backed terminal outcomes
1 parent 22acc22 commit d4f32de

7 files changed

Lines changed: 113 additions & 2 deletions

File tree

universal-refiner/src/core/blackboard.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ export class AgenticBlackboard {
3636
private static listeners: Array<() => void> = [];
3737

3838
private static getGlobalDir(): string {
39-
return process.env.PROMPT_REFINER_GLOBAL_DIR || path.join(os.homedir(), ".refiner");
39+
return process.env.PROMPT_REFINER_GLOBAL_DIR
40+
|| (process.env.PROMPT_REFINER_PROJECT_DIR
41+
? path.join(process.env.PROMPT_REFINER_PROJECT_DIR, ".global-refiner")
42+
: path.join(os.homedir(), ".refiner"));
4043
}
4144

4245
private static getGlobalLogPath(): string {
@@ -60,7 +63,10 @@ export class AgenticBlackboard {
6063
}
6164

6265
private static getStoragePath(rootPath: string): string {
63-
const projectRoot = this.findProjectRoot(rootPath);
66+
const effectiveRoot = rootPath === "."
67+
? process.env.PROMPT_REFINER_PROJECT_DIR || rootPath
68+
: rootPath;
69+
const projectRoot = this.findProjectRoot(effectiveRoot);
6470
return path.join(projectRoot, this.DOT_REFINER, this.STORAGE_NAME);
6571
}
6672

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const DEFAULT_DASHBOARD_PORT = 3000;
2+
3+
export function resolveDashboardPort(env: NodeJS.ProcessEnv = process.env): number {
4+
const raw = env.PROMPT_REFINER_DASHBOARD_PORT || env.PORT;
5+
if (!raw) return DEFAULT_DASHBOARD_PORT;
6+
const parsed = Number(raw);
7+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535) {
8+
throw new Error(`Invalid dashboard port: ${raw}`);
9+
}
10+
return parsed;
11+
}

universal-refiner/src/history/event-store.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,52 @@ export class EventStore {
181181
);
182182
}
183183

184+
recordTerminalOutcome(outcome: {
185+
goal_id: string;
186+
repo_id?: string;
187+
status: "completed" | "failed" | "cancelled" | "blocked" | "budget_exhausted";
188+
evidence: string[];
189+
summary: string;
190+
candidate?: {
191+
id: string;
192+
lesson_type: string;
193+
title: string;
194+
summary: string;
195+
confidence: string;
196+
};
197+
}): boolean {
198+
if (!outcome.goal_id.trim() || !outcome.summary.trim() || outcome.evidence.length === 0) {
199+
throw new Error("Terminal outcomes require goal id, summary, and evidence.");
200+
}
201+
const now = new Date().toISOString();
202+
const transaction = this.db.transaction(() => {
203+
const inserted = this.db.prepare(`
204+
INSERT OR IGNORE INTO terminal_outcomes (goal_id, repo_id, status, evidence_json, summary, created_at)
205+
VALUES (?, ?, ?, ?, ?, ?)
206+
`).run(outcome.goal_id, outcome.repo_id || null, outcome.status, JSON.stringify(outcome.evidence), outcome.summary, now);
207+
if (inserted.changes === 0) return false;
208+
if (outcome.candidate) {
209+
this.recordLesson({
210+
id: outcome.candidate.id,
211+
repo_id: outcome.repo_id,
212+
lesson_type: outcome.candidate.lesson_type,
213+
title: outcome.candidate.title,
214+
summary: outcome.candidate.summary,
215+
evidence_json: JSON.stringify(outcome.evidence),
216+
confidence: outcome.candidate.confidence,
217+
source: "terminal_outcome",
218+
approved: 0,
219+
});
220+
}
221+
return true;
222+
});
223+
return transaction();
224+
}
225+
226+
getTerminalOutcome(goalId: string): any | undefined {
227+
return this.db.prepare(`SELECT * FROM terminal_outcomes WHERE goal_id = ?`).get(goalId);
228+
}
229+
184230
linkCommitToExecution(executionId: string, commitId: string) {
185231
const stmt = this.db.prepare(`
186232
INSERT OR IGNORE INTO execution_commits (execution_id, commit_id)

universal-refiner/src/history/schema.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ CREATE TABLE IF NOT EXISTS executions (
5656
artifacts_json TEXT NOT NULL DEFAULT '{}'
5757
);
5858
59+
CREATE TABLE IF NOT EXISTS terminal_outcomes (
60+
goal_id TEXT PRIMARY KEY,
61+
repo_id TEXT,
62+
status TEXT NOT NULL,
63+
evidence_json TEXT NOT NULL,
64+
summary TEXT NOT NULL,
65+
created_at TEXT NOT NULL
66+
);
67+
5968
CREATE TABLE IF NOT EXISTS tests (
6069
id TEXT PRIMARY KEY,
6170
execution_id TEXT NOT NULL,

universal-refiner/tests/history.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,33 @@ describe("EventStore", () => {
120120
expect(store.getRecentLessons("repo").map(lesson => lesson.id)).toEqual(["approved"]);
121121
});
122122

123+
it("records one terminal outcome and gates its lesson candidate on approval", () => {
124+
const store = EventStore.getInstance();
125+
const outcome = {
126+
goal_id: "goal-1",
127+
repo_id: "repo",
128+
status: "completed" as const,
129+
evidence: ["cas://evidence/verification/1"],
130+
summary: "All mandatory checks passed.",
131+
candidate: {
132+
id: "lesson-goal-1",
133+
lesson_type: "quality",
134+
title: "Candidate lesson",
135+
summary: "Preserve deterministic verification.",
136+
confidence: "high",
137+
},
138+
};
139+
140+
expect(store.recordTerminalOutcome(outcome)).toBe(true);
141+
expect(store.recordTerminalOutcome(outcome)).toBe(false);
142+
expect(store.getTerminalOutcome("goal-1").status).toBe("completed");
143+
expect(store.getLearningCandidates("repo").lessons.map(lesson => lesson.id)).toEqual(["lesson-goal-1"]);
144+
expect(store.getRecentLessons("repo")).toEqual([]);
145+
146+
expect(store.reviewLesson("repo", "lesson-goal-1", true)).toBe(true);
147+
expect(store.getRecentLessons("repo").map(lesson => lesson.id)).toContain("lesson-goal-1");
148+
});
149+
123150
it("should persist learning candidate approval and rejection", () => {
124151
const store = EventStore.getInstance();
125152
store.recordLesson({

universal-refiner/tests/setup.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as fs from "fs";
2+
import * as os from "os";
3+
import * as path from "path";
4+
5+
const isolationRoot = fs.mkdtempSync(path.join(os.tmpdir(), "prompt-refiner-tests-"));
6+
const projectRoot = path.join(isolationRoot, "project");
7+
8+
fs.mkdirSync(path.join(projectRoot, ".refiner"), { recursive: true });
9+
process.env.PROMPT_REFINER_PROJECT_DIR = projectRoot;
10+
process.env.PROMPT_REFINER_GLOBAL_DIR = path.join(isolationRoot, "global");

universal-refiner/vitest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { defineConfig } from "vitest/config";
22

33
export default defineConfig({
44
test: {
5+
exclude: ["node_modules/**", "dist/**", "tests/e2e/**"],
6+
setupFiles: ["./tests/setup.ts"],
57
coverage: {
68
provider: "v8",
79
include: ["hooks/lib/**/*.ts", "src/**/*.ts"],

0 commit comments

Comments
 (0)