prism-mcp-server 7.2.0 → 7.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/README.md +83 -1
- package/dist/config.js +16 -0
- package/dist/darkfactory/clawInvocation.js +77 -0
- package/dist/darkfactory/runner.js +584 -0
- package/dist/darkfactory/safetyController.js +197 -0
- package/dist/darkfactory/schema.js +4 -0
- package/dist/dashboard/server.js +103 -0
- package/dist/dashboard/ui.js +118 -6
- package/dist/hivemindWatchdog.js +197 -4
- package/dist/lifecycle.js +9 -1
- package/dist/server.js +41 -3
- package/dist/storage/sqlite.js +88 -0
- package/dist/storage/supabase.js +79 -3
- package/dist/storage/supabaseMigrations.js +52 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/pipelineDefinitions.js +131 -0
- package/dist/tools/pipelineHandlers.js +214 -0
- package/dist/tools/sessionMemoryDefinitions.js +5 -3
- package/dist/verification/clawValidator.js +228 -0
- package/dist/verification/runner.js +479 -0
- package/dist/verification/schema.js +46 -0
- package/dist/verification/severityPolicy.js +94 -0
- package/package.json +10 -5
package/dist/hivemindWatchdog.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hivemind Watchdog (
|
|
2
|
+
* Hivemind Watchdog (v7.2) — Active Agent Health Monitoring
|
|
3
3
|
*
|
|
4
4
|
* Server-side health monitor for multi-agent coordination.
|
|
5
5
|
* Runs every WATCHDOG_INTERVAL_MS when PRISM_ENABLE_HIVEMIND=true.
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
* - Sweep is non-blocking: errors are caught and logged, never crash
|
|
23
23
|
*/
|
|
24
24
|
import { getStorage } from "./storage/index.js";
|
|
25
|
-
import { PRISM_USER_ID } from "./config.js";
|
|
25
|
+
import { PRISM_USER_ID, PRISM_VERIFICATION_HARNESS_ENABLED, PRISM_VERIFICATION_LAYERS, PRISM_VERIFICATION_DEFAULT_SEVERITY } from "./config.js";
|
|
26
|
+
import * as fs from "fs";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
import { VerificationRunner } from "./verification/runner.js";
|
|
29
|
+
import { TestSuiteSchema } from "./verification/schema.js";
|
|
30
|
+
import { validateWithClaw } from "./verification/clawValidator.js";
|
|
31
|
+
import { sessionSaveExperienceHandler } from "./tools/ledgerHandlers.js";
|
|
26
32
|
export const DEFAULT_WATCHDOG_CONFIG = {
|
|
27
33
|
intervalMs: 60_000,
|
|
28
34
|
staleThresholdMin: 5,
|
|
@@ -35,6 +41,11 @@ export const DEFAULT_WATCHDOG_CONFIG = {
|
|
|
35
41
|
* Only one alert per agent per status is kept until drained.
|
|
36
42
|
*/
|
|
37
43
|
const pendingAlerts = new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Deduplicates concurrent verification jobs per agent.
|
|
46
|
+
* Key format: project:user_id:role
|
|
47
|
+
*/
|
|
48
|
+
const inFlightVerifications = new Map();
|
|
38
49
|
/**
|
|
39
50
|
* Drain all pending alerts for a project.
|
|
40
51
|
* Called by server.ts in the CallToolRequestSchema handler
|
|
@@ -118,7 +129,8 @@ export async function runWatchdogSweep(cfg = DEFAULT_WATCHDOG_CONFIG) {
|
|
|
118
129
|
// ── State Transition: Heartbeat-based ──────────────────
|
|
119
130
|
let newStatus = null;
|
|
120
131
|
if (minutesSinceHeartbeat >= cfg.offlineThresholdMin) {
|
|
121
|
-
// OFFLINE → prune the agent
|
|
132
|
+
// OFFLINE → prune the agent and clean up assertion files
|
|
133
|
+
cleanupAssertionFiles(agent);
|
|
122
134
|
try {
|
|
123
135
|
await storage.deregisterAgent(agent.project, agent.user_id, agent.role);
|
|
124
136
|
queueAlert(agent, "OFFLINE", `No heartbeat for ${Math.floor(minutesSinceHeartbeat)}m — auto-pruned from registry.`);
|
|
@@ -161,6 +173,166 @@ export async function runWatchdogSweep(cfg = DEFAULT_WATCHDOG_CONFIG) {
|
|
|
161
173
|
}
|
|
162
174
|
}
|
|
163
175
|
}
|
|
176
|
+
// ── State Transition: Verification Phase (v7.2.0 Enhanced) ──
|
|
177
|
+
// ARCHITECTURE: Verification is fire-and-forget. The sweep transitions the agent
|
|
178
|
+
// to 'verifying' synchronously and spawns a detached async closure. This prevents
|
|
179
|
+
// long-running Claw/Runner calls (10-70s) from blocking heartbeat checks for
|
|
180
|
+
// other agents.
|
|
181
|
+
if (!newStatus && (currentStatus === "active" || currentStatus === "failed_validation")) {
|
|
182
|
+
// v7.2.0 FIX: Scope assertion file per project+role to prevent multi-agent collision
|
|
183
|
+
const scopedFile = path.join(".prism-mcp", `test_assertions_${agent.project}_${agent.role}.json`);
|
|
184
|
+
// Also check the legacy global path for backward compat
|
|
185
|
+
const legacyFile = "test_assertions.json";
|
|
186
|
+
const activeFile = fs.existsSync(scopedFile) ? scopedFile
|
|
187
|
+
: fs.existsSync(legacyFile) ? legacyFile
|
|
188
|
+
: null;
|
|
189
|
+
if (activeFile) {
|
|
190
|
+
const flightKey = `${agent.project}:${agent.user_id}:${agent.role}`;
|
|
191
|
+
// Skip if a verification is already in-flight for this agent
|
|
192
|
+
if (!inFlightVerifications.has(flightKey)) {
|
|
193
|
+
// Set verifying state synchronously (non-blocking for the sweep)
|
|
194
|
+
console.error(`[Watchdog] 🔬 Verifying agent "${agent.role}" on "${agent.project}"`);
|
|
195
|
+
newStatus = "verifying";
|
|
196
|
+
// Capture values for the async closure
|
|
197
|
+
const capturedAgent = { ...agent };
|
|
198
|
+
const capturedFile = activeFile;
|
|
199
|
+
const capturedFailCount = agent.loop_count || 0;
|
|
200
|
+
// Spawn detached verification — does NOT block the sweep
|
|
201
|
+
const verificationJob = (async () => {
|
|
202
|
+
try {
|
|
203
|
+
const assertionsContent = fs.readFileSync(capturedFile, "utf8");
|
|
204
|
+
const innerStorage = await getStorage();
|
|
205
|
+
// v7.2.0: Build verification config from env vars
|
|
206
|
+
const vConfig = {
|
|
207
|
+
enabled: PRISM_VERIFICATION_HARNESS_ENABLED,
|
|
208
|
+
layers: PRISM_VERIFICATION_LAYERS,
|
|
209
|
+
default_severity: PRISM_VERIFICATION_DEFAULT_SEVERITY,
|
|
210
|
+
};
|
|
211
|
+
// v7.2.0: Claw-as-Validator adversarial pre-check (fail-open)
|
|
212
|
+
if (PRISM_VERIFICATION_HARNESS_ENABLED) {
|
|
213
|
+
try {
|
|
214
|
+
const suite = TestSuiteSchema.parse(JSON.parse(assertionsContent));
|
|
215
|
+
const clawResult = await validateWithClaw({
|
|
216
|
+
suite,
|
|
217
|
+
project: capturedAgent.project,
|
|
218
|
+
files_changed: [],
|
|
219
|
+
change_summary: `Automated verification for ${capturedAgent.role}`,
|
|
220
|
+
}, async (prompt, cwd) => {
|
|
221
|
+
// @ts-ignore: Optional runtime dependency; handled by .catch()
|
|
222
|
+
const mod = await import("./tools/clawHandlers.js").catch(() => null);
|
|
223
|
+
if (!mod?.clawRunTaskHandler)
|
|
224
|
+
throw new Error("claw-agent not available");
|
|
225
|
+
return mod.clawRunTaskHandler({ prompt, cwd });
|
|
226
|
+
});
|
|
227
|
+
if (!clawResult.accepted) {
|
|
228
|
+
console.error(`[Watchdog] ⚠️ Claw validator flagged ${clawResult.issues.length} issues`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (clawErr) {
|
|
232
|
+
console.error(`[Watchdog] Claw validator skipped: ${clawErr instanceof Error ? clawErr.message : String(clawErr)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// v7.2.0: Use enhanced runner with layer filtering
|
|
236
|
+
const result = await VerificationRunner.runSuite(assertionsContent, PRISM_VERIFICATION_HARNESS_ENABLED
|
|
237
|
+
? { layers: PRISM_VERIFICATION_LAYERS, config: vConfig }
|
|
238
|
+
: undefined);
|
|
239
|
+
let resolvedStatus;
|
|
240
|
+
let resolvedLoopCount = capturedFailCount;
|
|
241
|
+
if (!result.passed) {
|
|
242
|
+
// Emit structured experience event
|
|
243
|
+
try {
|
|
244
|
+
await sessionSaveExperienceHandler({
|
|
245
|
+
project: capturedAgent.project,
|
|
246
|
+
event_type: "validation_result",
|
|
247
|
+
context: `Verification run for ${capturedAgent.role}`,
|
|
248
|
+
action: "automated_verification",
|
|
249
|
+
outcome: `${result.failed_count}/${result.total} failed — gate: ${result.severity_gate.action}`,
|
|
250
|
+
role: capturedAgent.role,
|
|
251
|
+
confidence_score: Math.round((result.passed_count / Math.max(result.total, 1)) * 100)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
console.error(`[Watchdog] Error saving failure experience: ${err}`);
|
|
256
|
+
}
|
|
257
|
+
// Severity gate enforcement
|
|
258
|
+
if (PRISM_VERIFICATION_HARNESS_ENABLED && result.severity_gate.action === "abort") {
|
|
259
|
+
resolvedStatus = "failed_validation";
|
|
260
|
+
resolvedLoopCount = capturedFailCount + 1;
|
|
261
|
+
queueAlert(capturedAgent, "FAILED_VALIDATION", `[ABORT] ${result.severity_gate.summary}`);
|
|
262
|
+
console.error(`[Watchdog] 🛑 ABORT gate triggered for "${capturedAgent.role}" — ${result.severity_gate.summary}`);
|
|
263
|
+
}
|
|
264
|
+
else if (PRISM_VERIFICATION_HARNESS_ENABLED && result.severity_gate.action === "block") {
|
|
265
|
+
resolvedStatus = "failed_validation";
|
|
266
|
+
resolvedLoopCount = capturedFailCount + 1;
|
|
267
|
+
queueAlert(capturedAgent, "FAILED_VALIDATION", `[BLOCKED] ${result.severity_gate.summary}`);
|
|
268
|
+
console.error(`[Watchdog] 🚫 Gate BLOCKED for "${capturedAgent.role}" — ${result.severity_gate.summary}`);
|
|
269
|
+
}
|
|
270
|
+
else if (capturedFailCount >= 3) {
|
|
271
|
+
resolvedStatus = "looping";
|
|
272
|
+
// FIX: Clean up orphaned assertion file on LOOPING
|
|
273
|
+
cleanupAssertionFiles(capturedAgent);
|
|
274
|
+
queueAlert(capturedAgent, "LOOPING", `Validation failed ${capturedFailCount} times. Bailing out.`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
resolvedStatus = "failed_validation";
|
|
278
|
+
resolvedLoopCount = capturedFailCount + 1;
|
|
279
|
+
const failSummary = result.assertion_results
|
|
280
|
+
.filter(a => !a.passed && !a.skipped)
|
|
281
|
+
.map(a => `[${a.layer}] ${a.description}: ${a.error}`)
|
|
282
|
+
.join(" | ");
|
|
283
|
+
queueAlert(capturedAgent, "FAILED_VALIDATION", `[Verification Failed] ${failSummary}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Passed! Clean up assertion file
|
|
288
|
+
cleanupAssertionFiles(capturedAgent);
|
|
289
|
+
resolvedStatus = "active";
|
|
290
|
+
resolvedLoopCount = 0;
|
|
291
|
+
queueAlert(capturedAgent, "SUCCESS", "All test assertions passed successfully.");
|
|
292
|
+
console.error(`[Watchdog] ✅ Verification PASSED for "${capturedAgent.role}" on "${capturedAgent.project}"`);
|
|
293
|
+
try {
|
|
294
|
+
await sessionSaveExperienceHandler({
|
|
295
|
+
project: capturedAgent.project,
|
|
296
|
+
event_type: "validation_result",
|
|
297
|
+
context: `Verification run for ${capturedAgent.role}`,
|
|
298
|
+
action: "automated_verification",
|
|
299
|
+
outcome: `Passed all ${result.total} assertions (${result.duration_ms}ms)`,
|
|
300
|
+
role: capturedAgent.role,
|
|
301
|
+
confidence_score: 100
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
console.error(`[Watchdog] Error saving success experience: ${err}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Persist final status
|
|
309
|
+
try {
|
|
310
|
+
await innerStorage.updateAgentStatus(capturedAgent.project, capturedAgent.user_id, capturedAgent.role, resolvedStatus, { loop_count: resolvedLoopCount });
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
console.error(`[Watchdog] Failed to update status after verification: ${err}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
// Verification script error — mark as failed_validation
|
|
318
|
+
try {
|
|
319
|
+
const innerStorage = await getStorage();
|
|
320
|
+
await innerStorage.updateAgentStatus(capturedAgent.project, capturedAgent.user_id, capturedAgent.role, "failed_validation", { loop_count: capturedFailCount + 1 });
|
|
321
|
+
}
|
|
322
|
+
catch { /* best-effort */ }
|
|
323
|
+
queueAlert(capturedAgent, "FAILED_VALIDATION", `[Verification Script Error] ${e.message}`);
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
inFlightVerifications.delete(flightKey);
|
|
327
|
+
}
|
|
328
|
+
})();
|
|
329
|
+
inFlightVerifications.set(flightKey, verificationJob);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (!newStatus && currentStatus === "verifying") {
|
|
334
|
+
// Agent is already verifying — don't re-trigger, just skip
|
|
335
|
+
}
|
|
164
336
|
// ── State Transition: LOOPING confirmation ─────────────
|
|
165
337
|
// Loop detection is primarily done in heartbeatAgent().
|
|
166
338
|
// The watchdog just confirms and queues alerts for it.
|
|
@@ -169,6 +341,8 @@ export async function runWatchdogSweep(cfg = DEFAULT_WATCHDOG_CONFIG) {
|
|
|
169
341
|
agent.loop_count >= cfg.loopThreshold &&
|
|
170
342
|
currentStatus !== "looping") {
|
|
171
343
|
newStatus = "looping";
|
|
344
|
+
// FIX: Clean up orphaned assertion files on LOOPING
|
|
345
|
+
cleanupAssertionFiles(agent);
|
|
172
346
|
queueAlert(agent, "LOOPING", `Same task repeated ${agent.loop_count} times — possible infinite loop.`);
|
|
173
347
|
console.error(`[Watchdog] 🔄 Agent "${agent.role}" on "${agent.project}" detected LOOPING ` +
|
|
174
348
|
`(task repeated ${agent.loop_count}x)`);
|
|
@@ -176,7 +350,7 @@ export async function runWatchdogSweep(cfg = DEFAULT_WATCHDOG_CONFIG) {
|
|
|
176
350
|
// ── Apply status update ────────────────────────────────
|
|
177
351
|
if (newStatus && newStatus !== currentStatus) {
|
|
178
352
|
try {
|
|
179
|
-
await storage.updateAgentStatus(agent.project, agent.user_id, agent.role, newStatus);
|
|
353
|
+
await storage.updateAgentStatus(agent.project, agent.user_id, agent.role, newStatus, { loop_count: agent.loop_count });
|
|
180
354
|
}
|
|
181
355
|
catch (err) {
|
|
182
356
|
console.error(`[Watchdog] Status update failed for ${agent.project}/${agent.role}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -204,3 +378,22 @@ function truncate(str, maxLen) {
|
|
|
204
378
|
return str;
|
|
205
379
|
return str.slice(0, maxLen - 3) + "...";
|
|
206
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Clean up assertion files for an agent (scoped + legacy).
|
|
383
|
+
* Prevents orphaned files from triggering phantom verifications on restart.
|
|
384
|
+
*/
|
|
385
|
+
function cleanupAssertionFiles(agent) {
|
|
386
|
+
const scopedFile = path.join(".prism-mcp", `test_assertions_${agent.project}_${agent.role}.json`);
|
|
387
|
+
const legacyFile = "test_assertions.json";
|
|
388
|
+
for (const filePath of [scopedFile, legacyFile]) {
|
|
389
|
+
try {
|
|
390
|
+
if (fs.existsSync(filePath)) {
|
|
391
|
+
fs.unlinkSync(filePath);
|
|
392
|
+
console.error(`[Watchdog] 🧹 Cleaned up assertion file: ${filePath}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
console.error(`[Watchdog] Failed to clean up ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
package/dist/lifecycle.js
CHANGED
|
@@ -157,7 +157,15 @@ export function registerShutdownHandlers() {
|
|
|
157
157
|
shuttingDown = true;
|
|
158
158
|
log(`Shutting down gracefully (${reason})...`);
|
|
159
159
|
try {
|
|
160
|
-
// 0.
|
|
160
|
+
// 0. Stop the Dark Factory background runner first (prevents new DB writes)
|
|
161
|
+
try {
|
|
162
|
+
const { stopDarkFactoryRunner } = await import("./darkfactory/runner.js");
|
|
163
|
+
stopDarkFactoryRunner();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Runner may not be initialized — safe to ignore
|
|
167
|
+
}
|
|
168
|
+
// 0.5 Await pending background tasks (max 5s timeout)
|
|
161
169
|
await BackgroundTaskRegistry.awaitAll(5000);
|
|
162
170
|
// 0.5. Flush OTel span buffer FIRST — before any DBs are closed.
|
|
163
171
|
// BatchSpanProcessor holds spans in memory (up to 5s). If we close
|
package/dist/server.js
CHANGED
|
@@ -58,9 +58,10 @@ ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequ
|
|
|
58
58
|
// Claude Desktop that the attached resource has changed.
|
|
59
59
|
// Without this, the paperclipped context becomes stale.
|
|
60
60
|
SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
61
|
-
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND, WATCHDOG_INTERVAL_MS, WATCHDOG_STALE_MIN, WATCHDOG_FROZEN_MIN, WATCHDOG_OFFLINE_MIN, WATCHDOG_LOOP_THRESHOLD, PRISM_SCHEDULER_ENABLED, PRISM_SCHEDULER_INTERVAL_MS, PRISM_SCHOLAR_ENABLED, PRISM_HDC_ENABLED, PRISM_TASK_ROUTER_ENABLED_ENV, } from "./config.js";
|
|
61
|
+
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND, WATCHDOG_INTERVAL_MS, WATCHDOG_STALE_MIN, WATCHDOG_FROZEN_MIN, WATCHDOG_OFFLINE_MIN, WATCHDOG_LOOP_THRESHOLD, PRISM_SCHEDULER_ENABLED, PRISM_SCHEDULER_INTERVAL_MS, PRISM_SCHOLAR_ENABLED, PRISM_HDC_ENABLED, PRISM_TASK_ROUTER_ENABLED_ENV, PRISM_DARK_FACTORY_ENABLED, } from "./config.js";
|
|
62
62
|
import { startWatchdog, drainAlerts } from "./hivemindWatchdog.js";
|
|
63
63
|
import { startScheduler, startScholarScheduler } from "./backgroundScheduler.js";
|
|
64
|
+
import { startDarkFactoryRunner } from "./darkfactory/runner.js";
|
|
64
65
|
import { getSyncBus } from "./sync/factory.js";
|
|
65
66
|
import { startDashboardServer } from "./dashboard/server.js";
|
|
66
67
|
import { acquireLock, registerShutdownHandlers } from "./lifecycle.js";
|
|
@@ -123,7 +124,9 @@ MAINTENANCE_VACUUM_TOOL, maintenanceVacuumHandler,
|
|
|
123
124
|
// ─── v3.0: Agent Hivemind tools ───
|
|
124
125
|
AGENT_REGISTRY_TOOLS, agentRegisterHandler, agentHeartbeatHandler, agentListTeamHandler,
|
|
125
126
|
// v7.1: Task Router
|
|
126
|
-
sessionTaskRouteHandler,
|
|
127
|
+
sessionTaskRouteHandler,
|
|
128
|
+
// v7.3: Dark Factory Pipeline tools
|
|
129
|
+
SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL, sessionStartPipelineHandler, sessionCheckPipelineStatusHandler, sessionAbortPipelineHandler, } from "./tools/index.js";
|
|
127
130
|
// ─── Dynamic Tool Registration ───────────────────────────────────
|
|
128
131
|
// Base tools: always available regardless of configuration
|
|
129
132
|
const BASE_TOOLS = [
|
|
@@ -282,6 +285,8 @@ export function createServer() {
|
|
|
282
285
|
...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
|
|
283
286
|
// v7.1: Task Router tool — only when PRISM_TASK_ROUTER_ENABLED=true
|
|
284
287
|
...(getSettingSync("task_router_enabled", String(PRISM_TASK_ROUTER_ENABLED_ENV)) === "true" ? [SESSION_TASK_ROUTE_TOOL] : []),
|
|
288
|
+
// v7.3: Dark Factory pipeline tools — only when PRISM_DARK_FACTORY_ENABLED=true
|
|
289
|
+
...(PRISM_DARK_FACTORY_ENABLED ? [SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL] : []),
|
|
285
290
|
];
|
|
286
291
|
const server = new Server({
|
|
287
292
|
name: SERVER_CONFIG.name,
|
|
@@ -811,6 +816,28 @@ export function createServer() {
|
|
|
811
816
|
throw new Error("Task router not enabled. Enable it in the dashboard or set PRISM_TASK_ROUTER_ENABLED=true.");
|
|
812
817
|
result = await sessionTaskRouteHandler(args);
|
|
813
818
|
break;
|
|
819
|
+
// ─── v7.3: Dark Factory Pipeline Tools ───
|
|
820
|
+
case "session_start_pipeline":
|
|
821
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
822
|
+
throw new Error("Session memory not configured.");
|
|
823
|
+
if (!PRISM_DARK_FACTORY_ENABLED)
|
|
824
|
+
throw new Error("Dark Factory not enabled. Set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
825
|
+
result = await sessionStartPipelineHandler(args);
|
|
826
|
+
break;
|
|
827
|
+
case "session_check_pipeline_status":
|
|
828
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
829
|
+
throw new Error("Session memory not configured.");
|
|
830
|
+
if (!PRISM_DARK_FACTORY_ENABLED)
|
|
831
|
+
throw new Error("Dark Factory not enabled. Set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
832
|
+
result = await sessionCheckPipelineStatusHandler(args);
|
|
833
|
+
break;
|
|
834
|
+
case "session_abort_pipeline":
|
|
835
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
836
|
+
throw new Error("Session memory not configured.");
|
|
837
|
+
if (!PRISM_DARK_FACTORY_ENABLED)
|
|
838
|
+
throw new Error("Dark Factory not enabled. Set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
839
|
+
result = await sessionAbortPipelineHandler(args);
|
|
840
|
+
break;
|
|
814
841
|
default:
|
|
815
842
|
result = {
|
|
816
843
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
@@ -892,7 +919,7 @@ export function createSandboxServer() {
|
|
|
892
919
|
});
|
|
893
920
|
// Register all tool listings unconditionally
|
|
894
921
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
895
|
-
tools: [...BASE_TOOLS, ...buildSessionMemoryTools([]), ...AGENT_REGISTRY_TOOLS, SESSION_TASK_ROUTE_TOOL],
|
|
922
|
+
tools: [...BASE_TOOLS, ...buildSessionMemoryTools([]), ...AGENT_REGISTRY_TOOLS, SESSION_TASK_ROUTE_TOOL, SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL],
|
|
896
923
|
}));
|
|
897
924
|
// Register prompts listing so scanners see resume_session
|
|
898
925
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
@@ -1146,6 +1173,17 @@ export async function startServer() {
|
|
|
1146
1173
|
console.error(`[WebScholar] Startup failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1147
1174
|
});
|
|
1148
1175
|
}
|
|
1176
|
+
// ─── v7.3: Dark Factory Background Runner ────────────────
|
|
1177
|
+
// Autonomous pipeline orchestration engine. Picks up RUNNING
|
|
1178
|
+
// pipelines and advances them through PLAN → EXECUTE → VERIFY
|
|
1179
|
+
// cycles. Non-blocking — uses setInterval to yield between ticks.
|
|
1180
|
+
if (PRISM_DARK_FACTORY_ENABLED && SESSION_MEMORY_ENABLED) {
|
|
1181
|
+
storageReady?.then(() => {
|
|
1182
|
+
startDarkFactoryRunner();
|
|
1183
|
+
}).catch(err => {
|
|
1184
|
+
console.error(`[DarkFactory] Startup failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1149
1187
|
// Keep the process alive — without this, Node.js would exit
|
|
1150
1188
|
// because there are no active event loop handles after the
|
|
1151
1189
|
// synchronous setup completes.
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -24,6 +24,7 @@ import { AccessLogBuffer } from "../utils/accessLogBuffer.js";
|
|
|
24
24
|
import { PRISM_ACTR_BUFFER_FLUSH_MS } from "../config.js";
|
|
25
25
|
import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
|
|
26
26
|
import { debugLog } from "../utils/logger.js";
|
|
27
|
+
import { SafetyController } from "../darkfactory/safetyController.js";
|
|
27
28
|
export class SqliteStorage {
|
|
28
29
|
db;
|
|
29
30
|
dbPath;
|
|
@@ -556,6 +557,23 @@ export class SqliteStorage {
|
|
|
556
557
|
ON memory_access_log(entry_id, accessed_at DESC)`);
|
|
557
558
|
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_access_log_time
|
|
558
559
|
ON memory_access_log(accessed_at)`);
|
|
560
|
+
// ─── v7.3 Migration: Dark Factory Pipelines ───────────────
|
|
561
|
+
await this.db.execute(`
|
|
562
|
+
CREATE TABLE IF NOT EXISTS dark_factory_pipelines (
|
|
563
|
+
id TEXT PRIMARY KEY,
|
|
564
|
+
project TEXT NOT NULL,
|
|
565
|
+
user_id TEXT NOT NULL DEFAULT 'default',
|
|
566
|
+
status TEXT NOT NULL,
|
|
567
|
+
current_step TEXT NOT NULL,
|
|
568
|
+
iteration INTEGER NOT NULL,
|
|
569
|
+
started_at TEXT NOT NULL,
|
|
570
|
+
updated_at TEXT NOT NULL,
|
|
571
|
+
spec TEXT NOT NULL,
|
|
572
|
+
error TEXT,
|
|
573
|
+
last_heartbeat TEXT
|
|
574
|
+
)
|
|
575
|
+
`);
|
|
576
|
+
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_pipelines_status ON dark_factory_pipelines(user_id, project, status)`);
|
|
559
577
|
// ─── v6.1 Migration: Integrity Check ──────────────────────
|
|
560
578
|
//
|
|
561
579
|
// REVIEWER NOTE: PRAGMA integrity_check scans the B-tree structure of
|
|
@@ -2786,4 +2804,74 @@ export class SqliteStorage {
|
|
|
2786
2804
|
debugLog(`[SqliteStorage] pruneAccessLog: removed ${pruned} entries older than ${olderThanDays} days`);
|
|
2787
2805
|
return pruned;
|
|
2788
2806
|
}
|
|
2807
|
+
// ─── Dark Factory (v7.3) ───────────────────────────────────
|
|
2808
|
+
async savePipeline(state) {
|
|
2809
|
+
const now = new Date().toISOString();
|
|
2810
|
+
const updatedState = { ...state, updated_at: now };
|
|
2811
|
+
// Status Guard: prevent overwriting a terminated pipeline
|
|
2812
|
+
const existing = await this.getPipeline(state.id, state.user_id);
|
|
2813
|
+
if (existing) {
|
|
2814
|
+
if (existing.status === 'ABORTED' || existing.status === 'COMPLETED') {
|
|
2815
|
+
throw new Error(`Cannot update pipeline ${state.id} because it is already ${existing.status}.`);
|
|
2816
|
+
}
|
|
2817
|
+
// Validate state machine transition
|
|
2818
|
+
if (!SafetyController.validateTransition(existing.status, updatedState.status)) {
|
|
2819
|
+
throw new Error(`Illegal pipeline transition: ${existing.status} → ${updatedState.status} ` +
|
|
2820
|
+
`for pipeline ${state.id}. Legal transitions from ${existing.status}: ` +
|
|
2821
|
+
`${SafetyController.getLegalTransitions(existing.status).join(', ') || 'NONE (terminal)'}.`);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
await this.db.execute({
|
|
2825
|
+
sql: `
|
|
2826
|
+
INSERT INTO dark_factory_pipelines (id, project, user_id, status, current_step, iteration, started_at, updated_at, spec, error, last_heartbeat)
|
|
2827
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2828
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2829
|
+
status = excluded.status,
|
|
2830
|
+
current_step = excluded.current_step,
|
|
2831
|
+
iteration = excluded.iteration,
|
|
2832
|
+
updated_at = excluded.updated_at,
|
|
2833
|
+
spec = excluded.spec,
|
|
2834
|
+
error = excluded.error,
|
|
2835
|
+
last_heartbeat = excluded.last_heartbeat
|
|
2836
|
+
`,
|
|
2837
|
+
args: [
|
|
2838
|
+
updatedState.id,
|
|
2839
|
+
updatedState.project,
|
|
2840
|
+
updatedState.user_id,
|
|
2841
|
+
updatedState.status,
|
|
2842
|
+
updatedState.current_step,
|
|
2843
|
+
updatedState.iteration,
|
|
2844
|
+
updatedState.started_at,
|
|
2845
|
+
updatedState.updated_at,
|
|
2846
|
+
updatedState.spec,
|
|
2847
|
+
updatedState.error || null,
|
|
2848
|
+
updatedState.last_heartbeat || null
|
|
2849
|
+
]
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
async getPipeline(id, userId) {
|
|
2853
|
+
const result = await this.db.execute({
|
|
2854
|
+
sql: `SELECT * FROM dark_factory_pipelines WHERE id = ? AND user_id = ?`,
|
|
2855
|
+
args: [id, userId]
|
|
2856
|
+
});
|
|
2857
|
+
if (result.rows.length === 0)
|
|
2858
|
+
return null;
|
|
2859
|
+
return result.rows[0];
|
|
2860
|
+
}
|
|
2861
|
+
async listPipelines(project, status, userId) {
|
|
2862
|
+
const conditions = ['user_id = ?'];
|
|
2863
|
+
const args = [userId];
|
|
2864
|
+
if (project) {
|
|
2865
|
+
conditions.push('project = ?');
|
|
2866
|
+
args.push(project);
|
|
2867
|
+
}
|
|
2868
|
+
if (status) {
|
|
2869
|
+
conditions.push('status = ?');
|
|
2870
|
+
args.push(status);
|
|
2871
|
+
}
|
|
2872
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
2873
|
+
const sql = `SELECT * FROM dark_factory_pipelines ${where} ORDER BY updated_at DESC`;
|
|
2874
|
+
const result = await this.db.execute({ sql, args });
|
|
2875
|
+
return result.rows;
|
|
2876
|
+
}
|
|
2789
2877
|
}
|
package/dist/storage/supabase.js
CHANGED
|
@@ -18,6 +18,7 @@ import { debugLog } from "../utils/logger.js";
|
|
|
18
18
|
import { PRISM_USER_ID } from "../config.js";
|
|
19
19
|
import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
|
|
20
20
|
import { runAutoMigrations } from "./supabaseMigrations.js";
|
|
21
|
+
import { SafetyController } from "../darkfactory/safetyController.js";
|
|
21
22
|
export class SupabaseStorage {
|
|
22
23
|
// ─── Lifecycle ─────────────────────────────────────────────
|
|
23
24
|
async initialize() {
|
|
@@ -1182,9 +1183,6 @@ export class SupabaseStorage {
|
|
|
1182
1183
|
return Number(first.prism_prune_access_log) || 0;
|
|
1183
1184
|
}
|
|
1184
1185
|
}
|
|
1185
|
-
if (rpcResult && typeof rpcResult.deleted_count !== "undefined") {
|
|
1186
|
-
return Number(rpcResult.deleted_count) || 0;
|
|
1187
|
-
}
|
|
1188
1186
|
if (rpcResult && typeof rpcResult.prism_prune_access_log !== "undefined") {
|
|
1189
1187
|
return Number(rpcResult.prism_prune_access_log) || 0;
|
|
1190
1188
|
}
|
|
@@ -1195,4 +1193,82 @@ export class SupabaseStorage {
|
|
|
1195
1193
|
return 0;
|
|
1196
1194
|
}
|
|
1197
1195
|
}
|
|
1196
|
+
// ─── Dark Factory (v7.3) ───────────────────────────────────
|
|
1197
|
+
async savePipeline(state) {
|
|
1198
|
+
const now = new Date().toISOString();
|
|
1199
|
+
const updatedState = { ...state, updated_at: now };
|
|
1200
|
+
// Status Guard: prevent overwriting a terminated pipeline
|
|
1201
|
+
const existing = await this.getPipeline(state.id, state.user_id);
|
|
1202
|
+
if (existing) {
|
|
1203
|
+
if (existing.status === 'ABORTED' || existing.status === 'COMPLETED') {
|
|
1204
|
+
throw new Error(`Cannot update pipeline ${state.id} because it is already ${existing.status}.`);
|
|
1205
|
+
}
|
|
1206
|
+
// Validate state machine transition
|
|
1207
|
+
if (!SafetyController.validateTransition(existing.status, updatedState.status)) {
|
|
1208
|
+
throw new Error(`Illegal pipeline transition: ${existing.status} → ${updatedState.status} ` +
|
|
1209
|
+
`for pipeline ${state.id}. Legal transitions from ${existing.status}: ` +
|
|
1210
|
+
`${SafetyController.getLegalTransitions(existing.status).join(', ') || 'NONE (terminal)'}.`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
await supabasePost("dark_factory_pipelines", {
|
|
1215
|
+
id: updatedState.id,
|
|
1216
|
+
project: updatedState.project,
|
|
1217
|
+
user_id: updatedState.user_id,
|
|
1218
|
+
status: updatedState.status,
|
|
1219
|
+
current_step: updatedState.current_step,
|
|
1220
|
+
iteration: updatedState.iteration,
|
|
1221
|
+
started_at: updatedState.started_at,
|
|
1222
|
+
updated_at: updatedState.updated_at,
|
|
1223
|
+
spec: updatedState.spec,
|
|
1224
|
+
error: updatedState.error || null,
|
|
1225
|
+
last_heartbeat: updatedState.last_heartbeat || null
|
|
1226
|
+
}, { on_conflict: "id" }, { Prefer: "return=minimal,resolution=merge-duplicates" });
|
|
1227
|
+
}
|
|
1228
|
+
catch (e) {
|
|
1229
|
+
// PGRST202 fallback if the table doesn't exist yet
|
|
1230
|
+
if (e.message?.includes("PGRST202") || e.message?.includes("Could not find the relation")) {
|
|
1231
|
+
debugLog("[SupabaseStorage] dark_factory_pipelines missing — please run migration 038");
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
throw e;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
async getPipeline(id, userId) {
|
|
1238
|
+
try {
|
|
1239
|
+
const result = await supabaseGet("dark_factory_pipelines", {
|
|
1240
|
+
id: `eq.${id}`,
|
|
1241
|
+
user_id: `eq.${userId}`,
|
|
1242
|
+
limit: "1"
|
|
1243
|
+
});
|
|
1244
|
+
const rows = Array.isArray(result) ? result : [];
|
|
1245
|
+
if (rows.length === 0)
|
|
1246
|
+
return null;
|
|
1247
|
+
return rows[0];
|
|
1248
|
+
}
|
|
1249
|
+
catch (e) {
|
|
1250
|
+
if (e.message?.includes("PGRST202") || e.message?.includes("Could not find the relation"))
|
|
1251
|
+
return null;
|
|
1252
|
+
throw e;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async listPipelines(project, status, userId) {
|
|
1256
|
+
try {
|
|
1257
|
+
const query = {
|
|
1258
|
+
user_id: `eq.${userId}`,
|
|
1259
|
+
order: "updated_at.desc"
|
|
1260
|
+
};
|
|
1261
|
+
if (project)
|
|
1262
|
+
query.project = `eq.${project}`;
|
|
1263
|
+
if (status)
|
|
1264
|
+
query.status = `eq.${status}`;
|
|
1265
|
+
const result = await supabaseGet("dark_factory_pipelines", query);
|
|
1266
|
+
return (Array.isArray(result) ? result : []);
|
|
1267
|
+
}
|
|
1268
|
+
catch (e) {
|
|
1269
|
+
if (e.message?.includes("PGRST202") || e.message?.includes("Could not find the relation"))
|
|
1270
|
+
return [];
|
|
1271
|
+
throw e;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1198
1274
|
}
|
|
@@ -721,6 +721,58 @@ export const MIGRATIONS = [
|
|
|
721
721
|
GRANT EXECUTE ON FUNCTION public.prism_seed_access_log_on_ledger_insert() TO service_role, authenticated;
|
|
722
722
|
`
|
|
723
723
|
},
|
|
724
|
+
{
|
|
725
|
+
// ─── v7.3: Dark Factory Pipelines ─────────────────────────────
|
|
726
|
+
//
|
|
727
|
+
// Creates the dark_factory_pipelines table for autonomous Plan-Execute-Verify
|
|
728
|
+
// pipeline orchestration. Includes status CHECK constraint for the canonical set.
|
|
729
|
+
//
|
|
730
|
+
// EXISTING DEPLOYMENT GUARD: If the table already exists (e.g., from running
|
|
731
|
+
// 038_dark_factory_pipelines.sql directly), CREATE TABLE IF NOT EXISTS is a no-op.
|
|
732
|
+
// We then ALTER TABLE to add the CHECK constraint for existing deployments.
|
|
733
|
+
version: 38,
|
|
734
|
+
name: "dark_factory_pipelines",
|
|
735
|
+
sql: `
|
|
736
|
+
-- Create the table if fresh install
|
|
737
|
+
CREATE TABLE IF NOT EXISTS public.dark_factory_pipelines (
|
|
738
|
+
id TEXT PRIMARY KEY,
|
|
739
|
+
project TEXT NOT NULL,
|
|
740
|
+
user_id TEXT NOT NULL DEFAULT 'default',
|
|
741
|
+
status TEXT NOT NULL,
|
|
742
|
+
current_step TEXT NOT NULL,
|
|
743
|
+
iteration INTEGER NOT NULL,
|
|
744
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
745
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
746
|
+
spec TEXT NOT NULL,
|
|
747
|
+
error TEXT,
|
|
748
|
+
last_heartbeat TIMESTAMPTZ
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
CREATE INDEX IF NOT EXISTS idx_pipelines_status
|
|
752
|
+
ON public.dark_factory_pipelines(user_id, project, status);
|
|
753
|
+
|
|
754
|
+
ALTER TABLE public.dark_factory_pipelines ENABLE ROW LEVEL SECURITY;
|
|
755
|
+
|
|
756
|
+
-- Idempotent policy creation
|
|
757
|
+
DO $$
|
|
758
|
+
BEGIN
|
|
759
|
+
IF NOT EXISTS (
|
|
760
|
+
SELECT 1 FROM pg_policies WHERE tablename = 'dark_factory_pipelines' AND policyname = 'allow_all_dark_factory'
|
|
761
|
+
) THEN
|
|
762
|
+
CREATE POLICY allow_all_dark_factory
|
|
763
|
+
ON public.dark_factory_pipelines AS PERMISSIVE FOR ALL USING (true);
|
|
764
|
+
END IF;
|
|
765
|
+
END $$;
|
|
766
|
+
|
|
767
|
+
-- Retrofit CHECK constraint for existing deployments.
|
|
768
|
+
-- DROP first (idempotent) then ADD — covers both fresh and upgraded tables.
|
|
769
|
+
ALTER TABLE public.dark_factory_pipelines
|
|
770
|
+
DROP CONSTRAINT IF EXISTS chk_pipeline_status;
|
|
771
|
+
ALTER TABLE public.dark_factory_pipelines
|
|
772
|
+
ADD CONSTRAINT chk_pipeline_status
|
|
773
|
+
CHECK (status IN ('PENDING', 'RUNNING', 'PAUSED', 'ABORTED', 'COMPLETED', 'FAILED'));
|
|
774
|
+
`
|
|
775
|
+
},
|
|
724
776
|
];
|
|
725
777
|
/**
|
|
726
778
|
* Current schema version — derived from the MIGRATIONS array.
|
package/dist/tools/index.js
CHANGED
|
@@ -47,3 +47,8 @@ export { agentRegisterHandler, agentHeartbeatHandler, agentListTeamHandler } fro
|
|
|
47
47
|
// Registered when PRISM_TASK_ROUTER_ENABLED=true.
|
|
48
48
|
// server.ts handles the conditional registration.
|
|
49
49
|
export { sessionTaskRouteHandler } from "./taskRouterHandler.js";
|
|
50
|
+
// ── Dark Factory Pipeline Tools (v7.3 — Autonomous Execution, Optional) ──
|
|
51
|
+
// Registered when PRISM_DARK_FACTORY_ENABLED=true.
|
|
52
|
+
// server.ts handles the conditional registration.
|
|
53
|
+
export { SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL, isStartPipelineArgs, isCheckPipelineStatusArgs, isAbortPipelineArgs, } from "./pipelineDefinitions.js";
|
|
54
|
+
export { sessionStartPipelineHandler, sessionCheckPipelineStatusHandler, sessionAbortPipelineHandler, } from "./pipelineHandlers.js";
|