Skip to content

Commit c2cb19e

Browse files
committed
chore(sync): resolve merge conflicts
2 parents eb5b8a8 + 0d957cb commit c2cb19e

12 files changed

Lines changed: 196 additions & 33 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: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
export const OPERATOR_DASHBOARD_PORT = 3000;
1+
const DEFAULT_DASHBOARD_PORT = 3000;
22

3-
export function resolveDashboardPort(
4-
preferred = process.env.PROMPT_REFINER_DASHBOARD_PORT,
5-
legacy = process.env.PORT,
6-
): number {
7-
const configured = preferred || legacy;
8-
if (!configured) {
9-
return OPERATOR_DASHBOARD_PORT;
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}`);
109
}
11-
12-
const port = Number.parseInt(configured, 10);
13-
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
14-
return OPERATOR_DASHBOARD_PORT;
15-
}
16-
return port;
10+
return parsed;
1711
}

universal-refiner/src/core/server.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,20 @@ Output ONLY the JSON array. If no gaps, return [].`,
309309
description: "Lists review-gated lesson and prompt-template candidates for the current repository.",
310310
inputSchema: { type: "object", properties: {} }
311311
},
312+
{
313+
name: "record_terminal_outcome",
314+
description: "Records one evidence-backed terminal goal outcome and an optional review-gated lesson candidate.",
315+
inputSchema: {
316+
type: "object",
317+
properties: {
318+
goal_id: { type: "string" },
319+
status: { type: "string", enum: ["completed", "failed", "cancelled", "blocked", "budget_exhausted"] },
320+
evidence: { type: "array", items: { type: "string" }, minItems: 1 },
321+
summary: { type: "string" }
322+
},
323+
required: ["goal_id", "status", "evidence", "summary"]
324+
}
325+
},
312326
{
313327
name: "review_lesson",
314328
description: "Approves or rejects a proposed lesson before it can influence future prompts.",
@@ -552,6 +566,19 @@ Output ONLY the JSON array. If no gaps, return [].`,
552566
const candidates = this.eventStore.getLearningCandidates(this.repository.id);
553567
return { content: [{ type: "text", text: JSON.stringify(candidates) }] };
554568
}
569+
case "record_terminal_outcome": {
570+
const outcome = z.object({
571+
goal_id: z.string().min(1),
572+
status: z.enum(["completed", "failed", "cancelled", "blocked", "budget_exhausted"]),
573+
evidence: z.array(z.string().min(1)).min(1),
574+
summary: z.string().min(1),
575+
}).parse(request.params.arguments);
576+
const recorded = this.eventStore.recordTerminalOutcome({
577+
...outcome,
578+
repo_id: this.repository.id,
579+
});
580+
return { content: [{ type: "text", text: JSON.stringify({ recorded, goal_id: outcome.goal_id }) }] };
581+
}
555582
case "review_lesson": {
556583
const { id, approved } = z.object({ id: z.string(), approved: z.boolean() }).parse(request.params.arguments);
557584
const changed = this.eventStore.reviewLesson(this.repository.id, id, approved);

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,52 @@ export class EventStore {
212212
);
213213
}
214214

215+
recordTerminalOutcome(outcome: {
216+
goal_id: string;
217+
repo_id?: string;
218+
status: "completed" | "failed" | "cancelled" | "blocked" | "budget_exhausted";
219+
evidence: string[];
220+
summary: string;
221+
candidate?: {
222+
id: string;
223+
lesson_type: string;
224+
title: string;
225+
summary: string;
226+
confidence: string;
227+
};
228+
}): boolean {
229+
if (!outcome.goal_id.trim() || !outcome.summary.trim() || outcome.evidence.length === 0) {
230+
throw new Error("Terminal outcomes require goal id, summary, and evidence.");
231+
}
232+
const now = new Date().toISOString();
233+
const transaction = this.db.transaction(() => {
234+
const inserted = this.db.prepare(`
235+
INSERT OR IGNORE INTO terminal_outcomes (goal_id, repo_id, status, evidence_json, summary, created_at)
236+
VALUES (?, ?, ?, ?, ?, ?)
237+
`).run(outcome.goal_id, outcome.repo_id || null, outcome.status, JSON.stringify(outcome.evidence), outcome.summary, now);
238+
if (inserted.changes === 0) return false;
239+
if (outcome.candidate) {
240+
this.recordLesson({
241+
id: outcome.candidate.id,
242+
repo_id: outcome.repo_id,
243+
lesson_type: outcome.candidate.lesson_type,
244+
title: outcome.candidate.title,
245+
summary: outcome.candidate.summary,
246+
evidence_json: JSON.stringify(outcome.evidence),
247+
confidence: outcome.candidate.confidence,
248+
source: "terminal_outcome",
249+
approved: 0,
250+
});
251+
}
252+
return true;
253+
});
254+
return transaction();
255+
}
256+
257+
getTerminalOutcome(goalId: string): any | undefined {
258+
return this.db.prepare(`SELECT * FROM terminal_outcomes WHERE goal_id = ?`).get(goalId);
259+
}
260+
215261
linkCommitToExecution(executionId: string, commitId: string) {
216262
const stmt = this.db.prepare(`
217263
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/acceptance/mcp-tools.acceptance.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("MCP all-tool acceptance", () => {
4141
const names = listResponse.tools.map(tool => tool.name);
4242

4343
expect(new Set(names).size).toBe(names.length);
44-
expect(names).toHaveLength(19);
44+
expect(names).toHaveLength(20);
4545
for (const tool of listResponse.tools) {
4646
expect(tool.description.length).toBeGreaterThan(10);
4747
expect(tool.inputSchema.type).toBe("object");
@@ -142,6 +142,12 @@ describe("MCP all-tool acceptance", () => {
142142
discover_rules: {},
143143
approve_rule: { id: "missing-rule" },
144144
list_learning_candidates: {},
145+
record_terminal_outcome: {
146+
goal_id: "acceptance-goal",
147+
status: "completed",
148+
evidence: ["cas://evidence/acceptance"],
149+
summary: "Acceptance outcome",
150+
},
145151
review_lesson: { id: "pending-lesson", approved: true },
146152
review_template: { id: "pending-template", approved: true },
147153
ingest_pattern: { id: "pattern", category: "quality", description: "Verify changes." },
@@ -163,5 +169,5 @@ describe("MCP all-tool acceptance", () => {
163169
await expect(dispatch({ params: { name: tool.name, arguments: args[tool.name] } }))
164170
.resolves.toHaveProperty("content");
165171
}
166-
});
172+
}, 15_000);
167173
});

universal-refiner/tests/blackboard.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,32 @@ describe("AgenticBlackboard", () => {
192192
});
193193

194194
it("uses the home-directory global store when no override is configured", () => {
195+
const previousHome = process.env.HOME;
196+
const previousProfile = process.env.USERPROFILE;
195197
delete process.env.PROMPT_REFINER_GLOBAL_DIR;
198+
delete process.env.PROMPT_REFINER_PROJECT_DIR;
199+
process.env.HOME = tmpDir;
200+
process.env.USERPROFILE = tmpDir;
201+
try {
202+
const data = AgenticBlackboard.getGlobalData();
203+
expect(data).toHaveProperty("logs");
204+
expect(data).toHaveProperty("projects");
205+
expect(Array.isArray(AgenticBlackboard.getLogs("."))).toBe(true);
206+
} finally {
207+
if (previousHome === undefined) delete process.env.HOME;
208+
else process.env.HOME = previousHome;
209+
if (previousProfile === undefined) delete process.env.USERPROFILE;
210+
else process.env.USERPROFILE = previousProfile;
211+
}
212+
});
213+
214+
it("uses the isolated project fallback when only project storage is configured", () => {
215+
delete process.env.PROMPT_REFINER_GLOBAL_DIR;
216+
process.env.PROMPT_REFINER_PROJECT_DIR = tmpDir;
196217

197218
const data = AgenticBlackboard.getGlobalData();
198219

199-
expect(data).toHaveProperty("logs");
200-
expect(data).toHaveProperty("projects");
220+
expect(data).toEqual({ logs: [], projects: [] });
221+
expect(fs.existsSync(path.join(tmpDir, ".global-refiner", "global_history.json"))).toBe(true);
201222
});
202223
});

