Skip to content

Commit abbff59

Browse files
authored
Merge pull request #17 from Coding-Autopilot-System/harden-antigravity-integrations-20260623
Harden autonomy dashboard and Obsidian integration
2 parents fde69e0 + c8ca890 commit abbff59

28 files changed

Lines changed: 2703 additions & 65 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"_comment_usage": "Legacy example only. Prefer .universal-refiner.example.json and copy that file to .universal-refiner.json. This file is safe to commit.",
3+
"_comment_fields": "All fields are optional. Omit a section to use built-in defaults.",
4+
"semantic": {
5+
"_comment": "Configure the local OpenAI-compatible provider (Ollama, LM Studio, etc.)",
6+
"localEnabled": true,
7+
"baseUrl": "http://localhost:11434/v1",
8+
"models": ["gemma3:12b", "gemma3"],
9+
"mcpSamplingEnabled": true,
10+
"timeoutMs": 120000,
11+
"temperature": 0.2
12+
}
13+
}

universal-refiner/package-lock.json

Lines changed: 421 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

universal-refiner/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@
4545
"dependencies": {
4646
"@hono/node-server": "^1.19.13",
4747
"@modelcontextprotocol/sdk": "^1.29.0",
48+
"@lancedb/lancedb": "^0.30.0",
4849
"better-sqlite3": "^12.8.0",
4950
"chokidar": "^5.0.0",
50-
"flexsearch": "^0.7.43",
51+
"flexsearch": "^0.8.212",
5152
"hono": "^4.12.25",
5253
"typescript": "^5.9.3",
5354
"zod": "^4.3.6"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { EventStore } from "../src/history/event-store.js";
2+
import { ObsidianOrchestrator } from "../src/integrations/obsidian/obsidian-orchestrator.js";
3+
import { ConfigManager } from "../src/core/config.js";
4+
import * as path from "path";
5+
import * as fs from "fs";
6+
7+
/**
8+
* Historical Migration Script
9+
* Sweeps the EventStore (events.db) and pushes all old session data to the Obsidian Vault.
10+
*/
11+
async function migrate() {
12+
console.log("Starting Historical Migration to Obsidian...");
13+
14+
// Force the orchestrator to use the global obsidian vault
15+
ConfigManager.getObsidianConfig = () => ({ vaultPath: "C:\\repo\\global.obsidian" });
16+
17+
const store = EventStore.getInstance();
18+
const db = (store as any).db;
19+
20+
// 1. Get all unique projects (repo_ids)
21+
const repos = db.prepare("SELECT DISTINCT repo_id FROM prompts WHERE repo_id IS NOT NULL").all() as { repo_id: string }[];
22+
console.log(`Found ${repos.length} projects with history.`);
23+
24+
for (const { repo_id } of repos) {
25+
console.log(`\nMigrating project: ${repo_id}...`);
26+
27+
// Simulate a rootPath for the orchestrator (it uses basename(rootPath) as repoId)
28+
// We'll use a dummy path that ends with the repo_id
29+
const dummyRootPath = `C:\\repo\\${repo_id}`;
30+
31+
// 2. Fetch all successful executions for this repo
32+
const executions = db.prepare(`
33+
SELECT p.raw_prompt, e.result_summary, e.ended_at, e.executor_name
34+
FROM prompts p
35+
JOIN executions e ON p.id = e.prompt_id
36+
WHERE p.repo_id = ? AND e.status = 'completed'
37+
ORDER BY e.ended_at ASC
38+
`).all(repo_id) as any[];
39+
40+
console.log(`- Found ${executions.length} historical executions.`);
41+
42+
for (const exec of executions) {
43+
const summary = `Historical: ${exec.result_summary || "Agent execution"}`;
44+
const rationale = `Prompt: ${exec.raw_prompt}\n\nExecutor: ${exec.executor_name}\nDate: ${new Date(exec.ended_at).toLocaleString()}`;
45+
46+
await ObsidianOrchestrator.logActivity(dummyRootPath, summary, rationale);
47+
}
48+
49+
// 3. Fetch all commits for this repo
50+
const commits = db.prepare(`
51+
SELECT message, committed_at, author, sha
52+
FROM commits
53+
WHERE repo_id = ?
54+
ORDER BY committed_at ASC
55+
`).all(repo_id) as any[];
56+
57+
console.log(`- Found ${commits.length} historical commits.`);
58+
59+
for (const commit of commits) {
60+
const summary = `Historical Commit: ${commit.sha.substring(0, 7)} - ${commit.message}`;
61+
const rationale = `Author: ${commit.author}\nDate: ${new Date(commit.committed_at).toLocaleString()}`;
62+
63+
await ObsidianOrchestrator.logActivity(dummyRootPath, summary, rationale);
64+
}
65+
66+
// 4. Sync lessons (Engineering Mandates)
67+
await ObsidianOrchestrator.syncToWiki(dummyRootPath);
68+
}
69+
70+
console.log("\nMigration Complete!");
71+
process.exit(0);
72+
}
73+
74+
migrate().catch(err => {
75+
console.error("Migration failed:", err);
76+
process.exit(1);
77+
});

universal-refiner/src/core/background-service.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { AutoPilotStatus } from "./autopilot-status.js";
77
import { RuntimeLogger } from "./logger.js";
88
import { CommandCenterDashboard } from "./dashboard.js";
99
import { SerializedJobQueue } from "./job-queue.js";
10+
import { ExecutionOrchestrator } from "./execution-orchestrator.js";
11+
import { EventStore } from "../history/event-store.js";
12+
import { ConfigManager } from "./config.js";
1013

1114
export class BackgroundAutonomyService {
1215
private watcher: chokidar.FSWatcher | null = null;
@@ -100,14 +103,21 @@ export class BackgroundAutonomyService {
100103
const engine = new CorrelationEngine();
101104
const extractor = new LessonExtractor(this.requestModelText);
102105
const lessonsBefore = AutoPilotStatus.getSnapshot().stats.lessonsExtracted;
106+
const orchestrator = new ExecutionOrchestrator(EventStore.getInstance(), this.requestModelText);
107+
103108
const results = await Promise.allSettled([
104109
engine.correlateAll(),
105110
extractor.extractNewLessons(),
111+
extractor.extractFailureLessons(),
106112
]);
113+
107114
const rejected = results.find((result): result is PromiseRejectedResult => result.status === "rejected");
108115
if (rejected) {
109116
throw rejected.reason;
110117
}
118+
119+
await this.attemptSelfHealing(orchestrator);
120+
111121
const lessonsAfter = AutoPilotStatus.getSnapshot().stats.lessonsExtracted;
112122
if (lessonsAfter > lessonsBefore) {
113123
AutoPilotStatus.record(`Extracted ${lessonsAfter - lessonsBefore} lesson(s)`, "lesson");
@@ -116,7 +126,9 @@ export class BackgroundAutonomyService {
116126
AutoPilotStatus.incrementCycles();
117127
AutoPilotStatus.setActive();
118128
AutoPilotStatus.record("Cycle complete", "cycle_complete");
119-
CommandCenterDashboard.log("Background Autonomy: Correlation and lesson extraction complete.");
129+
CommandCenterDashboard.log("Background Autonomy: Correlation, lesson extraction, and self-healing complete.");
130+
131+
await this.syncObsidian();
120132

121133
} catch (error) {
122134
AutoPilotStatus.setIdle();
@@ -127,6 +139,36 @@ export class BackgroundAutonomyService {
127139
}
128140
}
129141

142+
private async attemptSelfHealing(orchestrator: ExecutionOrchestrator) {
143+
try {
144+
const lessons = EventStore.getInstance().getApprovedLessonsWithExecutions(10);
145+
146+
for (const lesson of lessons) {
147+
// Heal and retry
148+
await orchestrator.healAndRetry(lesson.execution_id, lesson.id);
149+
}
150+
} catch (error) {
151+
RuntimeLogger.error("Self-healing failed", error);
152+
}
153+
}
154+
155+
private async syncObsidian() {
156+
const obsidianConfig = ConfigManager.getObsidianConfig(this.rootPath);
157+
if (!obsidianConfig?.syncLessons) {
158+
return;
159+
}
160+
161+
try {
162+
const { ObsidianOrchestrator } = await import("../integrations/obsidian/obsidian-orchestrator.js");
163+
await ObsidianOrchestrator.syncToWiki(this.rootPath);
164+
CommandCenterDashboard.log("Background Autonomy: Synced to Obsidian Vault.");
165+
AutoPilotStatus.record("Synced to Obsidian Vault", "cycle_complete");
166+
} catch (syncError) {
167+
RuntimeLogger.error("Failed to sync to Obsidian", syncError);
168+
CommandCenterDashboard.log("Background Autonomy: Failed to sync to Obsidian Vault.");
169+
}
170+
}
171+
130172
public stop() {
131173
if (this.watcher) {
132174
this.watcher.close();

universal-refiner/src/core/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface RefinerConfig {
66
mandates?: string[];
77
ignoredPaths?: string[];
88
semantic?: Partial<SemanticConfig>;
9+
atlassian?: any;
10+
obsidian?: any;
911
}
1012

1113
export interface SemanticConfig {
@@ -88,6 +90,14 @@ export class ConfigManager {
8890
};
8991
}
9092

93+
static getAtlassianConfig(rootPath: string = "."): any | null {
94+
return this.loadConfig(rootPath).atlassian || null;
95+
}
96+
97+
static getObsidianConfig(rootPath: string = "."): any | null {
98+
return this.loadConfig(rootPath).obsidian || null;
99+
}
100+
91101
static getPredictiveMandates(): string[] {
92102
const logs = AgenticBlackboard.getLogs();
93103
const recent = logs.slice(0, 10).map(l => l.message.toLowerCase());

universal-refiner/src/core/dashboard.html

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ <h2>Provider Metrics</h2>
179179
const escapeHtml = (v) => String(v ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
180180
const encodeCandidateId = (v) => encodeURIComponent(String(v)).replace(/'/g, '%27');
181181
const projectName = (p) => p.split(/[\\\/]/).filter(Boolean).pop() || 'ROOT';
182+
const selectProject = (encodedProject) => {
183+
currentProject = decodeURIComponent(encodedProject);
184+
refreshData();
185+
};
182186

183187
function switchView(viewId) {
184188
document.querySelectorAll('.main-view').forEach(v => v.classList.add('hidden'));
@@ -199,13 +203,13 @@ <h2>Provider Metrics</h2>
199203

200204
document.getElementById('project-badge').textContent = projectName(currentProject).toUpperCase();
201205
document.getElementById('project-dna').innerHTML = `
202-
<div class="stat-row"><span class="stat-label">STACK</span><span>${state.stack}</span></div>
203-
<div class="stat-row"><span class="stat-label">FRAMEWORK</span><span>${state.framework}</span></div>
204-
<div class="stat-row"><span class="stat-label">PATTERN</span><span>${state.pattern}</span></div>
206+
<div class="stat-row"><span class="stat-label">STACK</span><span>${escapeHtml(state.stack)}</span></div>
207+
<div class="stat-row"><span class="stat-label">FRAMEWORK</span><span>${escapeHtml(state.framework)}</span></div>
208+
<div class="stat-row"><span class="stat-label">PATTERN</span><span>${escapeHtml(state.pattern)}</span></div>
205209
`;
206210

207211
document.getElementById('project-list').innerHTML = state.projects.map(p =>
208-
`<div class="nav-item ${p === currentProject ? 'active' : ''}" onclick="currentProject='${p}';refreshData();" style="font-size: 0.7rem; margin-left: 0.5rem;">📂 ${projectName(p)}</div>`
212+
`<div class="nav-item ${p === currentProject ? 'active' : ''}" onclick="selectProject('${encodeCandidateId(p)}')" style="font-size: 0.7rem; margin-left: 0.5rem;">📂 ${escapeHtml(projectName(p))}</div>`
209213
).join('');
210214

211215
// 2. View Specific Refresh
@@ -214,14 +218,46 @@ <h2>Provider Metrics</h2>
214218
const data = await res.json();
215219
document.getElementById('timeline-terminal').innerHTML = data.map(e => {
216220
let icon = 'EVT', color = 'var(--text)';
217-
if (e.type === 'prompt') { icon = 'PRM'; color = 'var(--accent)'; }
221+
let extraHtml = '';
222+
223+
if (e.type === 'prompt') {
224+
icon = 'PRM'; color = 'var(--accent)';
225+
if (e.details && e.details.intent === 'self-heal') {
226+
icon = 'HEAL'; color = '#10b981';
227+
extraHtml += `<div class="badge" style="background: rgba(16, 185, 129, 0.15); color: #6ee7b7; border-color: rgba(16, 185, 129, 0.3); margin-top: 8px; display: inline-block;">AUTONOMOUS SELF-HEALING RETRY</div>`;
228+
}
229+
if (e.details && e.details.normalized_prompt) {
230+
extraHtml += `<div style="margin-top: 8px; padding: 10px; background: rgba(56, 189, 248, 0.05); border-left: 2px solid var(--accent); border-radius: 0 4px 4px 0; font-size: 0.75rem; white-space: pre-wrap; color: var(--dim);"><strong>IMPROVED PROMPT:</strong><br/>${escapeHtml(e.details.normalized_prompt)}</div>`;
231+
}
232+
}
218233
else if (e.type === 'commit') { icon = 'GIT'; color = '#f472b6'; }
219234
else if (e.type === 'log') { icon = 'LOG'; color = 'var(--dim)'; }
235+
else if (e.type === 'execution') {
236+
icon = 'EXEC';
237+
color = e.event_type === 'failed' ? '#ef4444' : '#22c55e';
238+
let execBadge = e.event_type === 'failed'
239+
? `<span class="badge" style="background: rgba(239, 68, 68, 0.15); color: #fca5a5; border-color: rgba(239, 68, 68, 0.3);">AI ERROR</span>`
240+
: `<span class="badge" style="background: rgba(34, 197, 94, 0.15); color: #86efac; border-color: rgba(34, 197, 94, 0.3);">SUCCESS</span>`;
241+
242+
extraHtml = `<div style="margin-top: 8px;">
243+
${execBadge} <span style="color: var(--dim); font-size: 0.7rem; margin-left: 6px;">EXECUTOR: ${escapeHtml(e.author || 'unknown')}</span>
244+
</div>`;
245+
246+
if (e.event_type === 'failed' && e.details && e.details.error) {
247+
extraHtml += `<div style="margin-top: 8px; padding: 10px; background: rgba(239, 68, 68, 0.05); border-left: 2px solid #ef4444; border-radius: 0 4px 4px 0; font-size: 0.75rem; white-space: pre-wrap; color: #fca5a5; font-family: monospace;"><strong>RAW ERROR:</strong><br/>${escapeHtml(typeof e.details.error === 'object' ? JSON.stringify(e.details.error, null, 2) : String(e.details.error))}</div>`;
248+
} else if (e.event_type === 'completed' && e.details && e.details.healedResponse) {
249+
extraHtml += `<div style="margin-top: 8px; padding: 10px; background: rgba(34, 197, 94, 0.05); border-left: 2px solid #22c55e; border-radius: 0 4px 4px 0; font-size: 0.75rem; white-space: pre-wrap; color: #86efac;"><strong>HEALED RESPONSE:</strong><br/>${escapeHtml(e.details.healedResponse)}</div>`;
250+
}
251+
}
252+
220253
return `
221254
<div class="log-line">
222255
<span class="log-ts">${new Date(e.timestamp).toLocaleTimeString()}</span>
223256
<span class="log-icon" style="color:${color}">${icon}</span>
224-
<span>${escapeHtml(e.summary)}</span>
257+
<div style="flex-grow: 1;">
258+
<div style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem;">${escapeHtml(e.summary)}</div>
259+
${extraHtml}
260+
</div>
225261
</div>
226262
`;
227263
}).join('');
@@ -235,14 +271,14 @@ <h2>Provider Metrics</h2>
235271
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
236272
<div>
237273
<code style="color:var(--accent)">${c.sha.substring(0, 7)}</code> <strong>${escapeHtml(c.message)}</strong>
238-
<div style="font-size:0.7rem; color:var(--dim); margin-top:4px;">by ${c.author}${new Date(c.committed_at).toLocaleString()}</div>
274+
<div style="font-size:0.7rem; color:var(--dim); margin-top:4px;">by ${escapeHtml(c.author)}${new Date(c.committed_at).toLocaleString()}</div>
239275
</div>
240276
<div style="text-align:right">
241277
<span class="diff-add">+${JSON.parse(c.diff_stats_json).insertions || 0}</span>
242278
<span class="diff-del">-${JSON.parse(c.diff_stats_json).deletions || 0}</span>
243279
</div>
244280
</div>
245-
${c.prompt_id ? `<div class="badge" style="margin-top:10px; display:inline-block">Linked to Prompt: ${c.prompt_id}</div>` : ''}
281+
${c.prompt_id ? `<div class="badge" style="margin-top:10px; display:inline-block">Linked to Prompt: ${escapeHtml(c.prompt_id)}</div>` : ''}
246282
</div>
247283
`).join('') || '<p style="color:var(--dim)">No commits ingested for this project.</p>';
248284
}
@@ -328,9 +364,13 @@ <h2>Provider Metrics</h2>
328364
}
329365
}
330366

367+
// Auto-poll every 3 seconds so the user never has to click anything
368+
setInterval(() => {
369+
refreshData().catch(err => console.error("Auto-refresh failed:", err));
370+
}, 3000);
371+
331372
// Initial Load
332373
refreshData();
333-
setInterval(refreshData, 10000);
334374
</script>
335375
</body>
336376
</html>

universal-refiner/src/core/dashboard.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ import { AutoPilotStatus } from "./autopilot-status.js";
1717
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1818

1919
export function resolveDashboardHost(configuredHost = process.env.PROMPT_REFINER_DASHBOARD_HOST): string {
20-
return configuredHost?.trim() || "127.0.0.1";
20+
const host = configuredHost?.trim() || "127.0.0.1";
21+
if (isLoopbackHost(host) || process.env.PROMPT_REFINER_DASHBOARD_ALLOW_REMOTE === "true") {
22+
return host;
23+
}
24+
RuntimeLogger.warn("Ignoring non-loopback dashboard host without PROMPT_REFINER_DASHBOARD_ALLOW_REMOTE=true", { host });
25+
return "127.0.0.1";
2126
}
2227

2328
interface DashboardState {
@@ -60,7 +65,7 @@ function sanitizeEndpoint(rawUrl: string): string {
6065

6166
export function isSameOriginRequest(origin: string | undefined, requestUrl: string): boolean {
6267
if (!origin) {
63-
return true;
68+
return false;
6469
}
6570

6671
try {
@@ -70,6 +75,23 @@ export function isSameOriginRequest(origin: string | undefined, requestUrl: stri
7075
}
7176
}
7277

78+
export function isJsonContentType(contentType: string | undefined): boolean {
79+
const mediaType = contentType?.split(";")[0]?.trim().toLowerCase();
80+
return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
81+
}
82+
83+
function isLoopbackHost(host: string): boolean {
84+
const normalized = host.toLowerCase();
85+
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
86+
}
87+
88+
function redactSensitive(value: string): string {
89+
return value
90+
.replace(/(ghp_|github_pat_|sk-|xox[baprs]-)[a-z0-9_\-]+/gi, "$1[REDACTED]")
91+
.replace(/(token|api[_-]?key|password|secret|authorization)\s*[:=]\s*["']?[^"'\s,;]+/gi, "$1=[REDACTED]")
92+
.slice(0, 2_000);
93+
}
94+
7395
export class CommandCenterDashboard {
7496
private static rootPath: string = ".";
7597
private static server: { close: (callback?: (error?: Error) => void) => void } | null = null;
@@ -92,7 +114,7 @@ export class CommandCenterDashboard {
92114
}
93115

94116
private static logRouteError(routeName: string, error: unknown, selectedPath?: string) {
95-
const message = error instanceof Error ? error.stack || error.message : String(error);
117+
const message = redactSensitive(error instanceof Error ? error.stack || error.message : String(error));
96118
RuntimeLogger.error(`Dashboard route failed: ${routeName}`, {
97119
selectedPath: selectedPath || this.rootPath,
98120
error: message,
@@ -241,8 +263,10 @@ export class CommandCenterDashboard {
241263

242264
app.get("/api/timeline", async (c) => {
243265
try {
266+
const store = EventStore.getInstance();
267+
const repoId = store.ensureRepository(this.resolveSelectedPath(c.req.query("project"))).id;
244268
const provider = new TimelineProvider();
245-
const timeline = provider.getUnifiedTimeline(50);
269+
const timeline = provider.getUnifiedTimeline(50, repoId);
246270
return c.json(timeline);
247271
} catch (error) {
248272
this.logRouteError("api/timeline", error);
@@ -302,7 +326,7 @@ export class CommandCenterDashboard {
302326
if (!isSameOriginRequest(c.req.header("origin"), c.req.url)) {
303327
return c.json({ error: "Cross-origin review requests are not allowed" }, 403);
304328
}
305-
if (!c.req.header("content-type")?.toLowerCase().startsWith("application/json")) {
329+
if (!isJsonContentType(c.req.header("content-type"))) {
306330
return c.json({ error: "Review requests must use application/json" }, 415);
307331
}
308332

0 commit comments

Comments
 (0)