@aicoffe/ai-native-sdlc-mcp 0.1.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/README.md +40 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +25 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +21 -0
- package/dist/tools/workflow-tools.d.ts +14 -0
- package/dist/tools/workflow-tools.js +171 -0
- package/dist/workflow/definition.d.ts +2 -0
- package/dist/workflow/definition.js +252 -0
- package/dist/workflow/event-bus.d.ts +16 -0
- package/dist/workflow/event-bus.js +46 -0
- package/dist/workflow/events.d.ts +12 -0
- package/dist/workflow/events.js +50 -0
- package/dist/workflow/handlers/close-intent.d.ts +2 -0
- package/dist/workflow/handlers/close-intent.js +21 -0
- package/dist/workflow/handlers/contain.d.ts +13 -0
- package/dist/workflow/handlers/contain.js +42 -0
- package/dist/workflow/handlers/create-feature-branch.d.ts +2 -0
- package/dist/workflow/handlers/create-feature-branch.js +25 -0
- package/dist/workflow/handlers/create-pr.d.ts +2 -0
- package/dist/workflow/handlers/create-pr.js +45 -0
- package/dist/workflow/handlers/create-spec-dir.d.ts +2 -0
- package/dist/workflow/handlers/create-spec-dir.js +50 -0
- package/dist/workflow/handlers/exec.d.ts +5 -0
- package/dist/workflow/handlers/exec.js +13 -0
- package/dist/workflow/handlers/fs-utils.d.ts +1 -0
- package/dist/workflow/handlers/fs-utils.js +11 -0
- package/dist/workflow/handlers/gh.d.ts +42 -0
- package/dist/workflow/handlers/gh.js +57 -0
- package/dist/workflow/handlers/git.d.ts +12 -0
- package/dist/workflow/handlers/git.js +59 -0
- package/dist/workflow/handlers/index.d.ts +12 -0
- package/dist/workflow/handlers/index.js +20 -0
- package/dist/workflow/handlers/merge-pr.d.ts +2 -0
- package/dist/workflow/handlers/merge-pr.js +13 -0
- package/dist/workflow/handlers/noop.d.ts +2 -0
- package/dist/workflow/handlers/noop.js +1 -0
- package/dist/workflow/handlers/notify-blocked.d.ts +2 -0
- package/dist/workflow/handlers/notify-blocked.js +12 -0
- package/dist/workflow/handlers/update-current-index.d.ts +2 -0
- package/dist/workflow/handlers/update-current-index.js +73 -0
- package/dist/workflow/ids.d.ts +27 -0
- package/dist/workflow/ids.js +63 -0
- package/dist/workflow/mutex.d.ts +12 -0
- package/dist/workflow/mutex.js +24 -0
- package/dist/workflow/paths.d.ts +6 -0
- package/dist/workflow/paths.js +15 -0
- package/dist/workflow/retry.d.ts +14 -0
- package/dist/workflow/retry.js +52 -0
- package/dist/workflow/state-machine.d.ts +46 -0
- package/dist/workflow/state-machine.js +313 -0
- package/dist/workflow/store.d.ts +30 -0
- package/dist/workflow/store.js +95 -0
- package/dist/workflow/template.d.ts +4 -0
- package/dist/workflow/template.js +18 -0
- package/dist/workflow/types.d.ts +102 -0
- package/dist/workflow/types.js +3 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ai-native-sdlc-mcp
|
|
2
|
+
|
|
3
|
+
MCP workflow state-machine server for the AI-native SDLC flow — harness-agnostic, runs over stdio.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Input | Returns |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `workflow_start` | `{title, description?}` | Creates a workflow and returns the current stage view (`llm_generate` / `gate_wait` / processing). |
|
|
10
|
+
| `workflow_stage_completed` | `{workflow_id, stage_id, output[]}` | Records the stage output and returns the next stage view. |
|
|
11
|
+
| `workflow_gate_decision` | `{workflow_id, stage_id, decision, changes?, approved_by?}` | Records `approved`/`rejected`/`modified` and returns the next stage view. |
|
|
12
|
+
| `workflow_get_active` | `{}` | Summaries of all running (non-completed/blocked) workflows, for session-restore. |
|
|
13
|
+
| `workflow_get_state` | `{workflow_id}` | Full persisted state of one workflow. |
|
|
14
|
+
| `workflow_get_gate_prompt` | `{workflow_id}` | Current gate's prompt template, checklist, decisions, and collected artifacts. |
|
|
15
|
+
|
|
16
|
+
## State layout (under `REPO_ROOT`)
|
|
17
|
+
|
|
18
|
+
`.workflow/workflows/{id}.json`, `.workflow/index.json`, `.workflow/events/{id}.jsonl`
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"ai-native-sdlc": {
|
|
26
|
+
"command": "node",
|
|
27
|
+
"args": ["<repo>/mcp-server/dist/main.js"],
|
|
28
|
+
"env": { "REPO_ROOT": "<repo>" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Development
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install && npm run build && npm test && npm start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
PR-related stages (`create-pr` / `merge-pr`) require the GitHub `gh` CLI; without it the workflow transitions to `blocked` with a clear error. stdout carries only the MCP protocol; logs go to stderr.
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mcp-server/src/main.ts — thin stdio entry. All wiring lives in server.ts.
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { buildWorkflowServer } from './server.js';
|
|
5
|
+
async function main() {
|
|
6
|
+
const repoRoot = process.env.REPO_ROOT || process.cwd();
|
|
7
|
+
const { mcpServer, recover } = buildWorkflowServer(repoRoot);
|
|
8
|
+
await recover();
|
|
9
|
+
await mcpServer.connect(new StdioServerTransport());
|
|
10
|
+
console.error(`ai-native-sdlc-mcp running on stdio (repo: ${repoRoot})`);
|
|
11
|
+
const shutdown = async () => {
|
|
12
|
+
try {
|
|
13
|
+
await mcpServer.close();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
process.on('SIGINT', () => void shutdown());
|
|
20
|
+
process.on('SIGTERM', () => void shutdown());
|
|
21
|
+
}
|
|
22
|
+
main().catch((error) => {
|
|
23
|
+
console.error('ai-native-sdlc-mcp fatal:', error instanceof Error ? error.message : error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { WorkflowEventBus } from './workflow/event-bus.js';
|
|
5
|
+
import { WorkflowStore } from './workflow/store.js';
|
|
6
|
+
import { WorkflowStateMachine } from './workflow/state-machine.js';
|
|
7
|
+
export interface WorkflowServer {
|
|
8
|
+
mcpServer: McpServer;
|
|
9
|
+
store: WorkflowStore;
|
|
10
|
+
bus: WorkflowEventBus;
|
|
11
|
+
machine: WorkflowStateMachine;
|
|
12
|
+
recover: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildWorkflowServer(repoRoot: string): WorkflowServer;
|
|
15
|
+
export declare function createTestPair(): [InMemoryTransport, InMemoryTransport];
|
|
16
|
+
export type McpClient = Client;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// mcp-server/src/server.ts
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
4
|
+
import { WORKFLOW_DEFINITION } from './workflow/definition.js';
|
|
5
|
+
import { WorkflowEventBus } from './workflow/event-bus.js';
|
|
6
|
+
import { WorkflowStore } from './workflow/store.js';
|
|
7
|
+
import { WorkflowStateMachine } from './workflow/state-machine.js';
|
|
8
|
+
import { ExecFileGhAdapter } from './workflow/handlers/gh.js';
|
|
9
|
+
import { buildActionHandlers } from './workflow/handlers/index.js';
|
|
10
|
+
import { registerWorkflowTools } from './tools/workflow-tools.js';
|
|
11
|
+
export function buildWorkflowServer(repoRoot) {
|
|
12
|
+
const mcpServer = new McpServer({ name: 'ai-native-sdlc-mcp', version: '0.1.0' });
|
|
13
|
+
const store = new WorkflowStore(repoRoot);
|
|
14
|
+
const bus = new WorkflowEventBus(repoRoot);
|
|
15
|
+
const machine = new WorkflowStateMachine(WORKFLOW_DEFINITION, store, bus, buildActionHandlers({ repoRoot, gh: new ExecFileGhAdapter(repoRoot) }));
|
|
16
|
+
registerWorkflowTools(mcpServer, { store, bus, machine, definition: WORKFLOW_DEFINITION });
|
|
17
|
+
return { mcpServer, store, bus, machine, recover: () => machine.recoverWorkflows() };
|
|
18
|
+
}
|
|
19
|
+
export function createTestPair() {
|
|
20
|
+
return InMemoryTransport.createLinkedPair();
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { WorkflowEventBus } from '../workflow/event-bus.js';
|
|
3
|
+
import { WorkflowStore } from '../workflow/store.js';
|
|
4
|
+
import { WorkflowStateMachine } from '../workflow/state-machine.js';
|
|
5
|
+
import type { WorkflowDefinition, WorkflowState } from '../workflow/types.js';
|
|
6
|
+
export interface WorkflowToolDeps {
|
|
7
|
+
store: WorkflowStore;
|
|
8
|
+
bus: WorkflowEventBus;
|
|
9
|
+
machine: WorkflowStateMachine;
|
|
10
|
+
definition: WorkflowDefinition;
|
|
11
|
+
}
|
|
12
|
+
/** Doc §6 view for a state: llm_generate / gate_wait / processing. */
|
|
13
|
+
export declare function buildToolResponse(state: WorkflowState, machine: WorkflowStateMachine): Record<string, unknown>;
|
|
14
|
+
export declare function registerWorkflowTools(mcpServer: McpServer, deps: WorkflowToolDeps): void;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { KeyedMutex } from '../workflow/mutex.js';
|
|
3
|
+
import { dateCompact, dateIso, generateIds, nextDailyCount } from '../workflow/ids.js';
|
|
4
|
+
import { gateDecisionEvent, stageCompletedEvent, workflowStartedEvent } from '../workflow/events.js';
|
|
5
|
+
/** F1: workflow_id is filesystem-derived — strict pattern, no traversal. */
|
|
6
|
+
const workflowIdSchema = z.string().regex(/^WF-\d{8}-\d{3}$/);
|
|
7
|
+
/** Doc §6 view for a state: llm_generate / gate_wait / processing. */
|
|
8
|
+
export function buildToolResponse(state, machine) {
|
|
9
|
+
return machine.getCurrentStageView(state);
|
|
10
|
+
}
|
|
11
|
+
function ok(payload) {
|
|
12
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
13
|
+
}
|
|
14
|
+
// D11: tools NEVER throw; errors come back as isError JSON.
|
|
15
|
+
function fail(error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// F4: ALL mutating tools serialize on ONE repo-level key — action handlers
|
|
23
|
+
// share the single git working tree, so per-workflow keys would let one
|
|
24
|
+
// workflow's checkout/commit race another's.
|
|
25
|
+
const REPO_MUTEX_KEY = '$repo';
|
|
26
|
+
export function registerWorkflowTools(mcpServer, deps) {
|
|
27
|
+
const { store, bus, machine, definition } = deps;
|
|
28
|
+
const mutex = new KeyedMutex();
|
|
29
|
+
mcpServer.registerTool('workflow_start', {
|
|
30
|
+
description: '启动一个新的工作流。由 /new-intent 命令或用户意图触发。',
|
|
31
|
+
inputSchema: z.object({
|
|
32
|
+
title: z.string(),
|
|
33
|
+
description: z.string().optional(),
|
|
34
|
+
}),
|
|
35
|
+
}, async ({ title, description }) => {
|
|
36
|
+
try {
|
|
37
|
+
const state = await mutex.run(REPO_MUTEX_KEY, async () => {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const states = await store.listAll();
|
|
40
|
+
const count = nextDailyCount(states.map((s) => s.workflow_id), dateCompact(now));
|
|
41
|
+
const ids = generateIds(title, now, count);
|
|
42
|
+
const state = {
|
|
43
|
+
workflow_id: ids.workflow_id,
|
|
44
|
+
intent_id: ids.intent_id,
|
|
45
|
+
title,
|
|
46
|
+
current_stage: definition.initial_stage,
|
|
47
|
+
status: 'running',
|
|
48
|
+
stages: {},
|
|
49
|
+
context: {
|
|
50
|
+
date: dateIso(now),
|
|
51
|
+
slug: ids.slug,
|
|
52
|
+
title,
|
|
53
|
+
domain: 'default',
|
|
54
|
+
...(description !== undefined ? { description } : {}),
|
|
55
|
+
},
|
|
56
|
+
created_at: now.toISOString(),
|
|
57
|
+
last_active: now.toISOString(),
|
|
58
|
+
};
|
|
59
|
+
await store.save(state);
|
|
60
|
+
// Audit trail: every workflow's event log starts with workflow.started.
|
|
61
|
+
await bus.emitWorkflowEvent(workflowStartedEvent(state.workflow_id, title));
|
|
62
|
+
return state;
|
|
63
|
+
});
|
|
64
|
+
return ok(buildToolResponse(state, machine));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return fail(error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
mcpServer.registerTool('workflow_stage_completed', {
|
|
71
|
+
description: '当完成当前阶段的内容生成后调用。通知状态机推进。',
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
workflow_id: workflowIdSchema,
|
|
74
|
+
stage_id: z.string(),
|
|
75
|
+
output: z.array(z.string()),
|
|
76
|
+
}),
|
|
77
|
+
}, async ({ workflow_id, stage_id, output }) => {
|
|
78
|
+
try {
|
|
79
|
+
const state = await mutex.run(REPO_MUTEX_KEY, async () => {
|
|
80
|
+
await bus.emitWorkflowEvent(stageCompletedEvent(workflow_id, stage_id, output));
|
|
81
|
+
const reloaded = await store.load(workflow_id);
|
|
82
|
+
if (!reloaded)
|
|
83
|
+
throw new Error(`Unknown workflow: ${workflow_id}`);
|
|
84
|
+
return reloaded;
|
|
85
|
+
});
|
|
86
|
+
return ok(buildToolResponse(state, machine));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return fail(error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
mcpServer.registerTool('workflow_gate_decision', {
|
|
93
|
+
description: '当用户在 gate 点做出决定后调用。',
|
|
94
|
+
inputSchema: z.object({
|
|
95
|
+
workflow_id: workflowIdSchema,
|
|
96
|
+
stage_id: z.string(),
|
|
97
|
+
decision: z.enum(['approved', 'rejected', 'modified']),
|
|
98
|
+
changes: z.string().optional(),
|
|
99
|
+
approved_by: z.string().optional(),
|
|
100
|
+
}),
|
|
101
|
+
}, async ({ workflow_id, stage_id, decision, changes, approved_by }) => {
|
|
102
|
+
try {
|
|
103
|
+
const state = await mutex.run(REPO_MUTEX_KEY, async () => {
|
|
104
|
+
await bus.emitWorkflowEvent(gateDecisionEvent({
|
|
105
|
+
workflow_id,
|
|
106
|
+
stage_id,
|
|
107
|
+
decision,
|
|
108
|
+
...(changes !== undefined ? { changes } : {}),
|
|
109
|
+
approved_by: approved_by ?? 'unknown',
|
|
110
|
+
}));
|
|
111
|
+
const reloaded = await store.load(workflow_id);
|
|
112
|
+
if (!reloaded)
|
|
113
|
+
throw new Error(`Unknown workflow: ${workflow_id}`);
|
|
114
|
+
return reloaded;
|
|
115
|
+
});
|
|
116
|
+
return ok(buildToolResponse(state, machine));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return fail(error);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
mcpServer.registerTool('workflow_get_active', {
|
|
123
|
+
description: '获取所有进行中的工作流。会话启动时调用,用于恢复上下文。',
|
|
124
|
+
inputSchema: z.object({}),
|
|
125
|
+
}, async () => {
|
|
126
|
+
try {
|
|
127
|
+
return ok({ active: await store.getActiveSummaries() });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return fail(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
mcpServer.registerTool('workflow_get_state', {
|
|
134
|
+
description: '获取指定工作流的完整状态。',
|
|
135
|
+
inputSchema: z.object({ workflow_id: workflowIdSchema }),
|
|
136
|
+
}, async ({ workflow_id }) => {
|
|
137
|
+
try {
|
|
138
|
+
const state = await store.load(workflow_id);
|
|
139
|
+
if (!state)
|
|
140
|
+
throw new Error(`Unknown workflow: ${workflow_id}`);
|
|
141
|
+
return ok(state);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return fail(error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
mcpServer.registerTool('workflow_get_gate_prompt', {
|
|
148
|
+
description: '获取当前 gate 的提示模板和检查清单。',
|
|
149
|
+
inputSchema: z.object({ workflow_id: workflowIdSchema }),
|
|
150
|
+
}, async ({ workflow_id }) => {
|
|
151
|
+
try {
|
|
152
|
+
const state = await store.load(workflow_id);
|
|
153
|
+
if (!state)
|
|
154
|
+
throw new Error(`Unknown workflow: ${workflow_id}`);
|
|
155
|
+
const stage = machine.getStage(state.current_stage);
|
|
156
|
+
if (stage.type !== 'gate') {
|
|
157
|
+
throw new Error(`Current stage ${stage.id} is not a gate`);
|
|
158
|
+
}
|
|
159
|
+
return ok({
|
|
160
|
+
gate_id: stage.id,
|
|
161
|
+
checklist: stage.checklist ?? [],
|
|
162
|
+
gate_prompt: machine.renderPrompt(stage.gate_prompt, state),
|
|
163
|
+
decisions: stage.decisions ?? [],
|
|
164
|
+
artifacts: machine.collectArtifacts(state),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return fail(error);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
export const WORKFLOW_DEFINITION = {
|
|
2
|
+
id: 'ai-native-sdlc',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
name: 'AI Native SDLC',
|
|
5
|
+
initial_stage: 'stage0_intent',
|
|
6
|
+
context_schema: {
|
|
7
|
+
date: { type: 'date', required: true, source: 'generated' },
|
|
8
|
+
slug: { type: 'slug', required: true, source: 'generated' },
|
|
9
|
+
title: { type: 'string', required: true, source: 'user_input' },
|
|
10
|
+
domain: { type: 'string', required: true, source: 'derived' },
|
|
11
|
+
repos: { type: 'array', required: false, source: 'derived' },
|
|
12
|
+
},
|
|
13
|
+
stages: [
|
|
14
|
+
// ========== 阶段0:意图孵化 ==========
|
|
15
|
+
{
|
|
16
|
+
id: 'stage0_intent',
|
|
17
|
+
name: '意图孵化',
|
|
18
|
+
type: 'llm_generation',
|
|
19
|
+
skill: 'intent-capture-skill',
|
|
20
|
+
prompt_template: `
|
|
21
|
+
引导用户描述:
|
|
22
|
+
1. 想解决什么问题
|
|
23
|
+
2. 谁受影响
|
|
24
|
+
3. 期望的结果
|
|
25
|
+
4. 硬性约束
|
|
26
|
+
5. 开放性问题
|
|
27
|
+
生成 intent.md 草稿。
|
|
28
|
+
`,
|
|
29
|
+
outputs: ['intent/{date}-{slug}.md'],
|
|
30
|
+
next: 'gate1_intent_review',
|
|
31
|
+
},
|
|
32
|
+
// ========== Gate1:意图审查 ==========
|
|
33
|
+
{
|
|
34
|
+
id: 'gate1_intent_review',
|
|
35
|
+
name: '意图审查',
|
|
36
|
+
type: 'gate',
|
|
37
|
+
checklist: ['问题描述清晰', '预期结果可衡量', '约束明确', '开放性问题已记录'],
|
|
38
|
+
gate_prompt: 'intent.md 已生成,请审查。确认后进入设计阶段。',
|
|
39
|
+
decisions: ['approved', 'rejected', 'modified'],
|
|
40
|
+
next: 'action_create_spec_dir',
|
|
41
|
+
on_reject: 'stage0_intent',
|
|
42
|
+
},
|
|
43
|
+
// ========== Action:创建规格目录 ==========
|
|
44
|
+
{
|
|
45
|
+
id: 'action_create_spec_dir',
|
|
46
|
+
name: '创建规格目录',
|
|
47
|
+
type: 'action',
|
|
48
|
+
handler: 'create-spec-dir',
|
|
49
|
+
handler_params: {
|
|
50
|
+
create_subdirs: ['reviews/code-review', 'reviews/task-verification', 'design'],
|
|
51
|
+
write_files: ['reviews/intent-review.md'],
|
|
52
|
+
git_commit: true,
|
|
53
|
+
},
|
|
54
|
+
next: 'stage2_spec',
|
|
55
|
+
on_error: 'blocked',
|
|
56
|
+
},
|
|
57
|
+
// ========== 阶段2:规格生成 ==========
|
|
58
|
+
{
|
|
59
|
+
id: 'stage2_spec',
|
|
60
|
+
name: '规格生成',
|
|
61
|
+
type: 'llm_generation',
|
|
62
|
+
skill: 'spec-generation-skill',
|
|
63
|
+
prompt_template: `
|
|
64
|
+
读取 intent.md 和活状态索引,生成 spec.md。
|
|
65
|
+
必须包含 Areas of Concern 章节,列出所有无法自动解决的冲突。
|
|
66
|
+
`,
|
|
67
|
+
inputs: ['intent/{date}-{slug}.md', 'specs/_current/{domain}.md'],
|
|
68
|
+
outputs: ['specs/{date}-{slug}/spec.md'],
|
|
69
|
+
next: 'gate2_spec_review',
|
|
70
|
+
},
|
|
71
|
+
// ========== Gate2:规格审查 + 冲突裁决 ==========
|
|
72
|
+
{
|
|
73
|
+
id: 'gate2_spec_review',
|
|
74
|
+
name: '规格审查与冲突裁决',
|
|
75
|
+
type: 'gate',
|
|
76
|
+
checklist: ['业务逻辑完整', '接口定义清晰', 'Areas of Concern 已列出'],
|
|
77
|
+
gate_prompt: 'spec.md 已生成,发现 {conflict_count} 个冲突需裁决:{conflicts}',
|
|
78
|
+
decisions: ['approved', 'rejected', 'modified'],
|
|
79
|
+
next: 'stage3_design',
|
|
80
|
+
on_reject: 'stage2_spec',
|
|
81
|
+
},
|
|
82
|
+
// ========== 阶段3:UE设计 ==========
|
|
83
|
+
{
|
|
84
|
+
id: 'stage3_design',
|
|
85
|
+
name: 'UE设计',
|
|
86
|
+
type: 'llm_generation',
|
|
87
|
+
skill: 'ui-ux-pro-max-skill',
|
|
88
|
+
prompt_template: '基于 spec.md 生成高保真设计产物。',
|
|
89
|
+
inputs: ['specs/{date}-{slug}/spec.md'],
|
|
90
|
+
outputs: ['specs/{date}-{slug}/design/pages/*.html', 'specs/{date}-{slug}/design/tokens/*.json'],
|
|
91
|
+
next: 'gate3_design_review',
|
|
92
|
+
},
|
|
93
|
+
// ========== Gate3:设计审查 ==========
|
|
94
|
+
{
|
|
95
|
+
id: 'gate3_design_review',
|
|
96
|
+
name: '设计审查',
|
|
97
|
+
type: 'gate',
|
|
98
|
+
checklist: ['视觉符合品牌', '交互符合预期'],
|
|
99
|
+
gate_prompt: '高保真设计已生成,请审查:{artifacts}',
|
|
100
|
+
decisions: ['approved', 'modified'],
|
|
101
|
+
next: 'stage4_plan',
|
|
102
|
+
on_reject: 'stage3_design',
|
|
103
|
+
},
|
|
104
|
+
// ========== 阶段4:计划生成 ==========
|
|
105
|
+
{
|
|
106
|
+
id: 'stage4_plan',
|
|
107
|
+
name: '计划生成',
|
|
108
|
+
type: 'llm_generation',
|
|
109
|
+
skill: 'plan-generation-skill',
|
|
110
|
+
prompt_template: '读取 spec.md + design/ + codebase-context.md,生成 plan.md。',
|
|
111
|
+
inputs: ['specs/{date}-{slug}/spec.md', 'specs/{date}-{slug}/design/'],
|
|
112
|
+
outputs: ['specs/{date}-{slug}/plan.md'],
|
|
113
|
+
next: 'gate4_plan_review',
|
|
114
|
+
},
|
|
115
|
+
// ========== Gate4:计划审查 ==========
|
|
116
|
+
{
|
|
117
|
+
id: 'gate4_plan_review',
|
|
118
|
+
name: '计划审查',
|
|
119
|
+
type: 'gate',
|
|
120
|
+
checklist: ['变更范围与 spec 一致', '风险已识别', '回滚预案明确'],
|
|
121
|
+
gate_prompt: 'plan.md 已生成,请审查实施步骤和风险。',
|
|
122
|
+
decisions: ['approved', 'modified'],
|
|
123
|
+
next: 'stage5_tasks',
|
|
124
|
+
on_reject: 'stage4_plan',
|
|
125
|
+
},
|
|
126
|
+
// ========== 阶段5:任务拆解 ==========
|
|
127
|
+
{
|
|
128
|
+
id: 'stage5_tasks',
|
|
129
|
+
name: '任务拆解',
|
|
130
|
+
type: 'llm_generation',
|
|
131
|
+
skill: 'task-decomposition-skill',
|
|
132
|
+
prompt_template: '基于 plan.md 拆解为原子任务,生成 tasks.md。',
|
|
133
|
+
inputs: ['specs/{date}-{slug}/plan.md'],
|
|
134
|
+
outputs: ['specs/{date}-{slug}/tasks.md'],
|
|
135
|
+
next: 'gate5_tasks_review',
|
|
136
|
+
},
|
|
137
|
+
// ========== Gate5:任务审查 ==========
|
|
138
|
+
{
|
|
139
|
+
id: 'gate5_tasks_review',
|
|
140
|
+
name: '任务审查',
|
|
141
|
+
type: 'gate',
|
|
142
|
+
checklist: ['任务粒度合理', '依赖关系清晰', '验证标准可执行'],
|
|
143
|
+
gate_prompt: 'tasks.md 已生成,共 {task_count} 个任务。确认后开始执行。',
|
|
144
|
+
decisions: ['approved', 'modified'],
|
|
145
|
+
next: 'action_create_branch',
|
|
146
|
+
on_reject: 'stage5_tasks',
|
|
147
|
+
},
|
|
148
|
+
// ========== Action:创建分支 ==========
|
|
149
|
+
{
|
|
150
|
+
id: 'action_create_branch',
|
|
151
|
+
name: '创建特性分支',
|
|
152
|
+
type: 'action',
|
|
153
|
+
handler: 'create-feature-branch',
|
|
154
|
+
handler_params: {
|
|
155
|
+
branch_pattern: 'feature/{date}-{slug}',
|
|
156
|
+
},
|
|
157
|
+
next: 'stage6_codegen',
|
|
158
|
+
on_error: 'blocked',
|
|
159
|
+
},
|
|
160
|
+
// ========== 阶段6:代码生成 ==========
|
|
161
|
+
{
|
|
162
|
+
id: 'stage6_codegen',
|
|
163
|
+
name: '代码生成',
|
|
164
|
+
type: 'llm_generation',
|
|
165
|
+
skill: 'code-generation-skill',
|
|
166
|
+
prompt_template: '在特性分支上逐 Task 执行,每个 Task 完成后运行验证命令。',
|
|
167
|
+
inputs: ['specs/{date}-{slug}/tasks.md', 'specs/{date}-{slug}/design/'],
|
|
168
|
+
outputs: ['src/**', 'specs/{date}-{slug}/reviews/task-verification/*.json'],
|
|
169
|
+
next: 'action_create_pr',
|
|
170
|
+
},
|
|
171
|
+
// ========== Action:创建 PR ==========
|
|
172
|
+
{
|
|
173
|
+
id: 'action_create_pr',
|
|
174
|
+
name: '创建 PR',
|
|
175
|
+
type: 'action',
|
|
176
|
+
handler: 'create-pr',
|
|
177
|
+
handler_params: {
|
|
178
|
+
title_pattern: 'feat: {title}',
|
|
179
|
+
base_branch: 'main',
|
|
180
|
+
trigger_ai_review: true,
|
|
181
|
+
},
|
|
182
|
+
next: 'gate6_pr_review',
|
|
183
|
+
on_error: 'blocked',
|
|
184
|
+
},
|
|
185
|
+
// ========== Gate6:PR 审查 ==========
|
|
186
|
+
{
|
|
187
|
+
id: 'gate6_pr_review',
|
|
188
|
+
name: 'PR 审查',
|
|
189
|
+
type: 'gate',
|
|
190
|
+
checklist: ['AI findings 已处理', '测试通过', '代码 Owner 批准'],
|
|
191
|
+
gate_prompt: 'PR #{pr_number} 已创建,AI Review 发现 {findings_summary}。',
|
|
192
|
+
decisions: ['approved', 'rejected', 'modified'],
|
|
193
|
+
next: 'action_merge_pr',
|
|
194
|
+
on_reject: 'stage6_codegen',
|
|
195
|
+
},
|
|
196
|
+
// ========== Action:合并 PR ==========
|
|
197
|
+
{
|
|
198
|
+
id: 'action_merge_pr',
|
|
199
|
+
name: '合并 PR',
|
|
200
|
+
type: 'action',
|
|
201
|
+
handler: 'merge-pr',
|
|
202
|
+
handler_params: {
|
|
203
|
+
require_approvals: true,
|
|
204
|
+
delete_branch: true,
|
|
205
|
+
},
|
|
206
|
+
next: 'action_update_current_index',
|
|
207
|
+
on_error: 'blocked',
|
|
208
|
+
},
|
|
209
|
+
// ========== Action:更新活状态索引 ==========
|
|
210
|
+
{
|
|
211
|
+
id: 'action_update_current_index',
|
|
212
|
+
name: '更新活状态索引',
|
|
213
|
+
type: 'action',
|
|
214
|
+
handler: 'update-current-index',
|
|
215
|
+
handler_params: {
|
|
216
|
+
target: 'specs/_current/{domain}.md',
|
|
217
|
+
git_commit: true,
|
|
218
|
+
},
|
|
219
|
+
next: 'action_close_intent',
|
|
220
|
+
},
|
|
221
|
+
// ========== Action:关闭 intent ==========
|
|
222
|
+
{
|
|
223
|
+
id: 'action_close_intent',
|
|
224
|
+
name: '关闭意图',
|
|
225
|
+
type: 'action',
|
|
226
|
+
handler: 'close-intent',
|
|
227
|
+
handler_params: {
|
|
228
|
+
update_status: 'implemented',
|
|
229
|
+
},
|
|
230
|
+
next: 'completed',
|
|
231
|
+
},
|
|
232
|
+
// ========== 终态 ==========
|
|
233
|
+
{
|
|
234
|
+
id: 'completed',
|
|
235
|
+
name: '已完成',
|
|
236
|
+
type: 'action',
|
|
237
|
+
handler: 'noop',
|
|
238
|
+
next: 'completed',
|
|
239
|
+
},
|
|
240
|
+
// ========== 异常态 ==========
|
|
241
|
+
{
|
|
242
|
+
id: 'blocked',
|
|
243
|
+
name: '阻塞',
|
|
244
|
+
type: 'action',
|
|
245
|
+
handler: 'notify-blocked',
|
|
246
|
+
handler_params: {
|
|
247
|
+
notify: ['architect', 'owner'],
|
|
248
|
+
},
|
|
249
|
+
next: 'blocked',
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkflowEvent, WorkflowEventType } from './types.js';
|
|
2
|
+
export type EventListener = (event: WorkflowEvent) => void | Promise<void>;
|
|
3
|
+
export declare class WorkflowEventBus {
|
|
4
|
+
private readonly listeners;
|
|
5
|
+
private readonly eventsDir;
|
|
6
|
+
constructor(repoRoot: string);
|
|
7
|
+
/** Registers a listener; returns an unsubscribe function. */
|
|
8
|
+
on(eventType: WorkflowEventType | '*', listener: EventListener): () => void;
|
|
9
|
+
/**
|
|
10
|
+
* (1) persist the event as a JSON line in {repoRoot}/.workflow/events/{id}.jsonl,
|
|
11
|
+
* (2) dispatch to typed + '*' listeners and AWAIT ALL of them via Promise.all.
|
|
12
|
+
* Listener errors reject the returned promise (propagate to the emit caller).
|
|
13
|
+
*/
|
|
14
|
+
emitWorkflowEvent(event: WorkflowEvent): Promise<void>;
|
|
15
|
+
private persistEvent;
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// mcp-server/src/workflow/event-bus.ts
|
|
2
|
+
//
|
|
3
|
+
// Deliberately NOT EventEmitter-based: EventEmitter cannot await async
|
|
4
|
+
// listeners, and the tool layer needs emit() to resolve only after every
|
|
5
|
+
// listener (e.g. the state machine) has fully finished, so state can be
|
|
6
|
+
// reloaded deterministically afterwards.
|
|
7
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { resolveUnderBase } from './paths.js';
|
|
10
|
+
export class WorkflowEventBus {
|
|
11
|
+
listeners = new Map();
|
|
12
|
+
eventsDir;
|
|
13
|
+
constructor(repoRoot) {
|
|
14
|
+
this.eventsDir = path.join(repoRoot, '.workflow', 'events');
|
|
15
|
+
}
|
|
16
|
+
/** Registers a listener; returns an unsubscribe function. */
|
|
17
|
+
on(eventType, listener) {
|
|
18
|
+
const list = this.listeners.get(eventType) ?? [];
|
|
19
|
+
list.push(listener);
|
|
20
|
+
this.listeners.set(eventType, list);
|
|
21
|
+
return () => {
|
|
22
|
+
const current = this.listeners.get(eventType) ?? [];
|
|
23
|
+
const idx = current.indexOf(listener);
|
|
24
|
+
if (idx >= 0)
|
|
25
|
+
current.splice(idx, 1);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* (1) persist the event as a JSON line in {repoRoot}/.workflow/events/{id}.jsonl,
|
|
30
|
+
* (2) dispatch to typed + '*' listeners and AWAIT ALL of them via Promise.all.
|
|
31
|
+
* Listener errors reject the returned promise (propagate to the emit caller).
|
|
32
|
+
*/
|
|
33
|
+
async emitWorkflowEvent(event) {
|
|
34
|
+
await this.persistEvent(event);
|
|
35
|
+
const targets = [
|
|
36
|
+
...(this.listeners.get(event.type) ?? []),
|
|
37
|
+
...(this.listeners.get('*') ?? []),
|
|
38
|
+
];
|
|
39
|
+
await Promise.all(targets.map((listener) => listener(event)));
|
|
40
|
+
}
|
|
41
|
+
async persistEvent(event) {
|
|
42
|
+
const target = resolveUnderBase(this.eventsDir, `${event.workflow_id}.jsonl`);
|
|
43
|
+
await mkdir(this.eventsDir, { recursive: true });
|
|
44
|
+
await appendFile(target, JSON.stringify(event) + '\n', 'utf8');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ActionCompletedEvent, ActionFailedEvent, GateDecision, GateDecisionEvent, StageCompletedEvent, WorkflowStartedEvent } from './types.js';
|
|
2
|
+
export declare function workflowStartedEvent(workflow_id: string, title: string): WorkflowStartedEvent;
|
|
3
|
+
export declare function stageCompletedEvent(workflow_id: string, stage_id: string, output: string[]): StageCompletedEvent;
|
|
4
|
+
export declare function gateDecisionEvent(args: {
|
|
5
|
+
workflow_id: string;
|
|
6
|
+
stage_id: string;
|
|
7
|
+
decision: GateDecision;
|
|
8
|
+
changes?: string;
|
|
9
|
+
approved_by: string;
|
|
10
|
+
}): GateDecisionEvent;
|
|
11
|
+
export declare function actionCompletedEvent(workflow_id: string, stage_id: string, result: Record<string, unknown>): ActionCompletedEvent;
|
|
12
|
+
export declare function actionFailedEvent(workflow_id: string, stage_id: string, error: string, retryable: boolean): ActionFailedEvent;
|