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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { VALID_ACTION_TYPES } from './schema.js';
|
|
2
|
+
import { PRISM_DARK_FACTORY_MAX_RUNTIME_MS } from '../config.js';
|
|
3
|
+
import { debugLog } from '../utils/logger.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
/**
|
|
6
|
+
* Controller strictly enforcing safety and invariant checks across Factory Pipelines.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* 1. Iteration limit enforcement (prevents runaway LLM loops)
|
|
10
|
+
* 2. Path scope validation (prevents filesystem escapes)
|
|
11
|
+
* 3. Heartbeat lapse detection (finds zombie pipelines)
|
|
12
|
+
* 4. State machine transition validation (prevents illegal status jumps)
|
|
13
|
+
* 5. System prompt boundary generation (scope injection into LLM calls)
|
|
14
|
+
* 6. Total wall-clock runtime enforcement
|
|
15
|
+
*/
|
|
16
|
+
export class SafetyController {
|
|
17
|
+
/**
|
|
18
|
+
* Defines how long a pipeline can go without a heartbeat before being considered "zombie".
|
|
19
|
+
* Handled by the Dark Factory Runner watchdog.
|
|
20
|
+
*/
|
|
21
|
+
static HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
22
|
+
/**
|
|
23
|
+
* Legal state transitions for the pipeline state machine.
|
|
24
|
+
* Any transition not listed here is rejected by validateTransition().
|
|
25
|
+
*/
|
|
26
|
+
static LEGAL_TRANSITIONS = {
|
|
27
|
+
'PENDING': ['RUNNING', 'ABORTED'], // Queued → Runner promotes or user aborts
|
|
28
|
+
'RUNNING': ['PAUSED', 'ABORTED', 'COMPLETED', 'FAILED'],
|
|
29
|
+
'PAUSED': ['RUNNING', 'ABORTED'],
|
|
30
|
+
'ABORTED': [], // Terminal — no exits
|
|
31
|
+
'COMPLETED': [], // Terminal — no exits
|
|
32
|
+
'FAILED': ['RUNNING'], // Allow retry from failed state
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Legal step transitions for the pipeline execution state machine.
|
|
36
|
+
* FINALIZE is entered from VERIFY when iteration == maxIterations or success.
|
|
37
|
+
*/
|
|
38
|
+
static STEP_ORDER = [
|
|
39
|
+
'INIT', 'PLAN', 'EXECUTE', 'VERIFY', 'FINALIZE'
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Prevents runaway LLM invocation loops by enforcing the max iteration envelope.
|
|
43
|
+
*/
|
|
44
|
+
static validateIterationLimit(iteration, spec) {
|
|
45
|
+
return iteration <= spec.maxIterations;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Ensure a target path operates only within the explicitly restricted spec zone.
|
|
49
|
+
* If workingDirectory is missing, the global app root is assumed.
|
|
50
|
+
*/
|
|
51
|
+
static isPathWithinScope(targetPath, spec) {
|
|
52
|
+
if (!spec.workingDirectory)
|
|
53
|
+
return true;
|
|
54
|
+
// Resolve symlinks and protect against ../ escapes.
|
|
55
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
56
|
+
const resolvedWorkspace = path.resolve(spec.workingDirectory);
|
|
57
|
+
// Path Traversal Guard: A naive startsWith() check is vulnerable to
|
|
58
|
+
// prefix collisions — e.g. /app/workspace-hacked passes startsWith('/app/workspace').
|
|
59
|
+
// We require EITHER exact match OR the target starts with workspace + path separator.
|
|
60
|
+
if (resolvedTarget !== resolvedWorkspace && !resolvedTarget.startsWith(resolvedWorkspace + path.sep)) {
|
|
61
|
+
debugLog(`[Safety] Rejecting out-of-scope path resolution: ${targetPath}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Batch-validate an array of ActionPayload objects against the pipeline spec.
|
|
68
|
+
*
|
|
69
|
+
* Checks:
|
|
70
|
+
* 1. Each action has a valid ActionType
|
|
71
|
+
* 2. Each action's targetPath is non-empty
|
|
72
|
+
* 3. Each action's targetPath resolves within workingDirectory (via isPathWithinScope)
|
|
73
|
+
*
|
|
74
|
+
* Returns the first violation message (string) if any action fails,
|
|
75
|
+
* or null if all actions are valid and in-scope.
|
|
76
|
+
*
|
|
77
|
+
* Used by runner.ts after parsing EXECUTE step output — any non-null return
|
|
78
|
+
* terminates the pipeline immediately (fail closed).
|
|
79
|
+
*/
|
|
80
|
+
static validateActionsInScope(actions, spec) {
|
|
81
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
82
|
+
return 'Actions array is empty or not an array';
|
|
83
|
+
}
|
|
84
|
+
for (let i = 0; i < actions.length; i++) {
|
|
85
|
+
const action = actions[i];
|
|
86
|
+
// Validate action type is in the restricted set
|
|
87
|
+
if (!action.type || !VALID_ACTION_TYPES.includes(action.type)) {
|
|
88
|
+
return `Action[${i}]: invalid type "${action.type}" (allowed: ${VALID_ACTION_TYPES.join(', ')})`;
|
|
89
|
+
}
|
|
90
|
+
// Validate targetPath is non-empty
|
|
91
|
+
if (!action.targetPath || typeof action.targetPath !== 'string' || action.targetPath.trim() === '') {
|
|
92
|
+
return `Action[${i}]: targetPath is empty or missing`;
|
|
93
|
+
}
|
|
94
|
+
// Resolve targetPath relative to workingDirectory for scope check
|
|
95
|
+
const resolvedTarget = spec.workingDirectory
|
|
96
|
+
? path.resolve(spec.workingDirectory, action.targetPath)
|
|
97
|
+
: path.resolve(action.targetPath);
|
|
98
|
+
if (!SafetyController.isPathWithinScope(resolvedTarget, spec)) {
|
|
99
|
+
return `Action[${i}]: path "${action.targetPath}" resolves outside permitted scope`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null; // All actions valid and in-scope
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Determine whether a pipeline has timed out based on its recorded heartbeat.
|
|
106
|
+
*/
|
|
107
|
+
static isHeartbeatLapsed(state, timeoutOverrideMs) {
|
|
108
|
+
if (!state.last_heartbeat) {
|
|
109
|
+
// Pipeline never heartbeat. Use started_at as fallback
|
|
110
|
+
const diff = Date.now() - new Date(state.started_at).getTime();
|
|
111
|
+
return diff > (timeoutOverrideMs || SafetyController.HEARTBEAT_TIMEOUT_MS);
|
|
112
|
+
}
|
|
113
|
+
const diff = Date.now() - new Date(state.last_heartbeat).getTime();
|
|
114
|
+
return diff > (timeoutOverrideMs || SafetyController.HEARTBEAT_TIMEOUT_MS);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validate that a status transition is legal under the state machine.
|
|
118
|
+
* Prevents impossible jumps (e.g., COMPLETED → RUNNING) that would
|
|
119
|
+
* corrupt pipeline audit trails.
|
|
120
|
+
*/
|
|
121
|
+
static validateTransition(from, to) {
|
|
122
|
+
const legal = SafetyController.LEGAL_TRANSITIONS[from];
|
|
123
|
+
if (!legal)
|
|
124
|
+
return false;
|
|
125
|
+
return legal.includes(to);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Return the list of legal target statuses for a given source status.
|
|
129
|
+
* Used to build descriptive error messages in storage backends.
|
|
130
|
+
*/
|
|
131
|
+
static getLegalTransitions(from) {
|
|
132
|
+
return SafetyController.LEGAL_TRANSITIONS[from] ?? [];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check whether the pipeline has exceeded its total wall-clock runtime.
|
|
136
|
+
* Uses the configurable PRISM_DARK_FACTORY_MAX_RUNTIME_MS (default: 15 min).
|
|
137
|
+
*/
|
|
138
|
+
static isRuntimeExceeded(state) {
|
|
139
|
+
const elapsed = Date.now() - new Date(state.started_at).getTime();
|
|
140
|
+
return elapsed > PRISM_DARK_FACTORY_MAX_RUNTIME_MS;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Generate a scoped system prompt that enforces operational boundaries
|
|
144
|
+
* for all LLM calls within the pipeline. This is the "boundary injection"
|
|
145
|
+
* that prevents the model from operating outside its mandate.
|
|
146
|
+
*
|
|
147
|
+
* Used by clawInvocation.ts instead of inline prompt construction.
|
|
148
|
+
*/
|
|
149
|
+
static generateBoundaryPrompt(spec, state) {
|
|
150
|
+
const lines = [
|
|
151
|
+
`You are Prism Dark Factory, operating in the background as an autonomous code agent.`,
|
|
152
|
+
`You are strictly limited to code actions within the defined scope.`,
|
|
153
|
+
``,
|
|
154
|
+
`── Operational Boundaries ──`,
|
|
155
|
+
`Pipeline ID: ${state.id}`,
|
|
156
|
+
`Project: ${state.project}`,
|
|
157
|
+
`Current Step: ${state.current_step}`,
|
|
158
|
+
`Iteration: ${state.iteration} / ${spec.maxIterations}`,
|
|
159
|
+
`Restricted Workspace: ${spec.workingDirectory || '(unrestricted)'}`,
|
|
160
|
+
];
|
|
161
|
+
if (spec.contextFiles && spec.contextFiles.length > 0) {
|
|
162
|
+
lines.push(`Context Files: ${spec.contextFiles.join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
lines.push(``, `── Objective ──`, spec.objective, ``, `── Safety Rules ──`, `1. Do NOT modify files outside the Restricted Workspace.`, `2. Do NOT make network requests unless the objective explicitly requires it.`, `3. Do NOT execute destructive operations (rm -rf, DROP TABLE, etc.).`, `4. Respond ONLY with actions relevant to the current step.`, `5. If you cannot complete the step, explain why and stop.`);
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Determine the next step in the pipeline execution sequence.
|
|
169
|
+
* Returns null if the pipeline should terminate (FINALIZE reached or iteration exceeded).
|
|
170
|
+
*/
|
|
171
|
+
static getNextStep(currentStep, iteration, spec, verifyPassed) {
|
|
172
|
+
switch (currentStep) {
|
|
173
|
+
case 'INIT':
|
|
174
|
+
return { step: 'PLAN', iteration };
|
|
175
|
+
case 'PLAN':
|
|
176
|
+
return { step: 'EXECUTE', iteration };
|
|
177
|
+
case 'EXECUTE':
|
|
178
|
+
return { step: 'VERIFY', iteration };
|
|
179
|
+
case 'VERIFY':
|
|
180
|
+
if (verifyPassed) {
|
|
181
|
+
return { step: 'FINALIZE', iteration };
|
|
182
|
+
}
|
|
183
|
+
// Verification failed — loop back to PLAN with incremented iteration
|
|
184
|
+
const nextIteration = iteration + 1;
|
|
185
|
+
if (!SafetyController.validateIterationLimit(nextIteration, spec)) {
|
|
186
|
+
// Exceeded max iterations — force finalize with failure
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return { step: 'PLAN', iteration: nextIteration };
|
|
190
|
+
case 'FINALIZE':
|
|
191
|
+
return null; // Terminal step
|
|
192
|
+
default:
|
|
193
|
+
debugLog(`[Safety] Unknown step "${currentStep}" — forcing termination`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -782,6 +782,109 @@ return false;}
|
|
|
782
782
|
return res.end(JSON.stringify({ error: err.message || "Failed to search memory" }));
|
|
783
783
|
}
|
|
784
784
|
}
|
|
785
|
+
// ─── API: Dark Factory Pipelines (v7.3) ───────────────────
|
|
786
|
+
// GET /api/pipelines — List pipelines (optional ?status=RUNNING&project=myproj)
|
|
787
|
+
if (url.pathname === "/api/pipelines" && req.method === "GET") {
|
|
788
|
+
try {
|
|
789
|
+
const s = await getStorageSafe();
|
|
790
|
+
if (!s) {
|
|
791
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
792
|
+
return res.end(JSON.stringify({ error: "Storage initializing..." }));
|
|
793
|
+
}
|
|
794
|
+
// Validate status filter against canonical set
|
|
795
|
+
var rawStatus = url.searchParams.get("status") || undefined;
|
|
796
|
+
var VALID_STATUSES = ['PENDING', 'RUNNING', 'PAUSED', 'ABORTED', 'COMPLETED', 'FAILED'];
|
|
797
|
+
if (rawStatus && VALID_STATUSES.indexOf(rawStatus) === -1) {
|
|
798
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
799
|
+
return res.end(JSON.stringify({ error: 'Invalid status filter: "' + rawStatus + '". Valid: ' + VALID_STATUSES.join(', ') }));
|
|
800
|
+
}
|
|
801
|
+
var statusFilter = rawStatus;
|
|
802
|
+
const projectFilter = url.searchParams.get("project") || undefined;
|
|
803
|
+
const pipelines = await s.listPipelines(projectFilter, statusFilter, PRISM_USER_ID);
|
|
804
|
+
// Parse spec JSON for frontend consumption
|
|
805
|
+
const enriched = pipelines.map((p) => {
|
|
806
|
+
let parsedSpec = null;
|
|
807
|
+
try {
|
|
808
|
+
parsedSpec = JSON.parse(p.spec);
|
|
809
|
+
}
|
|
810
|
+
catch { /* corrupt spec */ }
|
|
811
|
+
return { ...p, parsedSpec };
|
|
812
|
+
});
|
|
813
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
814
|
+
return res.end(JSON.stringify({ pipelines: enriched }));
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
console.error("[Dashboard] Pipeline list error:", err);
|
|
818
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
819
|
+
return res.end(JSON.stringify({ error: "Failed to list pipelines" }));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// GET /api/pipelines/:id — Single pipeline detail
|
|
823
|
+
if (url.pathname.startsWith("/api/pipelines/") && !url.pathname.includes("/abort") && req.method === "GET") {
|
|
824
|
+
try {
|
|
825
|
+
const pipelineId = url.pathname.replace("/api/pipelines/", "");
|
|
826
|
+
if (!pipelineId) {
|
|
827
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
828
|
+
return res.end(JSON.stringify({ error: "Missing pipeline ID" }));
|
|
829
|
+
}
|
|
830
|
+
const s = await getStorageSafe();
|
|
831
|
+
if (!s) {
|
|
832
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
833
|
+
return res.end(JSON.stringify({ error: "Storage initializing..." }));
|
|
834
|
+
}
|
|
835
|
+
const pipeline = await s.getPipeline(pipelineId, PRISM_USER_ID);
|
|
836
|
+
if (!pipeline) {
|
|
837
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
838
|
+
return res.end(JSON.stringify({ error: "Pipeline not found" }));
|
|
839
|
+
}
|
|
840
|
+
let parsedSpec = null;
|
|
841
|
+
try {
|
|
842
|
+
parsedSpec = JSON.parse(pipeline.spec);
|
|
843
|
+
}
|
|
844
|
+
catch { /* corrupt spec */ }
|
|
845
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
846
|
+
return res.end(JSON.stringify({ ...pipeline, parsedSpec }));
|
|
847
|
+
}
|
|
848
|
+
catch (err) {
|
|
849
|
+
console.error("[Dashboard] Pipeline detail error:", err);
|
|
850
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
851
|
+
return res.end(JSON.stringify({ error: "Failed to get pipeline" }));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// POST /api/pipelines/:id/abort — Dashboard kill switch
|
|
855
|
+
if (url.pathname.match(/^\/api\/pipelines\/[^/]+\/abort$/) && req.method === "POST") {
|
|
856
|
+
try {
|
|
857
|
+
const pipelineId = url.pathname.replace("/api/pipelines/", "").replace("/abort", "");
|
|
858
|
+
const s = await getStorageSafe();
|
|
859
|
+
if (!s) {
|
|
860
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
861
|
+
return res.end(JSON.stringify({ error: "Storage initializing..." }));
|
|
862
|
+
}
|
|
863
|
+
const pipeline = await s.getPipeline(pipelineId, PRISM_USER_ID);
|
|
864
|
+
if (!pipeline) {
|
|
865
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
866
|
+
return res.end(JSON.stringify({ error: "Pipeline not found" }));
|
|
867
|
+
}
|
|
868
|
+
// Already terminated?
|
|
869
|
+
if (["COMPLETED", "FAILED", "ABORTED"].includes(pipeline.status)) {
|
|
870
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
871
|
+
return res.end(JSON.stringify({ ok: true, status: pipeline.status, message: `Pipeline already in terminal state: ${pipeline.status}` }));
|
|
872
|
+
}
|
|
873
|
+
await s.savePipeline({
|
|
874
|
+
...pipeline,
|
|
875
|
+
status: "ABORTED",
|
|
876
|
+
error: "Manually aborted via dashboard kill switch.",
|
|
877
|
+
});
|
|
878
|
+
console.error(`[Dashboard] Pipeline ${pipelineId} aborted via dashboard.`);
|
|
879
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
880
|
+
return res.end(JSON.stringify({ ok: true, status: "ABORTED", message: "Pipeline aborted. Runner will stop on next tick." }));
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
console.error("[Dashboard] Pipeline abort error:", err);
|
|
884
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
885
|
+
return res.end(JSON.stringify({ error: "Failed to abort pipeline" }));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
785
888
|
if (url.pathname === "/manifest.json" && req.method === "GET") {
|
|
786
889
|
const manifest = {
|
|
787
890
|
name: "Prism Mind Palace",
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -602,6 +602,7 @@ export function renderDashboardHTML(version) {
|
|
|
602
602
|
<div class="main-tabs" style="display:flex; gap: 1rem; border-bottom: 1px solid var(--border-glass); margin-bottom: 1.5rem; padding-bottom: 0;">
|
|
603
603
|
<button class="s-tab active" id="mtab-project" onclick="switchMainTab('project')" style="font-size: 1rem;">📁 Project View</button>
|
|
604
604
|
<button class="s-tab" id="mtab-search" onclick="switchMainTab('search')" style="font-size: 1rem;">🔍 Vector Search</button>
|
|
605
|
+
<button class="s-tab" id="mtab-factory" onclick="switchMainTab('factory')" style="font-size: 1rem;">🏭 Factory</button>
|
|
605
606
|
</div>
|
|
606
607
|
|
|
607
608
|
<div id="welcome" class="empty">
|
|
@@ -929,6 +930,30 @@ export function renderDashboardHTML(version) {
|
|
|
929
930
|
</div>
|
|
930
931
|
</div>
|
|
931
932
|
|
|
933
|
+
<!-- Dark Factory View (v7.3) -->
|
|
934
|
+
<div id="factory-content" class="fade-in" style="display:none; margin: 0 auto; max-width: 1000px; padding: 0 1rem;">
|
|
935
|
+
<div class="card">
|
|
936
|
+
<div class="card-title" style="display:flex;align-items:center;">
|
|
937
|
+
<span class="dot" style="background:var(--accent-amber)"></span>
|
|
938
|
+
Dark Factory — Autonomous Pipelines 🏭
|
|
939
|
+
<div style="flex:1"></div>
|
|
940
|
+
<button onclick="loadPipelines()" class="refresh-btn">↻</button>
|
|
941
|
+
</div>
|
|
942
|
+
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;align-items:center;">
|
|
943
|
+
<select id="factoryStatusFilter" class="input-modern" style="font-size:0.75rem;padding:0.3rem 0.5rem" onchange="loadPipelines()">
|
|
944
|
+
<option value="">All Statuses</option>
|
|
945
|
+
<option value="PENDING">⏸ Pending</option>
|
|
946
|
+
<option value="RUNNING">⏳ Running</option>
|
|
947
|
+
<option value="COMPLETED">✅ Completed</option>
|
|
948
|
+
<option value="FAILED">❌ Failed</option>
|
|
949
|
+
<option value="ABORTED">🛑 Aborted</option>
|
|
950
|
+
</select>
|
|
951
|
+
<span id="factoryCount" style="font-size:0.75rem;color:var(--text-muted);margin-left:auto"></span>
|
|
952
|
+
</div>
|
|
953
|
+
<div id="factoryList" style="font-size:0.85rem;color:var(--text-muted)">Loading pipelines...</div>
|
|
954
|
+
</div>
|
|
955
|
+
</div>
|
|
956
|
+
|
|
932
957
|
<!-- Settings Modal (v3.0) -->
|
|
933
958
|
<div class="modal-overlay" id="settingsModal">
|
|
934
959
|
<div class="modal">
|
|
@@ -1350,15 +1375,102 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
|
|
|
1350
1375
|
function switchMainTab(tabId) {
|
|
1351
1376
|
document.getElementById('mtab-project').classList.toggle('active', tabId === 'project');
|
|
1352
1377
|
document.getElementById('mtab-search').classList.toggle('active', tabId === 'search');
|
|
1378
|
+
document.getElementById('mtab-factory').classList.toggle('active', tabId === 'factory');
|
|
1353
1379
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
document.getElementById('search-content').style.display = 'block';
|
|
1380
|
+
document.getElementById('content').style.display = tabId === 'project' ? '' : 'none';
|
|
1381
|
+
document.getElementById('search-content').style.display = tabId === 'search' ? 'block' : 'none';
|
|
1382
|
+
document.getElementById('factory-content').style.display = tabId === 'factory' ? 'block' : 'none';
|
|
1383
|
+
|
|
1384
|
+
if (tabId === 'search') {
|
|
1360
1385
|
document.getElementById('searchInput').focus();
|
|
1361
1386
|
}
|
|
1387
|
+
if (tabId === 'factory') {
|
|
1388
|
+
loadPipelines();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// ─── DARK FACTORY (v7.3) ───
|
|
1393
|
+
var factoryPollTimer = null;
|
|
1394
|
+
|
|
1395
|
+
function loadPipelines() {
|
|
1396
|
+
var statusFilter = document.getElementById('factoryStatusFilter').value;
|
|
1397
|
+
var url = '/api/pipelines';
|
|
1398
|
+
if (statusFilter) url += '?status=' + encodeURIComponent(statusFilter);
|
|
1399
|
+
|
|
1400
|
+
fetch(url)
|
|
1401
|
+
.then(function(r) { return r.json(); })
|
|
1402
|
+
.then(function(data) {
|
|
1403
|
+
var list = document.getElementById('factoryList');
|
|
1404
|
+
var count = document.getElementById('factoryCount');
|
|
1405
|
+
var pipelines = data.pipelines || [];
|
|
1406
|
+
count.textContent = pipelines.length + ' pipeline' + (pipelines.length !== 1 ? 's' : '');
|
|
1407
|
+
|
|
1408
|
+
if (pipelines.length === 0) {
|
|
1409
|
+
list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted)"><div style="font-size:2rem;margin-bottom:0.5rem">🏭</div>No pipelines found. Use <code>session_start_pipeline</code> to create one.</div>';
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
var html = '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
1414
|
+
for (var i = 0; i < pipelines.length; i++) {
|
|
1415
|
+
var p = pipelines[i];
|
|
1416
|
+
var emoji = p.status === 'COMPLETED' ? '✅' : p.status === 'FAILED' ? '❌' : p.status === 'ABORTED' ? '🛑' : p.status === 'RUNNING' ? '⏳' : p.status === 'PENDING' ? '⏸' : '📋';
|
|
1417
|
+
var statusColor = p.status === 'COMPLETED' ? 'var(--accent-green)' : p.status === 'FAILED' ? 'var(--accent-rose)' : p.status === 'ABORTED' ? 'var(--accent-amber)' : p.status === 'RUNNING' ? 'var(--accent-purple)' : p.status === 'PENDING' ? 'var(--accent-blue, #3b82f6)' : 'var(--text-muted)';
|
|
1418
|
+
var isActive = p.status === 'RUNNING' || p.status === 'PENDING';
|
|
1419
|
+
var objective = (p.parsedSpec && p.parsedSpec.objective) ? p.parsedSpec.objective : '(unknown)';
|
|
1420
|
+
if (objective.length > 120) objective = objective.slice(0, 120) + '…';
|
|
1421
|
+
var maxIter = (p.parsedSpec && p.parsedSpec.maxIterations) ? p.parsedSpec.maxIterations : '?';
|
|
1422
|
+
|
|
1423
|
+
html += '<div style="padding:0.75rem 1rem;background:rgba(15,23,42,0.6);border-radius:8px;border-left:3px solid ' + statusColor + ';">';
|
|
1424
|
+
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">';
|
|
1425
|
+
html += '<span style="font-weight:600;color:var(--text-primary)">' + emoji + ' ' + p.status + '</span>';
|
|
1426
|
+
html += '<span style="font-size:0.7rem;font-family:var(--font-mono);color:var(--text-muted)">' + p.id.slice(0, 8) + '…</span>';
|
|
1427
|
+
html += '</div>';
|
|
1428
|
+
html += '<div style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.35rem">' + objective + '</div>';
|
|
1429
|
+
html += '<div style="display:flex;gap:1rem;font-size:0.72rem;color:var(--text-muted);flex-wrap:wrap">';
|
|
1430
|
+
html += '<span>📁 ' + (p.project || '?') + '</span>';
|
|
1431
|
+
html += '<span>🔄 ' + p.iteration + ' / ' + maxIter + '</span>';
|
|
1432
|
+
html += '<span>📍 ' + (p.current_step || '?') + '</span>';
|
|
1433
|
+
html += '<span>🕐 ' + new Date(p.updated_at).toLocaleString() + '</span>';
|
|
1434
|
+
html += '</div>';
|
|
1435
|
+
if (p.error) {
|
|
1436
|
+
html += '<div style="font-size:0.72rem;color:var(--accent-rose);margin-top:0.35rem;padding:0.3rem 0.5rem;background:rgba(244,63,94,0.08);border-radius:4px">⚠ ' + p.error.slice(0, 200) + '</div>';
|
|
1437
|
+
}
|
|
1438
|
+
if (isActive) {
|
|
1439
|
+
html += '<div style="margin-top:0.5rem"><button onclick="abortPipeline(\'' + p.id + '\')" class="cleanup-btn" style="font-size:0.72rem">🛑 Abort Pipeline</button></div>';
|
|
1440
|
+
}
|
|
1441
|
+
html += '</div>';
|
|
1442
|
+
}
|
|
1443
|
+
html += '</div>';
|
|
1444
|
+
list.innerHTML = html;
|
|
1445
|
+
|
|
1446
|
+
// Auto-poll if any pipeline is running
|
|
1447
|
+
var hasActive = pipelines.some(function(p) { return p.status === 'RUNNING' || p.status === 'PENDING'; });
|
|
1448
|
+
clearInterval(factoryPollTimer);
|
|
1449
|
+
if (hasActive) {
|
|
1450
|
+
factoryPollTimer = setInterval(function() {
|
|
1451
|
+
if (document.getElementById('factory-content').style.display !== 'none') loadPipelines();
|
|
1452
|
+
else clearInterval(factoryPollTimer);
|
|
1453
|
+
}, 10000);
|
|
1454
|
+
}
|
|
1455
|
+
})
|
|
1456
|
+
.catch(function(err) {
|
|
1457
|
+
document.getElementById('factoryList').innerHTML = '<div style="color:var(--accent-rose);padding:1rem">Failed to load pipelines: ' + err.message + '</div>';
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function abortPipeline(id) {
|
|
1462
|
+
if (!confirm('Abort pipeline ' + id.slice(0, 8) + '…?')) return;
|
|
1463
|
+
fetch('/api/pipelines/' + id + '/abort', { method: 'POST' })
|
|
1464
|
+
.then(function(r) { return r.json(); })
|
|
1465
|
+
.then(function(data) {
|
|
1466
|
+
if (data.ok) {
|
|
1467
|
+
showToast('Pipeline aborted');
|
|
1468
|
+
loadPipelines();
|
|
1469
|
+
} else {
|
|
1470
|
+
showToast('Failed: ' + (data.error || 'Unknown error'));
|
|
1471
|
+
}
|
|
1472
|
+
})
|
|
1473
|
+
.catch(function(err) { showToast('Abort failed: ' + err.message); });
|
|
1362
1474
|
}
|
|
1363
1475
|
|
|
1364
1476
|
var searchTimeout = null;
|