jarvis-agent-factory 2.1.4 → 3.0.0-alpha
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/.claude/agents/api-docs-worker.md +17 -5
- package/README.md +3 -2
- package/package.json +5 -1
- package/src/cli.js +20 -1
- package/src/engine/server.js +223 -0
|
@@ -12,19 +12,31 @@ effort: high
|
|
|
12
12
|
|
|
13
13
|
**每个涉及后端 API 变更的任务必须执行**,验证"文档不撒谎"。
|
|
14
14
|
|
|
15
|
+
**"已有文档"指什么**:现代后端框架大多支持从代码注解/装饰器自动生成 OpenAPI spec。例如 FastAPI 的 Pydantic 模型 + `/openapi.json`、NestJS 的 `@ApiProperty` 装饰器 + swagger 插件、Spring Boot 的 springdoc、Go 的 swaggo 注解、Express 的 swagger-jsdoc。**验证就是拿这份自动生成的 spec,去对比实际的 route/controller 实现代码**,检查注解有没有过时、漏写或写错。
|
|
16
|
+
|
|
15
17
|
职责:
|
|
16
|
-
- 对比 API
|
|
18
|
+
- 对比 API 实现代码(路由/控制器)与自动生成的 OpenAPI/Swagger spec
|
|
17
19
|
- 检查路径、方法、参数、响应 schema 是否一致
|
|
18
|
-
-
|
|
20
|
+
- 标记漂移项:注解改了但 spec 没重新生成、实现改了注解没改、breaking change 未标注
|
|
19
21
|
- 输出契约一致性验证报告
|
|
20
22
|
|
|
21
23
|
执行流程:
|
|
22
|
-
1. 读取 API 路由实现代码(controller/router
|
|
23
|
-
2.
|
|
24
|
+
1. 读取 API 路由实现代码(controller/router 文件 + 类型/DTO 定义)
|
|
25
|
+
2. 定位项目的 OpenAPI spec 来源(`/openapi.json` 端点、`swagger.yaml` 文件、`@nestjs/swagger` 插件输出等)
|
|
24
26
|
3. 逐端点对比:路径、HTTP 方法、参数名/类型/必填、响应 status/schema
|
|
25
|
-
4. 标记每条端点的状态:✅ 一致 / ⚠
|
|
27
|
+
4. 标记每条端点的状态:✅ 一致 / ⚠ spec 过时(代码改了文档没更新)/ ❌ 未文档化(缺少注解)/ 🔴 breaking change
|
|
26
28
|
5. 输出 `docs/testing/YYYY-MM-DD-<topic>-api-contract-report.md`
|
|
27
29
|
|
|
30
|
+
**常见框架的 spec 来源**:
|
|
31
|
+
| 框架 | 自动生成机制 | 获取方式 |
|
|
32
|
+
|------|-------------|---------|
|
|
33
|
+
| FastAPI | Pydantic 模型 → OpenAPI | `GET /openapi.json` |
|
|
34
|
+
| NestJS | `@nestjs/swagger` 装饰器 | SwaggerModule 生成的 `/api-json` |
|
|
35
|
+
| Spring Boot | springdoc-openapi 注解 | `/v3/api-docs` |
|
|
36
|
+
| Express | swagger-jsdoc 注释 | 构建输出的 `swagger.json` |
|
|
37
|
+
| Go (swaggo) | 代码注释 → `swag init` | `docs/swagger.json` |
|
|
38
|
+
| Django | drf-spectacular | `GET /api/schema/` |
|
|
39
|
+
|
|
28
40
|
**红线**:不编写 API 实现代码、不修改路由、不凭记忆对比。
|
|
29
41
|
|
|
30
42
|
## 模式 B:手写 API 参考文档(按需触发)
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Jarvis Agent Factory · 贾维斯智能体工厂
|
|
2
2
|
|
|
3
3
|
[](./LICENSE)
|
|
4
|
-
[](https://gitee.com/wujl1124/JarvisAgentFactory/releases)
|
|
5
5
|
<br>**简体中文** | [English](./README_EN.md)
|
|
6
6
|
|
|
7
7
|
一套跨平台的多智能体(Multi-Agent)AI 编程助手配置集,定义了一条**从想法到交付的完整软件开发流水线**。支持 Claude Code、OpenCode、Codex 三平台,共享同一套工作流规范与技能体系。
|
|
8
8
|
|
|
9
|
-
> **v2.1.
|
|
9
|
+
> **v2.1.5** — Claude Code 47 agents + 15 commands / OpenCode 55 agents(纯智能体切换) / Codex 45 agents + 42 skills(Skill 触发)
|
|
10
10
|
|
|
11
11
|
## 核心概念
|
|
12
12
|
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
| 3 | **Bug 闭环** | `bug-fix` | Bug → agent-browser 复现 → 定位根因 → 修复 → 验证 |
|
|
45
45
|
| 4 | **审查闭环** | `review-fix-optimize` | 初审 → 规划 → 执行 → 验证 → 复审关闭 |
|
|
46
46
|
| 5 | **安全闭环** | Gate E | security-auditor → 威胁建模 + CVE + SAST → 修复 → 重扫 |
|
|
47
|
+
| 6 | **契约闭环** | Gate C2(API 变更强制) | api-docs-worker 模式A → 对比 auto-generated spec vs 代码实现 → 标记漂移 |
|
|
47
48
|
|
|
48
49
|
失败自动路由到修复闭环,最多 2 轮;第 3 轮仍失败标记 BLOCKED 并保留产物。
|
|
49
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jarvis-agent-factory",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-alpha",
|
|
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",
|
|
@@ -41,5 +41,9 @@
|
|
|
41
41
|
],
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
|
+
"express": "^5.2.1"
|
|
44
48
|
}
|
|
45
49
|
}
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createInterface } from 'node:readline';
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { install } from './install.js';
|
|
8
8
|
import { doctor } from './doctor.js';
|
|
9
|
+
import { startEngine, stopEngine, engineStatus } from './engine/server.js';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
@@ -32,7 +33,10 @@ Usage:
|
|
|
32
33
|
jarvis add <p...> [path] Add platform(s) to project
|
|
33
34
|
jarvis remove <p...> [path] Remove platform(s) from project
|
|
34
35
|
jarvis upgrade [path] Upgrade to latest config version
|
|
35
|
-
jarvis
|
|
36
|
+
jarvis engine start [--dashboard] [--port=N] Start MCP orchestration server
|
|
37
|
+
jarvis engine stop Stop engine
|
|
38
|
+
jarvis engine status Engine status
|
|
39
|
+
jarvis doctor [path] Verify installation
|
|
36
40
|
|
|
37
41
|
Options:
|
|
38
42
|
-g, --global Target user global directory instead of project
|
|
@@ -206,6 +210,21 @@ export async function run() {
|
|
|
206
210
|
break;
|
|
207
211
|
}
|
|
208
212
|
|
|
213
|
+
case 'engine': {
|
|
214
|
+
const sub = positional[1];
|
|
215
|
+
if (sub === 'start') {
|
|
216
|
+
const port = parseInt(positional.find(a => a.startsWith('--port='))?.split('=')[1] || '3456');
|
|
217
|
+
const dashboard = positional.includes('--dashboard') || positional.includes('-d');
|
|
218
|
+
await startEngine({ port, dashboard, projectRoot: positional.find(a => !a.startsWith('-') && a !== 'start' && a !== 'engine') || '.' });
|
|
219
|
+
} else if (sub === 'stop') {
|
|
220
|
+
stopEngine();
|
|
221
|
+
} else if (sub === 'status') {
|
|
222
|
+
engineStatus();
|
|
223
|
+
} else {
|
|
224
|
+
console.log('\nUsage: jarvis engine <start|stop|status> [--dashboard] [--port=<N>]\n');
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
209
228
|
case 'doctor':
|
|
210
229
|
case 'check': {
|
|
211
230
|
const path = positional[1];
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { resolve, join } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Jarvis Engine — HTTP MCP Server
|
|
11
|
+
* Phase 1: pipeline_status tool + requirements resource
|
|
12
|
+
*
|
|
13
|
+
* Start: jarvis engine start [--port 3456] [--dashboard]
|
|
14
|
+
* Stop: jarvis engine stop
|
|
15
|
+
* Status: jarvis engine status
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const PID_FILE = resolve(homedir(), '.jarvis', 'engine.pid');
|
|
19
|
+
const DEFAULT_PORT = 3456;
|
|
20
|
+
|
|
21
|
+
export async function startEngine({ port = DEFAULT_PORT, dashboard = false, projectRoot = '.' } = {}) {
|
|
22
|
+
const root = resolve(projectRoot);
|
|
23
|
+
|
|
24
|
+
// Save PID
|
|
25
|
+
const pidDir = resolve(homedir(), '.jarvis');
|
|
26
|
+
if (!existsSync(pidDir)) {
|
|
27
|
+
const { mkdirSync } = await import('node:fs');
|
|
28
|
+
mkdirSync(pidDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
31
|
+
|
|
32
|
+
const app = express();
|
|
33
|
+
app.use(express.json());
|
|
34
|
+
|
|
35
|
+
// ---- MCP Server ----
|
|
36
|
+
const server = new McpServer({
|
|
37
|
+
name: 'jarvis-engine',
|
|
38
|
+
version: readPkgVersion(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Tool: pipeline_status
|
|
42
|
+
server.tool(
|
|
43
|
+
'pipeline_status',
|
|
44
|
+
'读取当前项目的流水线状态:当前 Gate、已通过 Gate、产物文件列表',
|
|
45
|
+
{},
|
|
46
|
+
async () => {
|
|
47
|
+
const gates = ['Gate A', 'Gate B', 'Gate C', 'Gate C1', 'Gate C1.5', 'Gate C2', 'Gate D', 'Gate E'];
|
|
48
|
+
const status = [];
|
|
49
|
+
const docsDir = join(root, 'docs');
|
|
50
|
+
|
|
51
|
+
for (const gate of gates) {
|
|
52
|
+
const results = findGateArtifacts(docsDir, gate);
|
|
53
|
+
status.push({ gate, complete: results.length > 0, artifacts: results });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const currentGate = status.find(g => !g.complete)?.gate || 'Gate E';
|
|
57
|
+
const completedGates = status.filter(g => g.complete).map(g => g.gate);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
61
|
+
project: root,
|
|
62
|
+
current_gate: currentGate,
|
|
63
|
+
completed_gates: completedGates,
|
|
64
|
+
gates: status,
|
|
65
|
+
}, null, 2) }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Resource: requirements://list
|
|
71
|
+
server.resource(
|
|
72
|
+
'requirements_list',
|
|
73
|
+
'requirements://list',
|
|
74
|
+
{
|
|
75
|
+
name: 'Requirements List',
|
|
76
|
+
description: '列出项目中所有 REQ-XXX 需求文档',
|
|
77
|
+
mimeType: 'text/markdown',
|
|
78
|
+
},
|
|
79
|
+
async () => {
|
|
80
|
+
const reqsDir = join(root, 'docs', 'requirements');
|
|
81
|
+
if (!existsSync(reqsDir)) {
|
|
82
|
+
return { contents: [{ uri: 'requirements://list', text: '# No requirements found\n\nRun Gate A first.', mimeType: 'text/markdown' }] };
|
|
83
|
+
}
|
|
84
|
+
const files = readdirSync(reqsDir).filter(f => f.endsWith('.md'));
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
return { contents: [{ uri: 'requirements://list', text: '# No requirements yet\n\nNo REQ documents found.', mimeType: 'text/markdown' }] };
|
|
87
|
+
}
|
|
88
|
+
const list = files.map(f => {
|
|
89
|
+
const content = readFileSync(join(reqsDir, f), 'utf-8');
|
|
90
|
+
const reqMatch = content.match(/REQ-\d{3}/g);
|
|
91
|
+
const reqs = reqMatch ? [...new Set(reqMatch)] : [];
|
|
92
|
+
return `- **${f}** — ${reqs.join(', ') || 'no REQ found'}`;
|
|
93
|
+
}).join('\n');
|
|
94
|
+
return { contents: [{ uri: 'requirements://list', text: `# Requirements\n\n${list}`, mimeType: 'text/markdown' }] };
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Resource: requirement detail
|
|
99
|
+
server.resource(
|
|
100
|
+
'requirement_detail',
|
|
101
|
+
'requirements://{reqId}',
|
|
102
|
+
{
|
|
103
|
+
name: 'Requirement Detail',
|
|
104
|
+
description: '读取指定 REQ-XXX 的完整需求文档内容',
|
|
105
|
+
mimeType: 'text/markdown',
|
|
106
|
+
},
|
|
107
|
+
async (uri) => {
|
|
108
|
+
const reqId = uri.pathname.replace('/requirements/', '').replace('/', '');
|
|
109
|
+
const reqsDir = join(root, 'docs', 'requirements');
|
|
110
|
+
if (!existsSync(reqsDir)) return { contents: [{ uri: uri.href, text: `# ${reqId} — Not Found`, mimeType: 'text/markdown' }] };
|
|
111
|
+
|
|
112
|
+
const files = readdirSync(reqsDir).filter(f => f.endsWith('.md'));
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
const content = readFileSync(join(reqsDir, f), 'utf-8');
|
|
115
|
+
if (content.includes(reqId)) {
|
|
116
|
+
return { contents: [{ uri: uri.href, text: content, mimeType: 'text/markdown' }] };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { contents: [{ uri: uri.href, text: `# ${reqId} — Not Found`, mimeType: 'text/markdown' }] };
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ---- MCP Transport ----
|
|
124
|
+
const transport = new StreamableHTTPServerTransport({
|
|
125
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
app.post('/mcp', async (req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
await transport.handleRequest(req, res, req.body);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error('MCP error:', e.message);
|
|
133
|
+
res.status(500).json({ error: e.message });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
app.get('/mcp/sse', async (req, res) => {
|
|
138
|
+
await transport.handleRequest(req, res, undefined);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await server.connect(transport);
|
|
142
|
+
|
|
143
|
+
// ---- Health ----
|
|
144
|
+
app.get('/health', (_req, res) => res.json({ status: 'ok', version: readPkgVersion() }));
|
|
145
|
+
|
|
146
|
+
// ---- Dashboard (placeholder) ----
|
|
147
|
+
if (dashboard) {
|
|
148
|
+
app.get('/dashboard', (_req, res) => {
|
|
149
|
+
res.send(`<!DOCTYPE html><html><head><title>Jarvis Engine</title>
|
|
150
|
+
<style>body{font-family:system-ui;max-width:800px;margin:40px auto;padding:20px;background:#111;color:#eee}
|
|
151
|
+
h1{color:#FF6B35}.card{background:#1a1a2e;border-radius:8px;padding:16px;margin:8px 0}</style></head>
|
|
152
|
+
<body><h1>🧠 Jarvis Engine v${readPkgVersion()}</h1>
|
|
153
|
+
<div class="card"><h3>MCP Endpoint</h3><code>POST /mcp</code> · <code>GET /mcp/sse</code></div>
|
|
154
|
+
<div class="card"><h3>Tools</h3><p>pipeline_status — 流水线状态</p></div>
|
|
155
|
+
<div class="card"><h3>Resources</h3><p>requirements://list · requirements://{reqId}</p></div>
|
|
156
|
+
</body></html>`);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Start server
|
|
161
|
+
app.listen(port, () => {
|
|
162
|
+
console.log(`🧠 Jarvis Engine v${readPkgVersion()} — http://localhost:${port}`);
|
|
163
|
+
console.log(` MCP: POST http://localhost:${port}/mcp`);
|
|
164
|
+
if (dashboard) console.log(` Web: http://localhost:${port}/dashboard`);
|
|
165
|
+
console.log(` PID: ${process.pid} (saved to ${PID_FILE})`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function stopEngine() {
|
|
170
|
+
if (!existsSync(PID_FILE)) {
|
|
171
|
+
console.log('No running engine found.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const pid = readFileSync(PID_FILE, 'utf-8').trim();
|
|
175
|
+
try {
|
|
176
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
177
|
+
const { unlinkSync } = require('node:fs');
|
|
178
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
179
|
+
console.log(`Engine stopped (PID ${pid}).`);
|
|
180
|
+
} catch {
|
|
181
|
+
console.log(`Engine not running (stale PID ${pid}).`);
|
|
182
|
+
try { require('node:fs').unlinkSync(PID_FILE); } catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function engineStatus() {
|
|
187
|
+
if (!existsSync(PID_FILE)) {
|
|
188
|
+
console.log('Engine: not running');
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const pid = readFileSync(PID_FILE, 'utf-8').trim();
|
|
192
|
+
try { process.kill(Number(pid), 0); } catch {
|
|
193
|
+
console.log(`Engine: not running (stale PID ${pid})`);
|
|
194
|
+
try { require('node:fs').unlinkSync(PID_FILE); } catch {}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
console.log(`Engine: running (PID ${pid})`);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readPkgVersion() {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(readFileSync(resolve(import.meta.dirname, '..', '..', 'package.json'), 'utf-8')).version;
|
|
204
|
+
} catch { return '?.?.?'; }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function findGateArtifacts(docsDir, gate) {
|
|
208
|
+
const gateMap = {
|
|
209
|
+
'Gate A': 'requirements',
|
|
210
|
+
'Gate B': 'tasks',
|
|
211
|
+
'Gate C': 'plans',
|
|
212
|
+
'Gate C1': 'implementation',
|
|
213
|
+
'Gate C1.5': 'implementation',
|
|
214
|
+
'Gate C2': 'testing',
|
|
215
|
+
'Gate D': 'review',
|
|
216
|
+
'Gate E': 'shipping',
|
|
217
|
+
};
|
|
218
|
+
const subdir = gateMap[gate];
|
|
219
|
+
if (!subdir) return [];
|
|
220
|
+
const dir = join(docsDir, subdir);
|
|
221
|
+
if (!existsSync(dir)) return [];
|
|
222
|
+
return readdirSync(dir).filter(f => f.endsWith('.md')).slice(0, 5);
|
|
223
|
+
}
|