feat(workflow): 增加大方向闭环审计

This commit is contained in:
sunlei 2026-06-07 08:15:00 +08:00
parent 58062e2f44
commit fe5fab3479
10 changed files with 284 additions and 7 deletions

View File

@ -1,6 +1,6 @@
# KT Workflow MCP
`ktWorkflow` 是 KT 工作区的可复用 MCP 能力包,用 TypeScript 封装工具入参、项目别名和返回结构,用来把根目录 `AGENTS.md` 的硬规则、`SKILLS.md` 的工作流索引、`TASKS.md` 的上下文记录和 `docs/` 的详细流程变成可调用工具。当前版本:`0.6.1`。
`ktWorkflow` 是 KT 工作区的可复用 MCP 能力包,用 TypeScript 封装工具入参、项目别名和返回结构,用来把根目录 `AGENTS.md` 的硬规则、`SKILLS.md` 的工作流索引、`TASKS.md` 的上下文记录和 `docs/` 的详细流程变成可调用工具。当前版本:`0.6.2`。
## 能力边界
@ -10,6 +10,7 @@
- 生成验证建议按后端、前端、样式、页面、部署、MCP 等变更类型给出轻量验证命令。
- 固化改动后 review验证计划和提交清单都会提醒执行 `kt_global_code_review` / `pnpm run global-review`
- 生成任务收尾包:把状态、验证计划、可选验证执行、全局 review 和历史清理收成一份结果。
- 生成大方向完结闭环审计:确认开发目标、测试证据、问题记录、稳定解法和 ktWorkflow 升级是否齐备。
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
@ -31,6 +32,7 @@ cd D:/MyFiles/KT/mcp/ktWorkflow
pnpm install
pnpm run typecheck
pnpm run self-test
pnpm run workstream-closeout -- --title "发布闭环" --verification "Jenkins SUCCESS" --problem "无新卡点" --solution "无新增稳定解法"
pnpm run cleanup-history -- --dry-run
pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
```
@ -77,6 +79,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 |
| `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅深扫变更文件 |
| `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 |
| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、问题记录、稳定解法和 ktWorkflow 升级 |
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 |
@ -93,6 +96,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| --- | --- |
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告;确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 |
| `pnpm run workstream-closeout` | 生成大方向完结闭环审计;识别测试证据、问题记录、稳定解法和 ktWorkflow 升级缺口。 |
## 项目别名
@ -123,6 +127,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
- 写代码前先调用 `kt_prepare_task`;只需要单项信息时再调用 `kt_read_context``kt_inspect_project`。`kt_read_context` 默认紧凑输出,只有确实需要完整 `TASKS.md` 时再传 `includeFullDocs=true`
- 多项目联动时先调用 `kt_inspect_all_projects`
- 要收尾时调用 `kt_finish_task`,默认只生成计划和 review需要执行验证时显式传 `runValidation=true`
- 大方向结束前调用 `kt_workstream_closeout``pnpm run workstream-closeout`;如果识别到测试流、卡点、误报、清理、部署观测或命令模板,要先升级对应 ktWorkflow 规则再报告完成。
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。`findings` 先判定真实风险或工具误报,真实风险修业务代码,误报修 `mcp/ktWorkflow/src/tools/review.ts` 并复跑。
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。
- 要测真实业务链路时调用 `kt_business_test_plan`,选择 `admin-login`、`qqbot-auto-reply`、`qqbot-login-sse`、`fflogs-command`、`system-log-visualization` 等 flow。

View File

@ -1,6 +1,6 @@
{
"name": "@kt/mcp-kt-workflow",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"type": "module",
"description": "Reusable MCP workflow tools for the KT workspace.",
@ -10,7 +10,8 @@
"global-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --global-review",
"self-test": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --self-test",
"start": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"workstream-closeout": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --workstream-closeout"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.0",

View File

@ -1,4 +1,5 @@
import type { GlobalCodeReviewInput } from '../types.js';
import type { GlobalCodeReviewInput, WorkstreamCloseoutInput, WorkstreamReusablePattern } from '../types.js';
import { workstreamReusablePatternValues } from './constants.js';
function parseContentScanMode(argv: string[]): 'all' | 'changed' {
if (argv.includes('--content-scan-all') || argv.includes('--contentScanMode=all')) {
@ -20,3 +21,52 @@ export function parseGlobalReviewCliArgs(
includeRootScan: !argv.includes('--no-root-scan'),
};
}
function readOption(argv: string[], names: string[]): string | undefined {
for (const name of names) {
const keyValue = argv.find((item) => item.startsWith(`${name}=`));
if (keyValue) return keyValue.slice(name.length + 1);
const index = argv.indexOf(name);
if (index >= 0 && argv[index + 1] && !argv[index + 1].startsWith('--')) {
return argv[index + 1];
}
}
return undefined;
}
function readOptionList(argv: string[], names: string[]): string[] {
const values: string[] = [];
names.forEach((name) => {
argv.forEach((item, index) => {
if (item.startsWith(`${name}=`)) {
values.push(item.slice(name.length + 1));
} else if (item === name && argv[index + 1] && !argv[index + 1].startsWith('--')) {
values.push(argv[index + 1]);
}
});
});
return values
.flatMap((value) => value.split(','))
.map((value) => value.trim())
.filter(Boolean);
}
export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseoutInput {
const patternSet = new Set<string>(workstreamReusablePatternValues);
const patterns = readOptionList(argv, ['--pattern', '--reusable-pattern'])
.filter((pattern): pattern is WorkstreamReusablePattern => patternSet.has(pattern));
return {
changedProjects: readOptionList(argv, ['--project', '--changed-project']),
ktWorkflowUpdated: argv.includes('--kt-workflow-updated'),
problemRecords: readOptionList(argv, ['--problem', '--problem-record']),
reusablePatterns: patterns,
stableSolutions: readOptionList(argv, ['--solution', '--stable-solution']),
title: readOption(argv, ['--title']),
upgradeNotes: readOption(argv, ['--upgrade-notes']),
verificationEvidence: readOptionList(argv, ['--verification', '--verification-evidence']),
};
}

View File

@ -1,4 +1,4 @@
import type { ProjectAlias, TaskType } from '../types.js';
import type { ProjectAlias, TaskType, WorkstreamReusablePattern } from '../types.js';
export const projectAliases = {
admin: 'Vue/kt-template-admin',
api: 'Node/kt-template-online-api',
@ -38,6 +38,16 @@ export const taskTypeValues = [
'style',
] as const satisfies readonly TaskType[];
export const workstreamReusablePatternValues = [
'blocker',
'cleanup',
'command-template',
'deploy-observation',
'none',
'review-false-positive',
'test-flow',
] as const satisfies readonly WorkstreamReusablePattern[];
export const registeredToolNames = [
'kt_read_context',
'kt_inspect_project',
@ -53,6 +63,7 @@ export const registeredToolNames = [
'kt_risk_scan',
'kt_global_code_review',
'kt_finish_task',
'kt_workstream_closeout',
'kt_commit_plan',
'kt_push_plan',
'kt_business_test_plan',

View File

@ -12,7 +12,9 @@ import { buildGlobalCodeReview, defaultReviewProjects } from './tools/review.js'
import { appendTaskRecord, createCommitChecklist, prepareTask, readWorkflowContext } from './tools/task.js';
import { buildBusinessTestPlan, createApiTestPlan, createPageTestCase } from './tools/testing.js';
import { buildVerificationPlan } from './tools/verification.js';
import { buildWorkstreamCloseout } from './tools/closeout.js';
import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js';
import { workstreamReusablePatternValues } from './core/constants.js';
export function response(data: unknown): McpTextResponse {
return {
content: [
@ -238,6 +240,25 @@ export function registerTools(server: McpServer): void {
async (input) => response(await buildFinishTask(input)),
);
server.registerTool(
'kt_workstream_closeout',
{
description: '生成大方向完结闭环审计:开发、测试证据、问题记录、稳定解法和 ktWorkflow 升级是否齐备。',
inputSchema: {
changedProjects: z.array(z.string()).default([]),
ktWorkflowUpdated: z.boolean().default(false),
problemRecords: z.array(z.string()).default([]),
reusablePatterns: z.array(z.enum(workstreamReusablePatternValues)).default(['none']),
stableSolutions: z.array(z.string()).default([]),
title: z.string().optional(),
upgradeNotes: z.string().optional(),
verificationEvidence: z.array(z.string()).default([]),
},
title: 'KT Workstream Closeout',
},
async (input) => response(buildWorkstreamCloseout(input)),
);
server.registerTool(
'kt_commit_plan',
{

View File

@ -12,6 +12,7 @@ import { prepareTask, readWorkflowContext } from './tools/task.js';
import { buildGlobalCodeReview, isBenignCredentialReviewValue } from './tools/review.js';
import { buildBusinessTestPlan } from './tools/testing.js';
import { buildVerificationPlan } from './tools/verification.js';
import { buildWorkstreamCloseout } from './tools/closeout.js';
import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js';
export async function runSelfTest(): Promise<void> {
const reviewClassifier = {
@ -47,6 +48,29 @@ export async function runSelfTest(): Promise<void> {
throw new Error('refactor guardrail self-check failed');
}
const closeoutNeedsUpgrade = buildWorkstreamCloseout({
problemRecords: ['Jenkins 状态需要结合日志和 K8s 状态确认。'],
reusablePatterns: ['deploy-observation'],
stableSolutions: ['收尾时固定观测 build、commit、镜像、Deployment、Pod 和 smoke。'],
title: '发布闭环',
verificationEvidence: ['Jenkins #132 SUCCESSK8s Pod Running。'],
});
if ((closeoutNeedsUpgrade.canReportComplete as boolean) !== false) {
throw new Error('workstream closeout upgrade gate self-check failed');
}
const closeoutComplete = buildWorkstreamCloseout({
ktWorkflowUpdated: true,
problemRecords: ['无新卡点。'],
reusablePatterns: ['none'],
stableSolutions: ['无新增稳定解法。'],
title: '文档收尾',
verificationEvidence: ['typecheck/self-test/global-review 通过。'],
});
if ((closeoutComplete.canReportComplete as boolean) !== true) {
throw new Error('workstream closeout complete self-check failed');
}
const data = {
context: readWorkflowContext({ taskRecordCount: 2 }),
guardrails: buildGuardrails({
@ -78,6 +102,8 @@ export async function runSelfTest(): Promise<void> {
businessTestPlan: buildBusinessTestPlan({
flow: 'system-log-visualization',
}),
closeoutComplete,
closeoutNeedsUpgrade,
blockerResolution: buildBlockerResolution({
attempts: 2,
evidence: 'self-test',

View File

@ -2,8 +2,9 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { parseGlobalReviewCliArgs } from './core/cli.js';
import { parseGlobalReviewCliArgs, parseWorkstreamCloseoutCliArgs } from './core/cli.js';
import { cleanupHistoryArtifacts, parseCliCleanupArgs } from './tools/cleanup.js';
import { buildWorkstreamCloseout } from './tools/closeout.js';
import { buildGlobalCodeReview } from './tools/review.js';
import { registerTools } from './registerTools.js';
import { runSelfTest } from './selfTest.js';
@ -20,10 +21,12 @@ if (process.argv.includes('--cleanup-history')) {
);
} else if (process.argv.includes('--self-test')) {
await runSelfTest();
} else if (process.argv.includes('--workstream-closeout')) {
console.log(JSON.stringify(buildWorkstreamCloseout(parseWorkstreamCloseoutCliArgs(process.argv)), null, 2));
} else {
const server = new McpServer({
name: 'kt-workflow',
version: '0.6.1',
version: '0.6.2',
});
registerTools(server);
await server.connect(new StdioServerTransport());

129
src/tools/closeout.ts Normal file
View File

@ -0,0 +1,129 @@
import type { WorkstreamCloseoutInput, WorkstreamReusablePattern } from '../types.js';
import { workstreamReusablePatternValues } from '../core/constants.js';
import { defaultReviewProjects } from './review.js';
interface UpgradeTarget {
files: string[];
label: string;
prompt: string;
}
const upgradeTargets = {
blocker: {
files: ['src/tools/blocker.ts', 'src/tools/workflow.ts', 'README.md'],
label: '卡点固化',
prompt: '把重复失败的命令、稳定解法、停止条件和后续入口沉淀成 kt_blocker_resolution 或收尾规则。',
},
cleanup: {
files: ['src/tools/cleanup.ts', 'src/tools/verification.ts', 'README.md'],
label: '清理需求',
prompt: '把历史产物、验证进程或远程临时目录清理变成可复用命令或验证提醒。',
},
'command-template': {
files: ['src/tools/workflow.ts', 'src/tools/testing.ts', 'scripts/'],
label: '命令模板',
prompt: '把反复使用且容易写错的命令模板封装到工作流、业务测试计划或脚本。',
},
'deploy-observation': {
files: ['src/tools/workflow.ts', 'src/tools/testing.ts', 'README.md'],
label: '部署观测',
prompt: '把 Jenkins/K8s build、commit、镜像、Deployment、Pod、日志和 smoke 观测步骤写成固定计划。',
},
none: {
files: [],
label: '无可执行升级',
prompt: '明确说明本轮没有新的可复用流程、误报、清理或命令模板需要沉淀。',
},
'review-false-positive': {
files: ['src/tools/review.ts', 'src/selfTest.ts', 'README.md'],
label: '复审误报',
prompt: '把已确认的误报规则沉淀到 review 过滤器和 self-test避免后续污染上下文。',
},
'test-flow': {
files: ['src/tools/testing.ts', 'scripts/', 'README.md'],
label: '测试链路',
prompt: '把新的真实接口、页面、QQBot、部署或业务 smoke 流程加入测试计划或脚本。',
},
} satisfies Record<WorkstreamReusablePattern, UpgradeTarget>;
function normalizeList(values?: string[]): string[] {
return (values || []).map((value) => value.trim()).filter(Boolean);
}
function normalizePatterns(values?: WorkstreamReusablePattern[]): WorkstreamReusablePattern[] {
const validValues = new Set<WorkstreamReusablePattern>(workstreamReusablePatternValues);
const uniqueValues = Array.from(
new Set((values || []).filter((value) => validValues.has(value))),
);
const patterns: WorkstreamReusablePattern[] = uniqueValues.length > 0 ? uniqueValues : ['none'];
return patterns.length > 1 ? patterns.filter((pattern) => pattern !== 'none') : patterns;
}
function buildPhase(name: string, evidence: string[], emptyHint: string): Record<string, unknown> {
return {
evidence,
name,
status: evidence.length > 0 ? 'ok' : 'missing',
suggestion: evidence.length > 0 ? undefined : emptyHint,
};
}
export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Record<string, unknown> {
const title = input.title?.trim() || '';
const changedProjects = normalizeList(input.changedProjects);
const verificationEvidence = normalizeList(input.verificationEvidence);
const problemRecords = normalizeList(input.problemRecords);
const stableSolutions = normalizeList(input.stableSolutions);
const patterns = normalizePatterns(input.reusablePatterns);
const upgradeNeeded = patterns.some((pattern) => pattern !== 'none');
const ktWorkflowUpdated = input.ktWorkflowUpdated === true;
const missingItems = [
title ? '' : '填写本轮大方向名称或目标。',
verificationEvidence.length > 0 ? '' : '补充测试或 smoke 验证证据;只有计划不算闭环完成。',
problemRecords.length > 0 ? '' : '补充问题记录;如无新卡点,显式写“无新卡点”。',
stableSolutions.length > 0 ? '' : '补充稳定解法;如无新解法,显式写“无新增稳定解法”。',
upgradeNeeded && !ktWorkflowUpdated
? '本轮识别到可复用模式,先升级 ktWorkflow 对应规则或脚本,再报告完成。'
: '',
].filter(Boolean);
const targetProjects = changedProjects.length > 0 ? changedProjects : [...defaultReviewProjects];
const selectedUpgradeTargets = patterns.map((pattern) => ({
pattern,
...upgradeTargets[pattern],
}));
return {
canReportComplete: missingItems.length === 0,
closeoutOrder: [
buildPhase('development', title ? [title] : [], '写清本轮完成的开发目标。'),
buildPhase('test/smoke verification', verificationEvidence, '补一条可复验的验证证据。'),
buildPhase('problem record', problemRecords, '记录问题点;没有问题也要显式写明。'),
buildPhase('stable solution', stableSolutions, '记录稳定解法;没有新增解法也要显式写明。'),
{
evidence: upgradeNeeded
? [
ktWorkflowUpdated
? input.upgradeNotes || '已升级 ktWorkflow。'
: '待升级 ktWorkflow。',
]
: [input.upgradeNotes || '无新的可执行 ktWorkflow 升级需求。'],
name: 'ktWorkflow upgrade',
status: upgradeNeeded ? (ktWorkflowUpdated ? 'ok' : 'missing') : 'not-needed',
},
],
commands: [
'git status --short',
'pnpm run typecheck',
'pnpm run self-test',
'pnpm run global-review',
'git diff --check',
],
missingItems,
patterns,
targetProjects,
title: title || '未命名大方向',
upgradeNeeded,
upgradeTargets: selectedUpgradeTargets,
};
}

View File

@ -6,6 +6,7 @@ import { cleanupHistoryArtifacts } from './cleanup.js';
import { karpathyChangedFileChecklistItem, karpathyFinishNote, karpathyStopCondition } from './karpathy.js';
import { buildGlobalCodeReview, defaultReviewProjects } from './review.js';
import { buildVerificationPlan } from './verification.js';
import { buildWorkstreamCloseout } from './closeout.js';
interface RepoSnapshot {
branch: string;
changedFiles: string[];
@ -109,6 +110,16 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
return {
cleanup,
closeout: buildWorkstreamCloseout({
changedProjects: changedSnapshots.map((snapshot) => snapshot.project.relativePath),
problemRecords: [],
reusablePatterns: ['none'],
stableSolutions: [],
title: 'kt_finish_task 收尾审计',
verificationEvidence: validationRuns
.filter((item) => item.result.ok)
.map((item) => `${item.project.relativePath}: ${item.command}`),
}),
mode: input.runValidation ? 'validation_executed' : 'plan_only',
notes: [
'默认只生成收尾结果runValidation=true 时才执行验证命令。',

View File

@ -25,6 +25,15 @@ export type TaskType =
| 'refactor'
| 'style';
export type WorkstreamReusablePattern =
| 'blocker'
| 'cleanup'
| 'command-template'
| 'deploy-observation'
| 'none'
| 'review-false-positive'
| 'test-flow';
export interface ResolvedProject {
alias: ProjectAlias | null;
label: string;
@ -242,6 +251,17 @@ export interface WorkflowContextInput {
taskRecordCount?: number;
}
export interface WorkstreamCloseoutInput {
changedProjects?: string[];
ktWorkflowUpdated?: boolean;
problemRecords?: string[];
reusablePatterns?: WorkstreamReusablePattern[];
stableSolutions?: string[];
title?: string;
upgradeNotes?: string;
verificationEvidence?: string[];
}
export interface VerificationPlanResult {
changeType: TaskType;
commands: string[];