Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/flow/src/meta/meta.mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Meta MCP Server
*
* Provides workflow execution capability to AI agents.
* - workflow(name, args): Execute any workflow
* - use(name, args): Execute any workflow by name
* - reload(): Refresh workflow list and return updated description
* - buildMetaMcp(): Package workflows into an MCP server
*
Expand Down Expand Up @@ -185,7 +185,7 @@ async function buildToolDescription(
.join("\n");

// 动态生成 examples
const examples = workflows.slice(0, 3).map((w) => ` workflow("${w.name}", ["--help"])`).join("\n");
const examples = workflows.slice(0, 3).map((w) => ` use("${w.name}", ["--help"])`).join("\n");

return `Execute a workflow by name with arguments.

Expand All @@ -197,7 +197,7 @@ async function buildToolDescription(
${workflowList || "(none)"}

## Examples
${examples || ' workflow("<name>", ["--help"])'}`;
${examples || ' use("<name>", ["--help"])'}`;
}

// =============================================================================
Expand Down Expand Up @@ -341,7 +341,7 @@ async function createWorkflowTool(
const description = await buildToolDescription(directories, filter);

return defineTool({
name: "workflow",
name: "use",
description,
inputSchema: z.object({
name: z
Expand Down
137 changes: 133 additions & 4 deletions scripts/agent-flow/mcps/git-workflow.mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
* Git Workflow MCP - GitHub & Git 操作封装
*
* 提供原子化的 Git/GitHub 操作工具,供 workflow 调用。
*
* ## 标签管理
*
* 模块加载时通过 top-level await 从 GitHub 获取所有标签:
* - `getLabels()` - 获取标签列表(含 name, color, description)
* - `createLabel()` - 创建新标签(自动推断颜色)
* - `ensureLabels()` - 确保标签存在,可选自动创建
*
* 颜色推断规则:
* - `area/*` → #c5def5 (蓝色)
* - `type/*` → #0e8a16 (绿色)
* - `priority/*` → #d93f0b (红色)
* - 其他 → #ededed (灰色)
*
* ## Issue/PR 创建
*
* `createIssue()` 和 `createPr()` 支持:
* - `labels` - 要添加的标签数组
* - `createLabels` - 是否自动创建缺失的标签
*
* 如果标签不存在且 `createLabels=false`,会抛出错误提示。
*/

import { execSync } from "node:child_process";
Expand Down Expand Up @@ -46,8 +67,107 @@ function safeExec(cmd: string, cwd?: string): string | null {
// Pure Functions (Exports)
// =============================================================================

export async function createIssue(args: { title: string; body: string; labels?: string[]; assignee?: string }) {
const { title, body, labels, assignee = "@me" } = args;
/** Label info type */
interface LabelInfo {
name: string;
color: string;
description: string;
}

/** Cache for available labels with their colors */
let labelsCache: LabelInfo[] | null = null;

/**
* Load labels from GitHub (called once at startup via top-level await)
*/
async function loadLabelsFromGitHub(): Promise<LabelInfo[]> {
const output = safeExec(`gh label list --repo ${REPO} --limit 100 --json name,color,description`);
if (!output) return [];
try {
return JSON.parse(output) as LabelInfo[];
} catch {
return [];
}
}

// Top-level await: load labels at module initialization
labelsCache = await loadLabelsFromGitHub();
console.log(`📋 Loaded ${labelsCache.length} labels from GitHub`);

/**
* Get all available labels in the repository
*/
export async function getLabels(args?: { refresh?: boolean }): Promise<{ labels: LabelInfo[] }> {
if (args?.refresh || !labelsCache) {
labelsCache = await loadLabelsFromGitHub();
}
return { labels: labelsCache };
}

/**
* Get color for a label (from cache or generate based on prefix)
*/
function getLabelColor(name: string): string {
// Check if label exists in cache
const existing = labelsCache?.find(l => l.name === name);
if (existing) return existing.color;

// Generate color based on prefix
if (name.startsWith("area/")) return "c5def5";
if (name.startsWith("type/")) return "0e8a16";
if (name.startsWith("priority/")) return "d93f0b";
return "ededed"; // Default gray
}

/**
* Create a label if it doesn't exist
*/
export async function createLabel(args: { name: string; description?: string; color?: string }): Promise<{ created: boolean }> {
const { name, description = "", color = getLabelColor(name) } = args;
const { labels } = await getLabels();

if (labels.some(l => l.name === name)) {
return { created: false };
}

const descFlag = description ? `--description "${description}"` : "";
exec(`gh label create "${name}" --repo ${REPO} --color "${color}" ${descFlag}`);
labelsCache = null; // Invalidate cache
return { created: true };
}

/**
* Ensure all specified labels exist, creating missing ones if requested
*/
export async function ensureLabels(args: { labels: string[]; create?: boolean }): Promise<{ missing: string[]; created: string[] }> {
const { labels: requestedLabels, create = false } = args;
const { labels: existingLabels } = await getLabels();
const existingNames = existingLabels.map(l => l.name);

const missing = requestedLabels.filter(l => !existingNames.includes(l));
const created: string[] = [];

if (create && missing.length > 0) {
for (const label of missing) {
await createLabel({ name: label });
created.push(label);
}
}

return { missing: create ? [] : missing, created };
}

export async function createIssue(args: { title: string; body: string; labels?: string[]; assignee?: string; createLabels?: boolean }) {
const { title, body, labels, assignee = "@me", createLabels = false } = args;

// Ensure labels exist before creating issue
if (labels && labels.length > 0) {
const { missing } = await ensureLabels({ labels, create: createLabels });
if (missing.length > 0) {
throw new Error(`Labels not found: ${missing.join(", ")}. Use --create-labels to auto-create them.`);
}
}

const labelFlag = labels ? labels.map(l => `--label "${l}"`).join(" ") : "";
const assigneeFlag = assignee ? `--assignee "${assignee}"` : "";
const safeBody = body.replace(/"/g, '\\"');
Expand All @@ -70,8 +190,17 @@ export async function updateIssue(args: { issueId: string; body?: string; state?
return { success: true };
}

export async function createPr(args: { title: string; body: string; head: string; base?: string; draft?: boolean; labels?: string[] }) {
const { title, body, head, base = "main", draft = true, labels } = args;
export async function createPr(args: { title: string; body: string; head: string; base?: string; draft?: boolean; labels?: string[]; createLabels?: boolean }) {
const { title, body, head, base = "main", draft = true, labels, createLabels = false } = args;

// Ensure labels exist before creating PR
if (labels && labels.length > 0) {
const { missing } = await ensureLabels({ labels, create: createLabels });
if (missing.length > 0) {
throw new Error(`Labels not found: ${missing.join(", ")}. Use --create-labels to auto-create them.`);
}
}

const draftFlag = draft ? "--draft" : "";
const labelFlag = labels ? labels.map(l => `--label "${l}"`).join(" ") : "";

Expand Down
84 changes: 74 additions & 10 deletions scripts/agent-flow/workflows/task.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,38 @@
*
* 核心理念:AI 的计划即 Issue,AI 的执行即 PR。
*
* 主要功能:
* 1. start: 一键启动 (Issue -> Branch -> Worktree -> Draft PR)
* 2. sync: 同步进度 (Local Todo -> Issue Body)
* 3. submit: 提交任务 (Push -> Ready PR)
* ## 工作流程
*
* ```
* task start task submit
* │ │
* ▼ ▼
* Issue + Branch + Worktree Push + Ready PR
* + Draft PR [skip ci] (触发 CI)
* ```
*
* ## 主要功能
*
* 1. **start**: 一键启动开发环境
* - 创建 GitHub Issue (根据 type 选择模板)
* - 创建 Git Branch + Worktree
* - 创建 Draft PR (带 [skip ci],不触发 CI)
* - 支持 --list-labels 列出可用标签
* - 支持 --create-labels 自动创建缺失标签
*
* 2. **sync**: 同步进度到 Issue
* - 将本地 Todo/进度更新到 Issue Description
*
* 3. **submit**: 提交任务触发 CI
* - 推送代码 (不带 [skip ci],触发 CI)
* - 标记 PR 为 Ready for Review
*
* ## 标签管理
*
* 标签在模块加载时从 GitHub 动态获取,支持:
* - 按前缀分组显示 (type/, area/, etc.)
* - 自动推断新标签颜色
* - 创建前验证标签是否存在
*/

import { existsSync } from "jsr:@std/fs";
Expand All @@ -22,6 +50,7 @@ import {
createWorktree,
pushWorktree,
updateIssue,
getLabels,
} from "../mcps/git-workflow.mcp.ts";
import { getRelatedChapters } from "../mcps/whitebook.mcp.ts";

Expand Down Expand Up @@ -96,24 +125,54 @@ ${desc}
*/
const startWorkflow = defineWorkflow({
name: "start",
description: "启动新任务 (Issue -> Branch -> Worktree -> Draft PR)",
description: "启动新任务 (Issue + Worktree + Draft PR,不触发 CI)",
args: {
title: { type: "string", description: "任务标题", required: true },
title: { type: "string", description: "任务标题", required: false },
type: {
type: "string",
description: "任务类型 (ui|service|page|hybrid)",
default: "hybrid",
},
description: { type: "string", description: "任务描述", required: false },
"create-labels": {
type: "boolean",
description: "自动创建不存在的标签",
default: false,
},
"list-labels": {
type: "boolean",
description: "列出所有可用标签",
default: false,
},
},
handler: async (args) => {
// Handle --list-labels flag
if (args["list-labels"]) {
const { labels } = await getLabels({ refresh: true });
console.log("📋 可用标签列表:\n");
const grouped = new Map<string, typeof labels>();
for (const label of labels) {
const prefix = label.name.includes("/") ? label.name.split("/")[0] : "other";
if (!grouped.has(prefix)) grouped.set(prefix, []);
grouped.get(prefix)!.push(label);
}
for (const [prefix, items] of grouped) {
console.log(` [${prefix}]`);
for (const item of items) {
console.log(` - ${item.name} (#${item.color})${item.description ? ` - ${item.description}` : ""}`);
}
}
return;
}

const title = args.title || args._.join(" ");
if (!title) {
console.error("❌ 错误: 请提供任务标题");
Deno.exit(1);
}
const type = (args.type || "hybrid") as keyof typeof TEMPLATES;
const rawDesc = args.description || "Start development...";
const createLabels = args["create-labels"] as boolean;

// 1. 组装 Description
const template = TEMPLATES[type] || TEMPLATES.hybrid;
Expand All @@ -125,6 +184,7 @@ const startWorkflow = defineWorkflow({
if (type === "service") labels.push("area/core");

console.log(`🚀 启动任务: ${title} [${type}]\n`);
console.log(`🏷️ 标签: ${labels.join(", ")}${createLabels ? " (自动创建)" : ""}\n`);

// 3. 上下文注入
console.log("📚 推荐阅读白皮书章节:");
Expand All @@ -138,6 +198,7 @@ const startWorkflow = defineWorkflow({
title,
body: description,
labels,
createLabels,
});
console.log(` ✅ Issue #${issueId} Created: ${issueUrl}`);

Expand All @@ -152,11 +213,11 @@ const startWorkflow = defineWorkflow({
console.log(` ✅ Worktree Created: ${path}`);
console.log(` ✅ Branch Created: ${branch}`);

// 6. 初始化提交 & 推送
// 6. 初始化提交 & 推送 (skip CI for draft)
console.log("\n3️⃣ 初始化 Git 环境...");
await pushWorktree({
path,
message: `chore: start issue #${issueId}`,
message: `chore: start issue #${issueId} [skip ci]`,
});

// 7. 创建 Draft PR
Expand All @@ -168,6 +229,7 @@ const startWorkflow = defineWorkflow({
base: "main",
draft: true,
labels,
createLabels,
});
console.log(` ✅ Draft PR Created: ${prUrl}`);

Expand Down Expand Up @@ -216,11 +278,11 @@ const syncWorkflow = defineWorkflow({

/**
* 提交任务
* Push 代码 -> 标记 PR 为 Ready
* Push 代码 (触发 CI) -> 标记 PR 为 Ready
*/
const submitWorkflow = defineWorkflow({
name: "submit",
description: "提交任务 (Push -> Ready PR)",
description: "提交任务并触发 CI (Push + Ready PR)",
handler: async () => {
const wt = getCurrentWorktreeInfo();
if (!wt || !wt.path) {
Expand Down Expand Up @@ -280,6 +342,8 @@ export const workflow = createRouter({
examples: [
['task start --type ui --title "Button Component"', "启动 UI 任务"],
['task start --type service --title "Auth Service"', "启动服务任务"],
['task start --type service --title "New Feature" --create-labels', "启动任务并自动创建缺失标签"],
['task start --list-labels', "列出所有可用标签"],
['task sync "- [x] Step 1"', "同步进度"],
["task submit", "提交任务"],
],
Expand Down
Loading