jarvis-agent-factory 3.2.0 → 3.4.0
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/package.json +2 -1
- package/src/engine/agents.html +197 -0
- package/src/engine/db.js +112 -0
- package/src/engine/server.js +136 -147
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jarvis-agent-factory",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Jarvis Agent Factory CLI — 跨平台多智能体 AI 编程助手配置安装器 | Multi-agent AI coding assistant config installer for Claude Code / OpenCode / Codex",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jarvis",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
|
+
"better-sqlite3": "^12.9.0",
|
|
47
48
|
"express": "^5.2.1"
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang=zh>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
|
|
5
|
+
<title>Agents · Jarvis Engine</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--muted:#8b949e;--accent:#FF6B35;--green:#3fb950;--blue:#58a6ff;--purple:#bc8cff;--yellow:#d2991d}
|
|
8
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
9
|
+
body{font-family:system-ui;background:var(--bg);color:var(--text);min-height:100vh}
|
|
10
|
+
header{background:var(--card);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;justify-content:space-between;align-items:center}
|
|
11
|
+
h1{color:var(--accent);font-size:18px}
|
|
12
|
+
nav{display:flex;gap:8px}
|
|
13
|
+
nav a{color:var(--muted);text-decoration:none;padding:6px 14px;border-radius:6px;font-size:13px;transition:all .2s}
|
|
14
|
+
nav a:hover,nav a.active{color:var(--text);background:var(--border)}
|
|
15
|
+
main{max-width:1200px;margin:0 auto;padding:24px}
|
|
16
|
+
.toolbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;flex-wrap:wrap;gap:12px}
|
|
17
|
+
.toolbar .info{color:var(--muted);font-size:13px}
|
|
18
|
+
.model-filter{display:flex;gap:6px;flex-wrap:wrap}
|
|
19
|
+
.model-filter button{padding:4px 10px;border-radius:12px;border:1px solid var(--border);background:var(--card);color:var(--muted);cursor:pointer;font-size:11px;transition:all .2s}
|
|
20
|
+
.model-filter button:hover,.model-filter button.sel{border-color:var(--accent);color:var(--accent)}
|
|
21
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}
|
|
22
|
+
/* Pixel card */
|
|
23
|
+
.pixel-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px;cursor:pointer;transition:all .2s;position:relative;overflow:hidden}
|
|
24
|
+
.pixel-card:hover{border-color:var(--accent);transform:translateY(-2px)}
|
|
25
|
+
.pixel-card.custom{border-color:var(--purple)}
|
|
26
|
+
.pixel-card .role-tag{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
|
27
|
+
.pixel-card .agent-name{font-size:14px;font-weight:600;margin:4px 0}
|
|
28
|
+
.pixel-card .model-name{font-size:11px;color:var(--accent);margin-top:4px}
|
|
29
|
+
.pixel-card .model-name.custom{color:var(--purple)}
|
|
30
|
+
/* Pixel art avatar — 12x12 CSS grid */
|
|
31
|
+
.pixel-avatar{width:60px;height:60px;margin:8px auto;image-rendering:pixelated;position:relative}
|
|
32
|
+
.pixel-avatar .px{position:absolute;width:5px;height:5px;border-radius:0}
|
|
33
|
+
/* Pixel art patterns per icon */
|
|
34
|
+
.px-brain{background:var(--purple)} .px-layout{background:var(--blue)} .px-palette{background:var(--accent)}
|
|
35
|
+
.px-database{background:var(--green)} .px-test{background:var(--yellow)} .px-server{background:var(--blue)}
|
|
36
|
+
.px-route{background:var(--accent)} .px-cog{background:var(--muted)} .px-table{background:var(--green)}
|
|
37
|
+
.px-globe{background:var(--blue)} .px-play{background:var(--green)} .px-file{background:var(--muted)}
|
|
38
|
+
.px-map{background:var(--purple)} .px-list{background:var(--yellow)} .px-shield{background:var(--accent)}
|
|
39
|
+
.px-eye{background:var(--purple)}
|
|
40
|
+
/* Pixel patterns — simplified 8x8 */
|
|
41
|
+
.avatar-8{display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);width:56px;height:56px;gap:1px;margin:8px auto}
|
|
42
|
+
.avatar-8 span{width:100%;height:100%;border-radius:1px}
|
|
43
|
+
/* Model selector popup */
|
|
44
|
+
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:99;justify-content:center;align-items:center}
|
|
45
|
+
.modal-overlay.show{display:flex}
|
|
46
|
+
.modal{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;min-width:320px;max-width:400px}
|
|
47
|
+
.modal h3{font-size:16px;margin-bottom:4px}
|
|
48
|
+
.modal .sub{font-size:12px;color:var(--muted);margin-bottom:16px}
|
|
49
|
+
.modal select{width:100%;padding:10px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:8px;font-size:13px;margin-bottom:12px}
|
|
50
|
+
.modal .btns{display:flex;gap:8px;justify-content:flex-end}
|
|
51
|
+
.modal .btn{padding:8px 18px;border-radius:8px;border:1px solid var(--border);background:var(--bg);color:var(--text);cursor:pointer;font-size:13px}
|
|
52
|
+
.modal .btn.save{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
53
|
+
.modal .btn.reset{color:var(--muted)}
|
|
54
|
+
.toast{position:fixed;top:16px;right:16px;background:var(--green);color:#000;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:600;z-index:999;animation:fadein .3s}
|
|
55
|
+
@keyframes fadein{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
|
|
56
|
+
.empty{text-align:center;padding:40px;color:var(--muted)}
|
|
57
|
+
</style>
|
|
58
|
+
</head>
|
|
59
|
+
<body>
|
|
60
|
+
<header>
|
|
61
|
+
<div style=display:flex;gap:16px;align-items:center>
|
|
62
|
+
<h1>🧠 Jarvis Engine</h1>
|
|
63
|
+
<nav><a href=/dashboard>Pipeline</a><a href=/agents class=active>Agents</a></nav>
|
|
64
|
+
</div>
|
|
65
|
+
<span style=font-size:12px;color:var(--muted) id=agentCount>17 agents</span>
|
|
66
|
+
</header>
|
|
67
|
+
<main>
|
|
68
|
+
<div class=toolbar>
|
|
69
|
+
<div class=info id=toolbarInfo>Click a card to configure model</div>
|
|
70
|
+
<div class=model-filter id=modelFilter></div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class=grid id=agentsGrid><div class=empty>Loading...</div></div>
|
|
73
|
+
</main>
|
|
74
|
+
<div class=modal-overlay id=modal>
|
|
75
|
+
<div class=modal>
|
|
76
|
+
<h3 id=modalName>---</h3>
|
|
77
|
+
<div class=sub id=modalRole>---</div>
|
|
78
|
+
<select id=modalSelect></select>
|
|
79
|
+
<div class=btns>
|
|
80
|
+
<button class="btn reset" onclick=resetModel()>↩ Reset default</button>
|
|
81
|
+
<button class="btn save" onclick=saveModel()>💾 Save</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div id=toastContainer></div>
|
|
86
|
+
|
|
87
|
+
<script>
|
|
88
|
+
let agents=[],currentAgent=null,currentFilter='all';
|
|
89
|
+
|
|
90
|
+
const PIXEL_PATTERNS = {
|
|
91
|
+
brain: [0,0,0,1,1,0,0,0, 0,1,1,1,1,1,1,0, 1,1,0,1,1,0,1,1, 1,1,0,1,1,0,1,1, 1,1,1,1,1,1,1,1, 0,1,1,1,1,1,1,0, 0,0,1,0,0,1,0,0, 0,0,0,1,1,0,0,0],
|
|
92
|
+
layout:[0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,1, 1,0,1,1,1,0,0,1, 1,0,1,1,1,0,0,1, 1,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0],
|
|
93
|
+
palette:[0,0,1,0,0,1,0,0, 0,1,1,1,1,1,1,0, 1,0,0,0,0,0,0,1, 1,0,1,0,0,1,0,1, 1,0,0,1,1,0,0,1, 1,0,0,0,0,0,0,1, 0,1,1,1,1,1,1,0, 0,0,0,0,0,0,0,0],
|
|
94
|
+
database:[0,0,1,1,1,1,0,0, 0,1,0,0,0,0,1,0, 1,0,0,0,0,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1],
|
|
95
|
+
test:[0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 1,1,0,0,0,0,1,1, 0,0,1,0,0,1,0,0, 0,0,0,1,1,0,0,0, 0,0,1,0,0,1,0,0, 1,1,0,0,0,0,1,1, 0,0,0,0,0,0,0,0],
|
|
96
|
+
server:[0,0,1,1,1,1,0,0, 0,1,0,0,0,0,1,0, 1,0,1,1,1,0,0,1, 1,0,1,0,1,0,0,1, 1,0,1,1,1,0,0,1, 1,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,1, 0,1,1,1,1,1,1,0],
|
|
97
|
+
route:[0,0,0,0,0,0,1,0, 0,0,0,0,0,1,0,0, 0,0,0,0,1,0,1,0, 0,0,0,1,0,0,1,0, 0,0,1,0,0,0,1,0, 0,1,0,0,0,0,1,0, 1,0,0,0,0,0,1,0, 0,0,0,0,0,0,1,0],
|
|
98
|
+
cog:[0,0,0,1,1,0,0,0, 0,0,1,0,0,1,0,0, 0,1,0,1,1,0,1,0, 1,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,1, 0,1,0,1,1,0,1,0, 0,0,1,0,0,1,0,0, 0,0,0,1,1,0,0,0],
|
|
99
|
+
table:[1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,1, 1,0,1,0,0,0,0,1, 1,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1],
|
|
100
|
+
globe:[0,0,1,1,1,1,0,0, 0,1,0,1,1,0,1,0, 1,0,0,0,0,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,0,0,0,0,1, 1,0,1,0,0,1,0,1, 0,1,0,0,0,0,1,0, 0,0,1,1,1,1,0,0],
|
|
101
|
+
play:[0,0,0,0,0,0,0,0, 0,1,1,0,0,0,0,0, 0,1,1,1,0,0,0,0, 0,1,1,1,1,0,0,0, 0,1,1,1,1,1,0,0, 0,1,1,1,0,0,0,0, 0,1,1,0,0,0,0,0, 0,0,0,0,0,0,0,0],
|
|
102
|
+
file:[1,1,1,1,1,0,0,0, 1,0,0,0,1,0,0,0, 1,0,0,0,1,0,0,0, 1,0,0,0,1,0,0,0, 1,0,0,0,1,0,0,0, 1,0,0,0,1,0,0,0, 1,1,1,1,1,0,0,0, 1,1,1,1,1,1,1,0],
|
|
103
|
+
map:[0,0,0,1,1,0,0,0, 0,0,1,0,0,1,0,0, 0,1,0,1,1,0,1,0, 1,0,0,0,0,0,0,1, 1,0,0,1,1,0,0,1, 0,1,0,0,0,0,1,0, 0,0,1,0,0,1,0,0, 0,0,0,1,1,0,0,0],
|
|
104
|
+
list:[1,1,1,1,1,0,0,0, 1,0,0,0,0,0,0,0, 1,0,0,0,0,0,0,0, 0,1,1,1,1,0,0,0, 0,0,0,0,0,1,0,0, 0,0,0,0,0,1,0,0, 0,0,0,0,0,1,0,0, 1,1,1,1,1,1,0,0],
|
|
105
|
+
shield:[0,0,0,0,0,0,0,0, 0,1,1,1,1,1,1,0, 1,0,0,0,0,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,0,0,0,0,1, 0,1,0,0,0,0,1,0, 0,0,1,1,1,1,0,0],
|
|
106
|
+
eye:[0,0,0,0,0,0,0,0, 0,1,1,1,0,1,1,0, 1,0,0,1,0,0,0,1, 1,0,0,1,0,0,0,1, 0,1,1,1,0,1,1,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const ICON_COLORS = {
|
|
110
|
+
brain:'#bc8cff', layout:'#58a6ff', palette:'#FF6B35', database:'#3fb950', test:'#d2991d',
|
|
111
|
+
server:'#58a6ff', route:'#FF6B35', cog:'#8b949e', table:'#3fb950', globe:'#58a6ff',
|
|
112
|
+
play:'#3fb950', file:'#8b949e', map:'#bc8cff', list:'#d2991d', shield:'#FF6B35', eye:'#bc8cff',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function pixelAvatar(icon) {
|
|
116
|
+
const pattern = PIXEL_PATTERNS[icon] || PIXEL_PATTERNS.brain;
|
|
117
|
+
const color = ICON_COLORS[icon] || ICON_COLORS.brain;
|
|
118
|
+
return `<div class=avatar-8>${pattern.map(v => `<span style="background:${v?color:'transparent'}"></span>`).join('')}</div>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function load() {
|
|
122
|
+
try {
|
|
123
|
+
const r = await fetch('/api/agents');
|
|
124
|
+
const data = await r.json();
|
|
125
|
+
agents = data.agents;
|
|
126
|
+
renderAgents();
|
|
127
|
+
// Model filter buttons
|
|
128
|
+
const models = [...new Set(agents.map(a=>a.model))];
|
|
129
|
+
document.getElementById('modelFilter').innerHTML = [
|
|
130
|
+
`<button class="${currentFilter==='all'?'sel':''}" onclick="filterBy('all')">All</button>`,
|
|
131
|
+
...models.map(m => `<button class="${currentFilter===m?'sel':''}" onclick="filterBy('${m}')">${m}</button>`)
|
|
132
|
+
].join('');
|
|
133
|
+
} catch(e) { console.error(e); }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderAgents() {
|
|
137
|
+
const filtered = currentFilter === 'all' ? agents : agents.filter(a => a.model === currentFilter);
|
|
138
|
+
document.getElementById('agentCount').textContent = filtered.length + ' agents';
|
|
139
|
+
document.getElementById('agentsGrid').innerHTML = filtered.map(a =>
|
|
140
|
+
`<div class="pixel-card${a.is_custom?' custom':''}" onclick="openModal('${a.id}')">
|
|
141
|
+
${pixelAvatar(a.icon)}
|
|
142
|
+
<div class=role-tag>${a.role}</div>
|
|
143
|
+
<div class=agent-name>${a.name}</div>
|
|
144
|
+
<div class="model-name${a.is_custom?' custom':''}">${a.model}</div>
|
|
145
|
+
</div>`
|
|
146
|
+
).join('');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function filterBy(model) { currentFilter = model; renderAgents(); load(); }
|
|
150
|
+
|
|
151
|
+
function openModal(id) {
|
|
152
|
+
currentAgent = agents.find(a => a.id === id);
|
|
153
|
+
if (!currentAgent) return;
|
|
154
|
+
document.getElementById('modalName').textContent = currentAgent.name;
|
|
155
|
+
document.getElementById('modalRole').textContent = currentAgent.role + ' · Default: ' + currentAgent.defaultModel;
|
|
156
|
+
const sel = document.getElementById('modalSelect');
|
|
157
|
+
sel.innerHTML = data?.available_models?.map(m => `<option value="${m}" ${m===currentAgent.model?'selected':''}>${m}</option>`).join('') || '';
|
|
158
|
+
document.getElementById('modal').classList.add('show');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function loadModels() {
|
|
162
|
+
const r = await fetch('/api/agents'); data = await r.json();
|
|
163
|
+
agents = data.agents;
|
|
164
|
+
const sel = document.getElementById('modalSelect');
|
|
165
|
+
sel.innerHTML = data.available_models.map(m => `<option value="${m}">${m}</option>`).join('');
|
|
166
|
+
renderAgents();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let data = null;
|
|
170
|
+
|
|
171
|
+
async function saveModel() {
|
|
172
|
+
const model = document.getElementById('modalSelect').value;
|
|
173
|
+
await fetch('/api/agents', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({agent_id:currentAgent.id, model}) });
|
|
174
|
+
document.getElementById('modal').classList.remove('show');
|
|
175
|
+
toast(`💾 ${currentAgent.name} → ${model}`);
|
|
176
|
+
loadModels();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function resetModel() {
|
|
180
|
+
await fetch('/api/agents', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({agent_id:currentAgent.id, model:currentAgent.defaultModel}) });
|
|
181
|
+
document.getElementById('modal').classList.remove('show');
|
|
182
|
+
toast(`↩ ${currentAgent.name} → default (${currentAgent.defaultModel})`);
|
|
183
|
+
loadModels();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
document.getElementById('modal').addEventListener('click', e => { if (e.target === e.currentTarget) document.getElementById('modal').classList.remove('show'); });
|
|
187
|
+
|
|
188
|
+
function toast(msg) {
|
|
189
|
+
const t = document.createElement('div'); t.className='toast'; t.textContent=msg;
|
|
190
|
+
document.getElementById('toastContainer').appendChild(t);
|
|
191
|
+
setTimeout(() => t.remove(), 2500);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
loadModels();
|
|
195
|
+
</script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
package/src/engine/db.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export function openDb(root) {
|
|
6
|
+
const dir = join(root, '.jarvis');
|
|
7
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
8
|
+
const db = new Database(join(dir, 'engine.db'));
|
|
9
|
+
db.pragma('journal_mode = WAL');
|
|
10
|
+
db.pragma('busy_timeout = 5000');
|
|
11
|
+
initSchema(db);
|
|
12
|
+
return db;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function initSchema(db) {
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS pipeline (
|
|
18
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
19
|
+
project TEXT NOT NULL,
|
|
20
|
+
current_gate TEXT NOT NULL DEFAULT 'Gate A',
|
|
21
|
+
mode TEXT NOT NULL DEFAULT 'strict',
|
|
22
|
+
started_at TEXT NOT NULL,
|
|
23
|
+
updated_at TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
gate TEXT NOT NULL,
|
|
28
|
+
passed_at TEXT NOT NULL,
|
|
29
|
+
advance_to TEXT,
|
|
30
|
+
session_id TEXT,
|
|
31
|
+
UNIQUE(gate)
|
|
32
|
+
);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
platform TEXT DEFAULT 'unknown',
|
|
36
|
+
role TEXT NOT NULL DEFAULT 'observer',
|
|
37
|
+
created_at INTEGER NOT NULL,
|
|
38
|
+
last_heartbeat INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS agent_models (
|
|
41
|
+
agent_id TEXT PRIMARY KEY,
|
|
42
|
+
model TEXT NOT NULL,
|
|
43
|
+
updated_at TEXT NOT NULL
|
|
44
|
+
);
|
|
45
|
+
-- Init pipeline row if empty
|
|
46
|
+
INSERT OR IGNORE INTO pipeline (id, project, current_gate, mode, started_at, updated_at)
|
|
47
|
+
VALUES (1, '', 'Gate A', 'strict', datetime('now'), datetime('now'));
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- Pipeline ----
|
|
52
|
+
export function getPipeline(db) {
|
|
53
|
+
return db.prepare('SELECT * FROM pipeline WHERE id=1').get();
|
|
54
|
+
}
|
|
55
|
+
export function updatePipelineGate(db, gate) {
|
|
56
|
+
db.prepare('UPDATE pipeline SET current_gate=?, updated_at=datetime("now") WHERE id=1').run(gate);
|
|
57
|
+
}
|
|
58
|
+
export function initPipeline(db, project, sessionId) {
|
|
59
|
+
db.prepare('UPDATE pipeline SET project=?, current_gate="Gate A", mode="strict", started_at=datetime("now"), updated_at=datetime("now") WHERE id=1').run(project);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- Checkpoints ----
|
|
63
|
+
export function getCheckpoints(db, gate) {
|
|
64
|
+
return gate ? db.prepare('SELECT * FROM checkpoints WHERE gate=?').all(gate)
|
|
65
|
+
: db.prepare('SELECT * FROM checkpoints ORDER BY passed_at').all();
|
|
66
|
+
}
|
|
67
|
+
export function addCheckpoint(db, gate, advanceTo, sessionId) {
|
|
68
|
+
db.prepare('INSERT OR REPLACE INTO checkpoints (gate, passed_at, advance_to, session_id) VALUES (?, datetime("now"), ?, ?)').run(gate, advanceTo, sessionId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- Sessions ----
|
|
72
|
+
export function getSessions(db) {
|
|
73
|
+
return db.prepare('SELECT * FROM sessions ORDER BY created_at').all();
|
|
74
|
+
}
|
|
75
|
+
export function getSession(db, sid) {
|
|
76
|
+
return db.prepare('SELECT * FROM sessions WHERE id=?').get(sid);
|
|
77
|
+
}
|
|
78
|
+
export function addSession(db, sid, platform, role) {
|
|
79
|
+
db.prepare('INSERT OR REPLACE INTO sessions (id, platform, role, created_at, last_heartbeat) VALUES (?, ?, ?, ?, ?)').run(sid, platform, role, Date.now(), Date.now());
|
|
80
|
+
}
|
|
81
|
+
export function heartbeatSession(db, sid) {
|
|
82
|
+
db.prepare('UPDATE sessions SET last_heartbeat=? WHERE id=?').run(Date.now(), sid);
|
|
83
|
+
}
|
|
84
|
+
export function removeSession(db, sid) {
|
|
85
|
+
db.prepare('DELETE FROM sessions WHERE id=?').run(sid);
|
|
86
|
+
}
|
|
87
|
+
export function updateSessionRole(db, sid, role) {
|
|
88
|
+
db.prepare('UPDATE sessions SET role=? WHERE id=?').run(role, sid);
|
|
89
|
+
}
|
|
90
|
+
export function cleanupStaleSessions(db, timeoutMs) {
|
|
91
|
+
const cutoff = Date.now() - timeoutMs;
|
|
92
|
+
const stale = db.prepare('SELECT id FROM sessions WHERE last_heartbeat < ?').all(cutoff);
|
|
93
|
+
for (const s of stale) db.prepare('DELETE FROM sessions WHERE id=?').run(s.id);
|
|
94
|
+
return stale.map(s => s.id);
|
|
95
|
+
}
|
|
96
|
+
export function getOldestSession(db) {
|
|
97
|
+
return db.prepare('SELECT * FROM sessions ORDER BY created_at ASC LIMIT 1').get();
|
|
98
|
+
}
|
|
99
|
+
export function getLeader(db) {
|
|
100
|
+
return db.prepare('SELECT * FROM sessions WHERE role="leader" LIMIT 1').get();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- Agent Models ----
|
|
104
|
+
export function getAgentConfig(db) {
|
|
105
|
+
const rows = db.prepare('SELECT * FROM agent_models').all();
|
|
106
|
+
const cfg = {};
|
|
107
|
+
for (const r of rows) cfg[r.agent_id] = r.model;
|
|
108
|
+
return cfg;
|
|
109
|
+
}
|
|
110
|
+
export function setAgentModel(db, agentId, model) {
|
|
111
|
+
db.prepare('INSERT OR REPLACE INTO agent_models (agent_id, model, updated_at) VALUES (?, ?, datetime("now"))').run(agentId, model);
|
|
112
|
+
}
|
package/src/engine/server.js
CHANGED
|
@@ -2,30 +2,17 @@ import express from 'express';
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { readFileSync, readdirSync, existsSync
|
|
5
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
6
6
|
import { resolve, join } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
+
import { openDb, getPipeline, updatePipelineGate, initPipeline as dbInitPipeline, getCheckpoints, addCheckpoint, getSessions, getSession, addSession, heartbeatSession, removeSession, updateSessionRole, cleanupStaleSessions, getOldestSession, getLeader, getAgentConfig, setAgentModel } from './db.js';
|
|
8
9
|
|
|
9
10
|
const PID_FILE = resolve(homedir(), '.jarvis', 'engine.pid');
|
|
10
11
|
const DEFAULT_PORT = 3456;
|
|
12
|
+
const SESSION_TIMEOUT = 120_000;
|
|
11
13
|
const GATES = ['Gate A', 'Gate B', 'Gate C', 'Gate C1', 'Gate C1.5', 'Gate C2', 'Gate D', 'Gate E'];
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
'Gate A': 'requirements', 'Gate B': 'tasks', 'Gate C': 'plans',
|
|
15
|
-
'Gate C1': 'implementation', 'Gate C1.5': 'implementation',
|
|
16
|
-
'Gate C2': 'testing', 'Gate D': 'review', 'Gate E': 'shipping',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const GATE_CHECKS = {
|
|
20
|
-
'Gate A': { requires: ['requirements'], check: '至少 1 个需求文档,含 REQ-XXX 编号' },
|
|
21
|
-
'Gate B': { requires: ['tasks'], check: '每个 TASK-XXX 映射至少 1 个 REQ-XXX' },
|
|
22
|
-
'Gate C': { requires: ['plans'], check: '计划文档含 parallel_batches + Execution Packet' },
|
|
23
|
-
'Gate C1': { requires: ['implementation'], check: 'Lint + Type-check + Build + Deps Audit 全部通过' },
|
|
24
|
-
'Gate C1.5': { requires: ['implementation'], check: '页面/组件视觉验证截图证据已附' },
|
|
25
|
-
'Gate C2': { requires: ['testing'], check: '单元/集成/E2E/浏览器测试全部通过,API 契约验证通过' },
|
|
26
|
-
'Gate D': { requires: ['review'], check: 'review-qa 评审通过,REQ 追踪矩阵完整' },
|
|
27
|
-
'Gate E': { requires: ['shipping'], check: '安全审计 + 上线检查清单 + 回滚预案就绪' },
|
|
28
|
-
};
|
|
14
|
+
const GATE_DIRS = { 'Gate A':'requirements','Gate B':'tasks','Gate C':'plans','Gate C1':'implementation','Gate C1.5':'implementation','Gate C2':'testing','Gate D':'review','Gate E':'shipping' };
|
|
15
|
+
const GATE_CHECKS = { 'Gate A':{check:'至少1个需求文档,含REQ-XXX编号'},'Gate B':{check:'每个TASK-XXX映射至少1个REQ-XXX'},'Gate C':{check:'计划文档含parallel_batches+Execution Packet'},'Gate C1':{check:'Lint+Type-check+Build+Deps Audit全部通过'},'Gate C1.5':{check:'页面/组件视觉验证截图证据已附'},'Gate C2':{check:'单元/集成/E2E/浏览器测试全部通过,API契约验证通过'},'Gate D':{check:'review-qa评审通过,REQ追踪矩阵完整'},'Gate E':{check:'安全审计+上线检查清单+回滚预案就绪'} };
|
|
29
16
|
|
|
30
17
|
export async function startEngine({ port = DEFAULT_PORT, dashboard = false, projectRoot = '.' } = {}) {
|
|
31
18
|
const root = resolve(projectRoot);
|
|
@@ -38,131 +25,84 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
38
25
|
|
|
39
26
|
const server = new McpServer({ name: 'jarvis-engine', version: readPkgVersion() });
|
|
40
27
|
|
|
41
|
-
// ----
|
|
42
|
-
const
|
|
43
|
-
const SESSION_TIMEOUT = 120_000; // 2 min heartbeat timeout
|
|
44
|
-
let leaderSessionId = null;
|
|
45
|
-
|
|
46
|
-
function cleanupStaleSessions() {
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
for (const [sid, s] of sessions) {
|
|
49
|
-
if (now - s.last_heartbeat > SESSION_TIMEOUT) {
|
|
50
|
-
sessions.delete(sid);
|
|
51
|
-
if (sid === leaderSessionId) {
|
|
52
|
-
leaderSessionId = null;
|
|
53
|
-
// Elect new leader
|
|
54
|
-
const oldest = [...sessions.values()].sort((a, b) => a.created_at - b.created_at)[0];
|
|
55
|
-
if (oldest) { leaderSessionId = oldest.id; oldest.role = 'leader'; }
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// If no leader but sessions exist, elect oldest
|
|
60
|
-
if (!leaderSessionId && sessions.size > 0) {
|
|
61
|
-
const oldest = [...sessions.values()].sort((a, b) => a.created_at - b.created_at)[0];
|
|
62
|
-
leaderSessionId = oldest.id;
|
|
63
|
-
oldest.role = 'leader';
|
|
64
|
-
}
|
|
65
|
-
}
|
|
28
|
+
// ---- Database (SQLite) ----
|
|
29
|
+
const db = openDb(root);
|
|
66
30
|
|
|
31
|
+
// ---- Session Manager (SQLite-backed) ----
|
|
67
32
|
function requireLeader(sessionId) {
|
|
68
|
-
cleanupStaleSessions();
|
|
69
|
-
const s =
|
|
33
|
+
cleanupStaleSessions(db, SESSION_TIMEOUT);
|
|
34
|
+
const s = getSession(db, sessionId);
|
|
70
35
|
if (!s) return { error: 'Session not registered. Call session_join first.' };
|
|
71
|
-
if (s.role !== 'leader')
|
|
36
|
+
if (s.role !== 'leader') {
|
|
37
|
+
const leader = getLeader(db);
|
|
38
|
+
return { error: `Write lock held by session ${leader?.id || '?'}. You are observer (read-only).` };
|
|
39
|
+
}
|
|
72
40
|
return null;
|
|
73
41
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
function readPipeline() {
|
|
82
|
-
if (!existsSync(pipelinePath)) return null;
|
|
83
|
-
try { return JSON.parse(readFileSync(pipelinePath, 'utf-8')); } catch { return null; }
|
|
42
|
+
function electLeader() {
|
|
43
|
+
const leader = getLeader(db);
|
|
44
|
+
if (leader) return leader.id;
|
|
45
|
+
const oldest = getOldestSession(db);
|
|
46
|
+
if (oldest) { updateSessionRole(db, oldest.id, 'leader'); return oldest.id; }
|
|
47
|
+
return null;
|
|
84
48
|
}
|
|
49
|
+
// Heartbeat cleanup every 30s
|
|
50
|
+
setInterval(() => {
|
|
51
|
+
const stale = cleanupStaleSessions(db, SESSION_TIMEOUT);
|
|
52
|
+
if (stale.length) electLeader();
|
|
53
|
+
}, 30_000);
|
|
54
|
+
|
|
55
|
+
// ---- Pipeline state machine (SQLite-backed) ----
|
|
56
|
+
function readPipeline() { return getPipeline(db); }
|
|
85
57
|
function writePipeline(state) {
|
|
86
|
-
|
|
87
|
-
writeFileSync(pipelinePath, JSON.stringify({ ...state, updated_at: new Date().toISOString() }, null, 2));
|
|
58
|
+
updatePipelineGate(db, state.current_gate);
|
|
88
59
|
}
|
|
89
60
|
|
|
90
61
|
// ==============================
|
|
91
62
|
// TOOLS
|
|
92
63
|
// ==============================
|
|
93
64
|
|
|
94
|
-
// --- Session management ---
|
|
95
|
-
server.tool(
|
|
96
|
-
'
|
|
97
|
-
'【多会话安全】注册当前会话。第一个注册的会话获得 leader 写锁,后续会话为 observer(只读)。返回 session_id 和角色。',
|
|
98
|
-
{ platform: z.enum(['claude','opencode','codex','other']).optional().describe('平台名称') },
|
|
65
|
+
// --- Session management (SQLite) ---
|
|
66
|
+
server.tool('session_join', '注册会话。第一个=leader🔑,后续=observer👁。SQLite持久化,引擎重启不丢。',
|
|
67
|
+
{ platform: z.enum(['claude','opencode','codex','other']).optional() },
|
|
99
68
|
async ({ platform }, extra) => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
sessions.set(sessionId, s);
|
|
108
|
-
if (role === 'leader') leaderSessionId = sessionId;
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
112
|
-
session_id: sessionId, role, leader: leaderSessionId,
|
|
113
|
-
active_sessions: sessions.size,
|
|
114
|
-
message: role === 'leader' ? '🔑 You are leader — write access granted.' : '👁 You are observer — read-only. Leader holds write lock.',
|
|
115
|
-
}, null, 2) }],
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
server.tool('session_heartbeat', '【多会话安全】发送心跳,保持会话活跃。每 60 秒至少发一次,否则会话超时被清理。', {},
|
|
121
|
-
async (_args, extra) => {
|
|
122
|
-
const sid = extra?.sessionId;
|
|
123
|
-
if (!sid || !sessions.has(sid)) return { content: [{ type: 'text', text: JSON.stringify({ error: 'Session not found. Call session_join first.' }) }] };
|
|
124
|
-
sessions.get(sid).last_heartbeat = Date.now();
|
|
125
|
-
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, session_id: sid, role: sessions.get(sid).role, leader: leaderSessionId }) }] };
|
|
126
|
-
}
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
server.tool('session_list', '【多会话安全】列出所有活跃会话及其角色。', {},
|
|
130
|
-
async () => {
|
|
131
|
-
cleanupStaleSessions();
|
|
132
|
-
const list = [...sessions.values()].map(s => ({ session_id: s.id, platform: s.platform, role: s.role, leader: s.id === leaderSessionId, last_heartbeat_ago: `${Math.round((Date.now() - s.last_heartbeat) / 1000)}s` }));
|
|
133
|
-
return { content: [{ type: 'text', text: JSON.stringify({ active_sessions: sessions.size, leader_session: leaderSessionId, sessions: list }) }] };
|
|
134
|
-
}
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
server.tool('session_leave', '【多会话安全】主动离开。如果是 leader,锁自动移交给最老的 observer。', {},
|
|
138
|
-
async (_args, extra) => {
|
|
139
|
-
const sid = extra?.sessionId;
|
|
140
|
-
if (!sid || !sessions.has(sid)) return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'Session not registered.' }) }] };
|
|
141
|
-
const wasLeader = sessions.get(sid).role === 'leader';
|
|
142
|
-
sessions.delete(sid);
|
|
143
|
-
if (wasLeader) {
|
|
144
|
-
leaderSessionId = null;
|
|
145
|
-
const oldest = [...sessions.values()].sort((a, b) => a.created_at - b.created_at)[0];
|
|
146
|
-
if (oldest) { leaderSessionId = oldest.id; oldest.role = 'leader'; }
|
|
147
|
-
}
|
|
148
|
-
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'Session left.', lock_transferred: wasLeader && leaderSessionId ? `Lock → ${leaderSessionId}` : 'No active sessions' }) }] };
|
|
69
|
+
const sid = extra?.sessionId || `s${Date.now()}`;
|
|
70
|
+
const existing = getSession(db, sid);
|
|
71
|
+
if (existing) { heartbeatSession(db, sid); const leader = getLeader(db); return { content: [{ type: 'text', text: JSON.stringify({ session_id: sid, role: existing.role, leader: leader?.id, active_sessions: getSessions(db).length }) }] }; }
|
|
72
|
+
const role = getSessions(db).length === 0 ? 'leader' : 'observer';
|
|
73
|
+
addSession(db, sid, platform || 'unknown', role);
|
|
74
|
+
const leader = getLeader(db);
|
|
75
|
+
return { content: [{ type: 'text', text: JSON.stringify({ session_id: sid, role, leader: leader?.id, active_sessions: getSessions(db).length, message: role==='leader'?'🔑 Leader — write access granted.':'👁 Observer — read-only.' }) }] };
|
|
149
76
|
}
|
|
150
77
|
);
|
|
78
|
+
server.tool('session_heartbeat', '心跳保活。60s一次,超时自动清理。', {}, async (_args, extra) => {
|
|
79
|
+
const sid = extra?.sessionId; if (!sid || !getSession(db, sid)) return { content: [{ type: 'text', text: JSON.stringify({ error: 'Session not found.' }) }] };
|
|
80
|
+
heartbeatSession(db, sid); return { content: [{ type: 'text', text: JSON.stringify({ ok: true, session_id: sid }) }] };
|
|
81
|
+
});
|
|
82
|
+
server.tool('session_list', '列出所有活跃会话。', {}, async () => {
|
|
83
|
+
cleanupStaleSessions(db, SESSION_TIMEOUT); electLeader();
|
|
84
|
+
const leader = getLeader(db);
|
|
85
|
+
const list = getSessions(db).map(s => ({ session_id: s.id, platform: s.platform, role: s.role, leader: s.id===leader?.id, last_heartbeat_ago: `${Math.round((Date.now()-s.last_heartbeat)/1000)}s` }));
|
|
86
|
+
return { content: [{ type: 'text', text: JSON.stringify({ active_sessions: list.length, leader_session: leader?.id, sessions: list }) }] };
|
|
87
|
+
});
|
|
88
|
+
server.tool('session_leave', '主动离开。leader离开自动移交锁。', {}, async (_args, extra) => {
|
|
89
|
+
const sid = extra?.sessionId; if (!sid || !getSession(db, sid)) return { content: [{ type: 'text', text: JSON.stringify({ ok: true }) }] };
|
|
90
|
+
const wasLeader = getSession(db, sid)?.role === 'leader';
|
|
91
|
+
removeSession(db, sid);
|
|
92
|
+
const newLeader = wasLeader ? electLeader() : null;
|
|
93
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, lock_transferred: newLeader ? `→ ${newLeader}` : null }) }] };
|
|
94
|
+
});
|
|
151
95
|
|
|
152
96
|
// --- Pipeline management ---
|
|
153
97
|
|
|
154
|
-
// Tool: pipeline_init —
|
|
155
|
-
server.tool(
|
|
156
|
-
|
|
157
|
-
'【硬约束·需Leader】初始化流水线状态机。只有 leader 会话可调用。observer 会被拒绝。',
|
|
158
|
-
{ project_name: z.string().optional().describe('项目名称(可选)') },
|
|
98
|
+
// Tool: pipeline_init — DB-backed
|
|
99
|
+
server.tool('pipeline_init', '【硬约束·需Leader】初始化流水线。SQLite持久化。',
|
|
100
|
+
{ project_name: z.string().optional() },
|
|
159
101
|
async ({ project_name }, extra) => {
|
|
160
102
|
const lockErr = requireLeader(extra?.sessionId); if (lockErr) return { content: [{ type: 'text', text: JSON.stringify(lockErr) }] };
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
writePipeline(state);
|
|
165
|
-
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'Pipeline initialized — hard state machine active. Next: Gate A', state, session: extra?.sessionId, role: 'leader' }, null, 2) }] };
|
|
103
|
+
dbInitPipeline(db, project_name || root, extra?.sessionId);
|
|
104
|
+
const state = readPipeline();
|
|
105
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'Pipeline initialized (SQLite). Next: Gate A', state }) }] };
|
|
166
106
|
}
|
|
167
107
|
);
|
|
168
108
|
|
|
@@ -174,11 +114,12 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
174
114
|
async () => {
|
|
175
115
|
const pstate = readPipeline();
|
|
176
116
|
const gates = GATES.map(gate => {
|
|
177
|
-
const checkpoints =
|
|
117
|
+
const checkpoints = readCheckpointsDb( gate);
|
|
178
118
|
return { gate, passed: checkpoints.length > 0, checkpoints, artifacts: findGateArtifacts(join(root, 'docs'), gate), requirement: GATE_CHECKS[gate]?.check || '' };
|
|
179
119
|
});
|
|
180
120
|
const current = pstate?.current_gate || (gates.find(g => !g.passed)?.gate || 'Gate A');
|
|
181
|
-
const
|
|
121
|
+
const allSessions = getSessions(db); const leader = getLeader(db);
|
|
122
|
+
const sessionInfo = { active_sessions: allSessions.length, leader: leader?.id, sessions: allSessions.map(s => ({ id: s.id, role: s.role, platform: s.platform, alive_s: Math.round((Date.now()-s.last_heartbeat)/1000) })) };
|
|
182
123
|
return {
|
|
183
124
|
content: [{ type: 'text', text: JSON.stringify({
|
|
184
125
|
project: root,
|
|
@@ -202,7 +143,7 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
202
143
|
const pstate = readPipeline();
|
|
203
144
|
const targetGate = gate || pstate?.current_gate || 'Gate A';
|
|
204
145
|
const artifacts = findGateArtifacts(join(root, 'docs'), targetGate);
|
|
205
|
-
const checkpoints =
|
|
146
|
+
const checkpoints = readCheckpointsDb( targetGate);
|
|
206
147
|
const requirement = GATE_CHECKS[targetGate];
|
|
207
148
|
|
|
208
149
|
// Hard check: must have artifacts and/or checkpoints
|
|
@@ -251,16 +192,14 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
251
192
|
|
|
252
193
|
// Enforce: must pass current gate
|
|
253
194
|
const artifacts = findGateArtifacts(join(root, 'docs'), currentGate);
|
|
254
|
-
const checkpoints =
|
|
195
|
+
const checkpoints = readCheckpointsDb( currentGate);
|
|
255
196
|
if (artifacts.length === 0 && checkpoints.length === 0) {
|
|
256
197
|
return { content: [{ type: 'text', text: JSON.stringify({ allowed: false, error: `FSM blocked: ${currentGate} conditions NOT met. Run gate_enforce first. Required: ${GATE_CHECKS[currentGate]?.check}` }) }] };
|
|
257
198
|
}
|
|
258
199
|
|
|
259
|
-
// Allowed — advance
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
writeFileSync(join(cpDir, `${currentGate.replace(/ /g, '_')}.json`), JSON.stringify({ gate: currentGate, passed_at: new Date().toISOString(), advance_to: gate }, null, 2));
|
|
263
|
-
writePipeline({ ...pstate, current_gate: gate, gates_passed: [...(pstate?.gates_passed || []), currentGate] });
|
|
200
|
+
// Allowed — advance (SQLite)
|
|
201
|
+
addCheckpoint(db, currentGate, gate, extra?.sessionId);
|
|
202
|
+
updatePipelineGate(db, gate);
|
|
264
203
|
|
|
265
204
|
const nextGate = GATES[targetIdx + 1];
|
|
266
205
|
return {
|
|
@@ -282,7 +221,7 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
282
221
|
async () => {
|
|
283
222
|
const gates = GATES.map(gate => ({
|
|
284
223
|
gate,
|
|
285
|
-
passed:
|
|
224
|
+
passed: readCheckpointsDb( gate).length > 0,
|
|
286
225
|
artifacts: findGateArtifacts(join(root, 'docs'), gate),
|
|
287
226
|
}));
|
|
288
227
|
const completed = gates.filter(g => g.passed).length;
|
|
@@ -292,7 +231,7 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
292
231
|
if (gate.passed) {
|
|
293
232
|
reports[gate.gate] = {
|
|
294
233
|
artifacts: gate.artifacts,
|
|
295
|
-
checkpoints:
|
|
234
|
+
checkpoints: readCheckpointsDb( gate.gate),
|
|
296
235
|
};
|
|
297
236
|
}
|
|
298
237
|
}
|
|
@@ -350,7 +289,7 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
350
289
|
}, async (uri) => {
|
|
351
290
|
const gate = decodeURIComponent(uri.pathname.split('/').pop()).replace(/_/g, ' ');
|
|
352
291
|
const artifacts = findGateArtifacts(join(root, 'docs'), gate);
|
|
353
|
-
const checkpoints =
|
|
292
|
+
const checkpoints = readCheckpointsDb( gate);
|
|
354
293
|
const text = `# ${gate} Report\n\n**Passed:** ${checkpoints.length > 0}\n**Checkpoints:** ${checkpoints.map(c => c.passed_at).join(', ') || 'none'}\n\n**Artifacts:**\n${artifacts.map(a => `- ${a}`).join('\n') || 'none'}`;
|
|
355
294
|
return { contents: [{ uri: uri.href, text, mimeType: 'text/markdown' }] };
|
|
356
295
|
});
|
|
@@ -366,16 +305,72 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
366
305
|
// ---- Health ----
|
|
367
306
|
app.get('/health', (_req, res) => res.json({ status: 'ok', version: readPkgVersion(), tools: ['pipeline_init', 'pipeline_status', 'gate_enforce', 'advance_gate', 'report_status'] }));
|
|
368
307
|
|
|
369
|
-
// ----
|
|
308
|
+
// ---- Agent Model Config (SQLite) ----
|
|
309
|
+
const AVAILABLE_MODELS = [
|
|
310
|
+
'deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek/deepseek-v4-pro', 'deepseek/deepseek-v4-flash',
|
|
311
|
+
'gpt-5.5', 'gpt-5.4', 'gpt-5.3-codex', 'gpt-5.3-codex-spark', 'gpt-5.4-mini', 'gpt-5.2',
|
|
312
|
+
'claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5',
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const AGENT_LIST = [
|
|
316
|
+
{ id:'jarvis', name:'Jarvis', role:'编排中枢', icon:'brain', defaultModel:'deepseek-v4-pro' },
|
|
317
|
+
{ id:'frontend-implementer', name:'Frontend', role:'前端全栈', icon:'layout', defaultModel:'deepseek-v4-pro' },
|
|
318
|
+
{ id:'frontend-ui-worker', name:'UI Worker', role:'UI/样式', icon:'palette', defaultModel:'deepseek-v4-flash' },
|
|
319
|
+
{ id:'frontend-state-worker', name:'State Worker', role:'状态/数据', icon:'database', defaultModel:'deepseek-v4-flash' },
|
|
320
|
+
{ id:'frontend-test-worker', name:'Frontend Test', role:'前端测试', icon:'test', defaultModel:'deepseek-v4-flash' },
|
|
321
|
+
{ id:'backend-implementer', name:'Backend', role:'后端全栈', icon:'server', defaultModel:'deepseek-v4-pro' },
|
|
322
|
+
{ id:'backend-api-worker', name:'API Worker', role:'API/路由', icon:'route', defaultModel:'deepseek-v4-flash' },
|
|
323
|
+
{ id:'backend-service-worker', name:'Service Worker', role:'业务逻辑', icon:'cog', defaultModel:'deepseek-v4-flash' },
|
|
324
|
+
{ id:'backend-data-worker', name:'Data Worker', role:'数据层', icon:'table', defaultModel:'deepseek-v4-flash' },
|
|
325
|
+
{ id:'backend-test-worker', name:'Backend Test', role:'后端测试', icon:'test', defaultModel:'deepseek-v4-flash' },
|
|
326
|
+
{ id:'browser-test-worker', name:'Browser Test', role:'浏览器测试', icon:'globe', defaultModel:'deepseek-v4-flash' },
|
|
327
|
+
{ id:'e2e-test-worker', name:'E2E Test', role:'端到端测试', icon:'play', defaultModel:'deepseek-v4-flash' },
|
|
328
|
+
{ id:'api-docs-worker', name:'API Docs', role:'API文档', icon:'file', defaultModel:'deepseek-v4-flash' },
|
|
329
|
+
{ id:'planner', name:'Planner', role:'执行规划', icon:'map', defaultModel:'deepseek-v4-pro' },
|
|
330
|
+
{ id:'task-design', name:'Task Design', role:'任务分解', icon:'list', defaultModel:'deepseek-v4-pro' },
|
|
331
|
+
{ id:'security-auditor', name:'Security', role:'安全审计', icon:'shield', defaultModel:'deepseek-v4-pro' },
|
|
332
|
+
{ id:'review-qa', name:'Review QA', role:'评审', icon:'eye', defaultModel:'deepseek-v4-pro' },
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
// REST: agent config (SQLite)
|
|
336
|
+
app.get('/api/agents', (_req, res) => {
|
|
337
|
+
const cfg = getAgentConfig(db);
|
|
338
|
+
const list = AGENT_LIST.map(a => ({ ...a, model: cfg[a.id] || a.defaultModel, is_custom: !!cfg[a.id] }));
|
|
339
|
+
res.json({ agents: list, available_models: AVAILABLE_MODELS });
|
|
340
|
+
});
|
|
341
|
+
app.post('/api/agents', (req, res) => {
|
|
342
|
+
const { agent_id, model } = req.body;
|
|
343
|
+
if (!agent_id || !model) return res.status(400).json({ error: 'agent_id and model required' });
|
|
344
|
+
if (!AVAILABLE_MODELS.includes(model)) return res.status(400).json({ error: `Unknown model. Available: ${AVAILABLE_MODELS.join(', ')}` });
|
|
345
|
+
setAgentModel(db, agent_id, model);
|
|
346
|
+
res.json({ ok: true, agent_id, model });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// MCP: agent_config (SQLite)
|
|
350
|
+
server.tool('agent_config', '配置子Agent模型(SQLite持久化)。', {
|
|
351
|
+
agent_id: z.string().optional(), model: z.string().optional(),
|
|
352
|
+
}, async ({ agent_id, model }) => {
|
|
353
|
+
const cfg = getAgentConfig(db);
|
|
354
|
+
if (agent_id && model) {
|
|
355
|
+
if (!AVAILABLE_MODELS.includes(model)) return { content: [{ type: 'text', text: JSON.stringify({ error: `Unknown model. Available: ${AVAILABLE_MODELS.join(', ')}` }) }] };
|
|
356
|
+
setAgentModel(db, agent_id, model);
|
|
357
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, agent_id, model }) }] };
|
|
358
|
+
}
|
|
359
|
+
const list = AGENT_LIST.map(a => ({ id: a.id, name: a.name, role: a.role, model: cfg[a.id] || a.defaultModel, is_custom: !!cfg[a.id] }));
|
|
360
|
+
return { content: [{ type: 'text', text: JSON.stringify({ agents: list, available_models: AVAILABLE_MODELS }) }] };
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ---- SSE ----
|
|
370
364
|
const sseClients = new Set();
|
|
371
365
|
app.get('/api/events', (req, res) => {
|
|
372
366
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
|
373
|
-
sseClients.add(res);
|
|
374
|
-
req.on('close', () => sseClients.delete(res));
|
|
367
|
+
sseClients.add(res); req.on('close', () => sseClients.delete(res));
|
|
375
368
|
});
|
|
376
369
|
setInterval(() => {
|
|
377
370
|
if (sseClients.size === 0) return;
|
|
378
|
-
const
|
|
371
|
+
const checkpoints = getCheckpoints(db);
|
|
372
|
+
const cpGateMap = {}; for (const c of checkpoints) cpGateMap[c.gate] = c;
|
|
373
|
+
const gates = GATES.map(g => ({ gate: g, passed: !!cpGateMap[g], checkpoints: cpGateMap[g] ? [cpGateMap[g]] : [], artifacts: findGateArtifacts(join(root, 'docs'), g), requirement: GATE_CHECKS[g]?.check || '' }));
|
|
379
374
|
const current = gates.find(g => !g.passed)?.gate || 'Complete';
|
|
380
375
|
const completed = gates.filter(g => g.passed).map(g => g.gate);
|
|
381
376
|
const pct = Math.round(completed.length / gates.length * 100);
|
|
@@ -385,8 +380,8 @@ export async function startEngine({ port = DEFAULT_PORT, dashboard = false, proj
|
|
|
385
380
|
|
|
386
381
|
// ---- Dashboard ----
|
|
387
382
|
if (dashboard) {
|
|
388
|
-
|
|
389
|
-
app.get('/
|
|
383
|
+
app.get('/dashboard', (_req, res) => res.type('html').send(readFileSync(resolve(import.meta.dirname, 'dashboard.html'), 'utf-8')));
|
|
384
|
+
app.get('/agents', (_req, res) => res.type('html').send(readFileSync(resolve(import.meta.dirname, 'agents.html'), 'utf-8')));
|
|
390
385
|
}
|
|
391
386
|
|
|
392
387
|
app.listen(port, () => {
|
|
@@ -415,13 +410,7 @@ export function engineStatus() {
|
|
|
415
410
|
}
|
|
416
411
|
|
|
417
412
|
function readPkgVersion() { try { return JSON.parse(readFileSync(resolve(import.meta.dirname, '..', '..', 'package.json'), 'utf-8')).version; } catch { return '?.?.?'; } }
|
|
418
|
-
|
|
419
|
-
function readCheckpoints(root, gate) {
|
|
420
|
-
const cpDir = join(root, '.jarvis', 'checkpoints');
|
|
421
|
-
if (!existsSync(cpDir)) return [];
|
|
422
|
-
const files = readdirSync(cpDir).filter(f => f.includes(gate.replace(/ /g, '_')));
|
|
423
|
-
return files.map(f => { try { return JSON.parse(readFileSync(join(cpDir, f), 'utf-8')); } catch { return null; } }).filter(Boolean);
|
|
424
|
-
}
|
|
413
|
+
function readCheckpointsDb(gate) { return getCheckpoints(db, gate); }
|
|
425
414
|
|
|
426
415
|
function findGateArtifacts(docsDir, gate) {
|
|
427
416
|
const subdir = GATE_DIRS[gate]; if (!subdir) return [];
|