universal-refiner/tests/history.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,51 @@ 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+
expect(store.recordTerminalOutcome({
150+
goal_id: "goal-without-repo",
151+
status: "completed",
152+
evidence: ["cas://evidence/global"],
153+
summary: "Global terminal outcome",
154+
})).toBe(true);
155+
});
156+
157+
it("rejects terminal outcomes without required evidence", () => {
158+
const store = EventStore.getInstance();
159+
160+
expect(() => store.recordTerminalOutcome({
161+
goal_id: "goal-without-evidence",
162+
status: "failed",
163+
evidence: [],
164+
summary: "Missing evidence",
165+
})).toThrow("Terminal outcomes require goal id, summary, and evidence.");
166+
});
167+
123168
it("should persist learning candidate approval and rejection", () => {
124169
const store = EventStore.getInstance();
125170
store.recordLesson({
Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import { describe, expect, it } from "vitest";
2-
import { OPERATOR_DASHBOARD_PORT, resolveDashboardPort } from "../src/core/ports.js";
32

4-
describe("dashboard port policy", () => {
5-
it("uses the fixed operator dashboard port by default", () => {
6-
expect(OPERATOR_DASHBOARD_PORT).toBe(3000);
7-
expect(resolveDashboardPort(undefined, undefined)).toBe(3000);
8-
});
3+
import { resolveDashboardPort } from "../src/core/ports.js";
94

10-
it("prefers the named dashboard port over legacy PORT", () => {
11-
expect(resolveDashboardPort("3000", "4321")).toBe(3000);
5+
describe("resolveDashboardPort", () => {
6+
it("uses the dedicated port, generic fallback, and default in priority order", () => {
7+
expect(resolveDashboardPort({ PROMPT_REFINER_DASHBOARD_PORT: "4100", PORT: "4200" })).toBe(4100);
8+
expect(resolveDashboardPort({ PORT: "4200" })).toBe(4200);
9+
expect(resolveDashboardPort({})).toBe(3000);
1210
});
1311

14-
it("keeps legacy PORT as a compatibility fallback and rejects invalid values", () => {
15-
expect(resolveDashboardPort(undefined, "3999")).toBe(3999);
16-
expect(resolveDashboardPort("not-a-port", "3999")).toBe(3000);
17-
expect(resolveDashboardPort("70000", undefined)).toBe(3000);
12+
it.each(["0", "65536", "not-a-number", "1.5"])("rejects invalid port %s", (value) => {
13+
expect(() => resolveDashboardPort({ PROMPT_REFINER_DASHBOARD_PORT: value })).toThrow(
14+
`Invalid dashboard port: ${value}`,
15+
);
1816
});
1917
});

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");

0 commit comments

Comments
 (0)