@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// mcp-server/src/workflow/store.ts
|
|
2
|
+
import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolveUnderBase } from './paths.js';
|
|
5
|
+
export function summarize(state) {
|
|
6
|
+
return {
|
|
7
|
+
workflow_id: state.workflow_id,
|
|
8
|
+
title: state.title,
|
|
9
|
+
current_stage: state.current_stage,
|
|
10
|
+
status: state.status,
|
|
11
|
+
last_active: state.last_active,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function atomicWriteFile(filePath, data) {
|
|
15
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
+
const tmpPath = `${filePath}.tmp`;
|
|
17
|
+
await writeFile(tmpPath, data, 'utf8');
|
|
18
|
+
await rename(tmpPath, filePath);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* File-backed state store under {repoRoot}/.workflow:
|
|
22
|
+
* workflows/{id}.json — one file per workflow state
|
|
23
|
+
* index.json — array of workflow summaries
|
|
24
|
+
* All file writes go through tmp+rename so readers never observe partial files.
|
|
25
|
+
*/
|
|
26
|
+
export class WorkflowStore {
|
|
27
|
+
root;
|
|
28
|
+
workflowsDir;
|
|
29
|
+
indexPath;
|
|
30
|
+
constructor(repoRoot) {
|
|
31
|
+
this.root = repoRoot;
|
|
32
|
+
this.workflowsDir = path.join(repoRoot, '.workflow', 'workflows');
|
|
33
|
+
this.indexPath = path.join(repoRoot, '.workflow', 'index.json');
|
|
34
|
+
}
|
|
35
|
+
statePath(id) {
|
|
36
|
+
return resolveUnderBase(this.workflowsDir, `${id}.json`);
|
|
37
|
+
}
|
|
38
|
+
async save(state) {
|
|
39
|
+
await atomicWriteFile(this.statePath(state.workflow_id), JSON.stringify(state, null, 2));
|
|
40
|
+
await this.updateIndex(state);
|
|
41
|
+
}
|
|
42
|
+
/** Missing workflow → null; callers turn that into an "Unknown workflow" error.
|
|
43
|
+
* Containment is checked OUTSIDE the try so traversal ids throw, not return null. */
|
|
44
|
+
async load(id) {
|
|
45
|
+
const filePath = this.statePath(id);
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = await readFile(filePath, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
async listAll() {
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = await readdir(this.workflowsDir);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const states = [];
|
|
64
|
+
for (const entry of entries.sort()) {
|
|
65
|
+
if (!entry.endsWith('.json'))
|
|
66
|
+
continue;
|
|
67
|
+
const raw = await readFile(path.join(this.workflowsDir, entry), 'utf8');
|
|
68
|
+
states.push(JSON.parse(raw));
|
|
69
|
+
}
|
|
70
|
+
return states;
|
|
71
|
+
}
|
|
72
|
+
/** Active = neither completed nor blocked. Derived from state files so the list self-heals. */
|
|
73
|
+
async getActiveSummaries() {
|
|
74
|
+
const states = await this.listAll();
|
|
75
|
+
return states.filter((s) => s.status !== 'completed' && s.status !== 'blocked').map(summarize);
|
|
76
|
+
}
|
|
77
|
+
async updateIndex(state) {
|
|
78
|
+
let index;
|
|
79
|
+
try {
|
|
80
|
+
index = JSON.parse(await readFile(this.indexPath, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
index = [];
|
|
84
|
+
}
|
|
85
|
+
const summary = summarize(state);
|
|
86
|
+
const existing = index.findIndex((e) => e.workflow_id === summary.workflow_id);
|
|
87
|
+
if (existing >= 0) {
|
|
88
|
+
index[existing] = summary;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
index.push(summary);
|
|
92
|
+
}
|
|
93
|
+
await atomicWriteFile(this.indexPath, JSON.stringify(index, null, 2));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Renders {var} placeholders; unknown placeholders stay literal. */
|
|
2
|
+
export declare function renderTemplate(template: string, vars: Record<string, unknown>): string;
|
|
3
|
+
/** Renders {var} placeholders; a missing variable is an error (paths must resolve). */
|
|
4
|
+
export declare function renderStrict(template: string, vars: Record<string, unknown>): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// mcp-server/src/workflow/template.ts
|
|
2
|
+
/** Renders {var} placeholders; unknown placeholders stay literal. */
|
|
3
|
+
export function renderTemplate(template, vars) {
|
|
4
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
5
|
+
const value = vars[key];
|
|
6
|
+
return value !== undefined ? String(value) : match;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/** Renders {var} placeholders; a missing variable is an error (paths must resolve). */
|
|
10
|
+
export function renderStrict(template, vars) {
|
|
11
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
12
|
+
const value = vars[key];
|
|
13
|
+
if (value === undefined) {
|
|
14
|
+
throw new Error(`Missing context var: ${key}`);
|
|
15
|
+
}
|
|
16
|
+
return String(value);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type StageType = 'llm_generation' | 'gate' | 'action';
|
|
2
|
+
export type GateDecision = 'approved' | 'rejected' | 'modified';
|
|
3
|
+
export interface Stage {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
type: StageType;
|
|
7
|
+
skill?: string;
|
|
8
|
+
prompt_template?: string;
|
|
9
|
+
inputs?: string[];
|
|
10
|
+
outputs?: string[];
|
|
11
|
+
checklist?: string[];
|
|
12
|
+
gate_prompt?: string;
|
|
13
|
+
decisions?: GateDecision[];
|
|
14
|
+
handler?: string;
|
|
15
|
+
handler_params?: Record<string, unknown>;
|
|
16
|
+
next: string;
|
|
17
|
+
on_reject?: string;
|
|
18
|
+
on_error?: string;
|
|
19
|
+
requires_approval?: boolean;
|
|
20
|
+
timeout_hours?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ContextVar {
|
|
23
|
+
type: 'string' | 'date' | 'slug' | 'array';
|
|
24
|
+
required: boolean;
|
|
25
|
+
source: 'user_input' | 'generated' | 'derived';
|
|
26
|
+
default?: unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface WorkflowDefinition {
|
|
29
|
+
id: string;
|
|
30
|
+
version: string;
|
|
31
|
+
name: string;
|
|
32
|
+
stages: Stage[];
|
|
33
|
+
initial_stage: string;
|
|
34
|
+
context_schema: Record<string, ContextVar>;
|
|
35
|
+
}
|
|
36
|
+
export type WorkflowStatus = 'running' | 'waiting_for_user' | 'completed' | 'blocked';
|
|
37
|
+
export interface StageRecord {
|
|
38
|
+
status: string;
|
|
39
|
+
output?: string[];
|
|
40
|
+
completed_at?: string;
|
|
41
|
+
approved_by?: string;
|
|
42
|
+
approved_at?: string;
|
|
43
|
+
rejected_by?: string;
|
|
44
|
+
rejected_at?: string;
|
|
45
|
+
reason?: string;
|
|
46
|
+
changes?: string;
|
|
47
|
+
result?: Record<string, unknown>;
|
|
48
|
+
error?: string;
|
|
49
|
+
retryable?: boolean;
|
|
50
|
+
failed_at?: string;
|
|
51
|
+
modified_at?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface WorkflowState {
|
|
54
|
+
workflow_id: string;
|
|
55
|
+
intent_id: string;
|
|
56
|
+
title: string;
|
|
57
|
+
current_stage: string;
|
|
58
|
+
status: WorkflowStatus;
|
|
59
|
+
stages: Record<string, StageRecord>;
|
|
60
|
+
context: Record<string, unknown>;
|
|
61
|
+
created_at: string;
|
|
62
|
+
last_active: string;
|
|
63
|
+
}
|
|
64
|
+
export interface StageCompletedEvent {
|
|
65
|
+
type: 'stage.completed';
|
|
66
|
+
workflow_id: string;
|
|
67
|
+
stage_id: string;
|
|
68
|
+
output: string[];
|
|
69
|
+
timestamp: string;
|
|
70
|
+
}
|
|
71
|
+
export interface GateDecisionEvent {
|
|
72
|
+
type: 'gate.decision';
|
|
73
|
+
workflow_id: string;
|
|
74
|
+
stage_id: string;
|
|
75
|
+
decision: GateDecision;
|
|
76
|
+
changes?: string;
|
|
77
|
+
approved_by: string;
|
|
78
|
+
timestamp: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ActionCompletedEvent {
|
|
81
|
+
type: 'action.completed';
|
|
82
|
+
workflow_id: string;
|
|
83
|
+
stage_id: string;
|
|
84
|
+
result: Record<string, unknown>;
|
|
85
|
+
timestamp: string;
|
|
86
|
+
}
|
|
87
|
+
export interface ActionFailedEvent {
|
|
88
|
+
type: 'action.failed';
|
|
89
|
+
workflow_id: string;
|
|
90
|
+
stage_id: string;
|
|
91
|
+
error: string;
|
|
92
|
+
retryable: boolean;
|
|
93
|
+
timestamp: string;
|
|
94
|
+
}
|
|
95
|
+
export interface WorkflowStartedEvent {
|
|
96
|
+
type: 'workflow.started';
|
|
97
|
+
workflow_id: string;
|
|
98
|
+
title: string;
|
|
99
|
+
timestamp: string;
|
|
100
|
+
}
|
|
101
|
+
export type WorkflowEvent = WorkflowStartedEvent | StageCompletedEvent | GateDecisionEvent | ActionCompletedEvent | ActionFailedEvent;
|
|
102
|
+
export type WorkflowEventType = WorkflowEvent['type'];
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aicoffe/ai-native-sdlc-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP workflow state machine server for the AI-native SDLC plugin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"workflow",
|
|
10
|
+
"state-machine",
|
|
11
|
+
"sdlc",
|
|
12
|
+
"ai-native"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/aicoffe/ai-native-sdlc-plugin.git"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"ai-native-sdlc-mcp": "dist/main.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"start": "node dist/main.js",
|
|
34
|
+
"prepublishOnly": "npm run build && npm test"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
38
|
+
"zod": "^4.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.6.0",
|
|
42
|
+
"vitest": "^2.1.0",
|
|
43
|
+
"@types/node": "^22.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|