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.
@@ -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 实现代码(路由/控制器)与已有 OpenAPI/Swagger 文档
18
+ - 对比 API 实现代码(路由/控制器)与自动生成的 OpenAPI/Swagger spec
17
19
  - 检查路径、方法、参数、响应 schema 是否一致
18
- - 标记漂移项(文档未更新、实现未文档化、breaking change 未标注)
20
+ - 标记漂移项:注解改了但 spec 没重新生成、实现改了注解没改、breaking change 未标注
19
21
  - 输出契约一致性验证报告
20
22
 
21
23
  执行流程:
22
- 1. 读取 API 路由实现代码(controller/router 文件)
23
- 2. 读取已有 OpenAPI/Swagger 文档(如有)
24
+ 1. 读取 API 路由实现代码(controller/router 文件 + 类型/DTO 定义)
25
+ 2. 定位项目的 OpenAPI spec 来源(`/openapi.json` 端点、`swagger.yaml` 文件、`@nestjs/swagger` 插件输出等)
24
26
  3. 逐端点对比:路径、HTTP 方法、参数名/类型/必填、响应 status/schema
25
- 4. 标记每条端点的状态:✅ 一致 / ⚠ 文档过时 /未文档化 / 🔴 breaking change
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: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
4
- [![Version](https://img.shields.io/badge/version-v2.1.0-green)](https://gitee.com/wujl1124/JarvisAgentFactory/releases)
4
+ [![Version](https://img.shields.io/badge/version-v2.1.5-green)](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.0** — Claude Code 47 agents + 15 commands / OpenCode 55 agents(纯智能体切换) / Codex 45 agents + 42 skills(Skill 触发)
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": "2.1.4",
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 doctor [path] Verify installation
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
+ }