refactor: create 从 SDD 阶段降级为工具命令,移除阶段配置和提示词
This commit is contained in:
@@ -36,6 +36,22 @@ export async function injectClaudeCode(
|
||||
}
|
||||
}
|
||||
|
||||
// create 是工具命令,不是 SDD 阶段,但仍需生成对应的 command 文件
|
||||
{
|
||||
const stage = "create";
|
||||
const cmd = `${command} ${stage} <变更名>`;
|
||||
const smartGuide = `\n${buildSmartGuide(command)}\n`;
|
||||
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||
await mkdir(commandDir, { recursive: true });
|
||||
const commandPath = join(commandDir, `rune-${stage}.md`);
|
||||
if (!existsSync(commandPath)) {
|
||||
await writeFile(
|
||||
commandPath,
|
||||
`执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${smartGuide}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const introCommandPath = join(projectRoot, COMMANDS_DIR, "rune-intro.md");
|
||||
if (!existsSync(introCommandPath)) {
|
||||
await mkdir(join(projectRoot, COMMANDS_DIR), { recursive: true });
|
||||
@@ -59,6 +75,18 @@ export async function updateClaudeCode(
|
||||
await writeIfChanged(commandPath, newContent);
|
||||
}
|
||||
|
||||
// create 是工具命令,不是 SDD 阶段,但仍需生成对应的 command 文件
|
||||
{
|
||||
const stage = "create";
|
||||
const cmd = `${command} ${stage} <变更名>`;
|
||||
const smartGuide = `\n${buildSmartGuide(command)}\n`;
|
||||
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||
await mkdir(commandDir, { recursive: true });
|
||||
const commandPath = join(commandDir, `rune-${stage}.md`);
|
||||
const newContent = `执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${smartGuide}\n`;
|
||||
await writeIfChanged(commandPath, newContent);
|
||||
}
|
||||
|
||||
const introCommandPath = join(projectRoot, COMMANDS_DIR, "rune-intro.md");
|
||||
await writeIfChanged(introCommandPath, generateIntroCommand(command));
|
||||
}
|
||||
@@ -66,11 +94,11 @@ export async function updateClaudeCode(
|
||||
function generateIntroCommand(command: string): string {
|
||||
return `Rune 是基于规格驱动开发(SDD)的 AI 开发辅助工具。SDD 工作流程:
|
||||
|
||||
discuss → create → plan → build → archive
|
||||
discuss → plan → build → archive
|
||||
|
||||
可用命令:
|
||||
- /rune-discuss — 自由讨论需求和方案
|
||||
- /rune-create — 创建变更目录
|
||||
- /rune-create — 创建变更目录(辅助命令,plan 阶段的前置步骤)
|
||||
- /rune-plan — 生成设计文档和任务清单
|
||||
- /rune-build — 按任务清单逐步实现
|
||||
- /rune-archive — 归档已完成的变更
|
||||
|
||||
@@ -26,6 +26,24 @@ export async function injectOpenCode(projectRoot: string, command: string = "run
|
||||
}
|
||||
}
|
||||
|
||||
// create 是工具命令,不是 SDD 阶段,但仍需生成对应的 command 和 skill 文件
|
||||
{
|
||||
const stage = "create";
|
||||
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||
await mkdir(commandDir, { recursive: true });
|
||||
const commandPath = join(commandDir, `rune-${stage}.md`);
|
||||
if (!existsSync(commandPath)) {
|
||||
await writeFile(commandPath, generateCommand(stage));
|
||||
}
|
||||
|
||||
const skillStageDir = join(projectRoot, SKILLS_DIR, `rune-${stage}`);
|
||||
await mkdir(skillStageDir, { recursive: true });
|
||||
const skillPath = join(skillStageDir, "SKILL.md");
|
||||
if (!existsSync(skillPath)) {
|
||||
await writeFile(skillPath, generateSkill(stage, command));
|
||||
}
|
||||
}
|
||||
|
||||
const introSkillDir = join(projectRoot, SKILLS_DIR, "rune-intro");
|
||||
await mkdir(introSkillDir, { recursive: true });
|
||||
const introSkillPath = join(introSkillDir, "SKILL.md");
|
||||
@@ -47,6 +65,20 @@ export async function updateOpenCode(projectRoot: string, command: string = "run
|
||||
await writeIfChanged(skillPath, generateSkill(stage, command));
|
||||
}
|
||||
|
||||
// create 是工具命令,不是 SDD 阶段,但仍需生成对应的 command 和 skill 文件
|
||||
{
|
||||
const stage = "create";
|
||||
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||
await mkdir(commandDir, { recursive: true });
|
||||
const commandPath = join(commandDir, `rune-${stage}.md`);
|
||||
await writeIfChanged(commandPath, generateCommand(stage));
|
||||
|
||||
const skillStageDir = join(projectRoot, SKILLS_DIR, `rune-${stage}`);
|
||||
await mkdir(skillStageDir, { recursive: true });
|
||||
const skillPath = join(skillStageDir, "SKILL.md");
|
||||
await writeIfChanged(skillPath, generateSkill(stage, command));
|
||||
}
|
||||
|
||||
const introSkillDir = join(projectRoot, SKILLS_DIR, "rune-intro");
|
||||
await mkdir(introSkillDir, { recursive: true });
|
||||
const introSkillPath = join(introSkillDir, "SKILL.md");
|
||||
@@ -69,7 +101,7 @@ function generateSkill(stage: string, command: string): string {
|
||||
|
||||
const descriptionMap: Record<string, string> = {
|
||||
discuss: "Use when 需要进入 SDD 讨论阶段,自由讨论需求和架构方案",
|
||||
create: "Use when 需要创建变更目录,为 SDD 流程准备变更工作区",
|
||||
create: "Use when 需要创建变更目录(plan 阶段的辅助命令,prepare workspace)",
|
||||
plan: "Use when 需要进入 SDD 规划阶段,生成设计文档和任务清单",
|
||||
build: "Use when 需要进入 SDD 构建阶段,按任务清单逐步实现变更",
|
||||
archive: "Use when 需要进入 SDD 归档阶段,确认变更完成并归档",
|
||||
@@ -115,8 +147,8 @@ Rune 是基于规格驱动开发(SDD)的 AI 开发辅助工具。它通过
|
||||
## SDD 工作流程
|
||||
|
||||
\`\`\`
|
||||
discuss → create → plan → build → archive
|
||||
探索 创建 规划 构建 归档
|
||||
discuss → plan → build → archive
|
||||
探索 规划 构建 归档
|
||||
\`\`\`
|
||||
|
||||
## 可用命令
|
||||
@@ -124,7 +156,7 @@ discuss → create → plan → build → archive
|
||||
| 阶段 | 编辑器命令 | 说明 |
|
||||
|------|-----------|------|
|
||||
| discuss | /rune-discuss | 自由讨论需求和方案 |
|
||||
| create | /rune-create | 创建变更目录 |
|
||||
| create | /rune-create | 创建变更目录(辅助命令) |
|
||||
| plan | /rune-plan | 生成设计文档和任务清单 |
|
||||
| build | /rune-build | 按任务清单逐步实现 |
|
||||
| archive | /rune-archive | 归档已完成的变更 |
|
||||
@@ -138,9 +170,8 @@ ${command} status
|
||||
## 快速开始
|
||||
|
||||
1. 使用 /rune-discuss 进入讨论,自由探索需求
|
||||
2. 使用 /rune-create 创建变更目录
|
||||
3. 使用 /rune-plan 生成设计文档和任务清单
|
||||
4. 使用 /rune-build 按任务顺序实现功能
|
||||
5. 使用 /rune-archive 归档已完成的变更
|
||||
2. 使用 /rune-create 创建变更目录,然后用 /rune-plan 生成设计文档和任务清单
|
||||
3. 使用 /rune-build 按任务顺序实现功能
|
||||
4. 使用 /rune-archive 归档已完成的变更
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { runInit } from "./commands/init.ts";
|
||||
import { findProjectRoot, loadConfig, getChangeDir, getArchiveDir } from "./core/config.ts";
|
||||
import {
|
||||
assembleDiscussPrompt,
|
||||
assembleCreatePrompt,
|
||||
assemblePlanPrompt,
|
||||
assembleBuildPrompt,
|
||||
assembleArchivePrompt,
|
||||
@@ -216,8 +215,10 @@ cli.command("create <change-name>", "创建变更").action(async (changeName: st
|
||||
});
|
||||
}
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
const prompt = assembleCreatePrompt(config);
|
||||
console.log(prompt);
|
||||
const prefix = getPmPrefix(config);
|
||||
console.log(`变更 "${changeName}" 已创建。
|
||||
|
||||
下一步:${prefix} plan ${changeName} <文档名>`);
|
||||
});
|
||||
|
||||
cli
|
||||
|
||||
@@ -30,10 +30,11 @@ const COMMANDS: Record<string, CommandHelpDef> = {
|
||||
create: {
|
||||
name: "create",
|
||||
alias: "create <变更>",
|
||||
description: "创建:创建变更目录",
|
||||
description: "创建变更目录(plan 阶段的辅助命令)",
|
||||
usageTemplate: "create <change-name>",
|
||||
args: [{ name: "<change-name>", desc: '变更名称,如 "add-login"' }],
|
||||
detail: "在 .rune/changes/ 下创建以变更名称命名的目录,并生成创建阶段提示词。",
|
||||
detail:
|
||||
"在 .rune/changes/ 下创建以变更名称命名的目录。这不是独立的 SDD 阶段,而是为 plan 阶段准备变更工作区的工具命令。使用 plan 前必须先 create 创建变更目录。",
|
||||
exampleArgs: ["create add-user-auth", "create fix-memory-leak"],
|
||||
},
|
||||
plan: {
|
||||
|
||||
@@ -11,11 +11,11 @@ import { parse as parseYaml } from "yaml";
|
||||
const CONFIG_TEMPLATE = `# Rune 配置文件
|
||||
#
|
||||
# 未配置的阶段将使用内置默认配置。
|
||||
# 阶段顺序:discuss -> create -> plan -> build -> archive
|
||||
# SDD 四阶段:discuss -> plan -> build -> archive
|
||||
# 辅助命令:create(创建变更目录,plan 阶段的前置步骤)
|
||||
#
|
||||
# 可配置阶段:
|
||||
# discuss - 探索阶段:深度思考、调查代码库、对比方案
|
||||
# create - 创建阶段:拟定变更名称并创建变更目录
|
||||
# plan - 规划阶段:生成设计文档和任务清单
|
||||
# build - 构建阶段:按任务清单逐步实现
|
||||
# archive - 归档阶段:确认完成并归档变更
|
||||
|
||||
@@ -16,15 +16,6 @@ export function assembleDiscussPrompt(config: RuneConfig): string {
|
||||
return applyCommandPrefix(discuss.prompt, config);
|
||||
}
|
||||
|
||||
export function assembleCreatePrompt(config: RuneConfig): string {
|
||||
const create = config.stages.create;
|
||||
if (!create)
|
||||
throw new CommandError("创建阶段未配置", {
|
||||
hint: "请在 .rune/config.yaml 中配置 stages.create",
|
||||
});
|
||||
return applyCommandPrefix(create.prompt, config);
|
||||
}
|
||||
|
||||
export async function assemblePlanPrompt(
|
||||
config: RuneConfig,
|
||||
projectRoot: string,
|
||||
|
||||
@@ -91,7 +91,7 @@ export function validateConfig(config: RuneConfig): void {
|
||||
|
||||
function mergeConfig(userConfig: Partial<RuneConfig>): RuneConfig {
|
||||
const result: RuneConfig = { stages: {} };
|
||||
const stageKeys = ["discuss", "create", "plan", "build", "archive"] as const;
|
||||
const stageKeys = ["discuss", "plan", "build", "archive"] as const;
|
||||
|
||||
for (const stage of stageKeys) {
|
||||
if (userConfig.stages?.[stage]) {
|
||||
|
||||
@@ -232,14 +232,6 @@ rune status
|
||||
|
||||
除非……有同步组件?
|
||||
\`\`\``,
|
||||
},
|
||||
create: {
|
||||
prompt: `请根据讨论内容,拟定一个简短、有意义的变更名称,然后执行 create 命令创建变更目录。
|
||||
|
||||
要求:
|
||||
- 变更名称应简洁明了,能概括变更的核心内容
|
||||
- 仅支持中文、英文和短横线(-)
|
||||
- 创建成功后,引导用户使用 /rune-plan <变更名> <文档名> 进入规划阶段`,
|
||||
},
|
||||
plan: {
|
||||
documents: [
|
||||
|
||||
@@ -15,10 +15,6 @@ export interface DiscussStage {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface CreateStage {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface PlanStage {
|
||||
documents: DocumentConfig[];
|
||||
}
|
||||
@@ -33,7 +29,6 @@ export interface ArchiveStage {
|
||||
|
||||
export interface StagesConfig {
|
||||
discuss?: DiscussStage;
|
||||
create?: CreateStage;
|
||||
plan?: PlanStage;
|
||||
build?: BuildStage;
|
||||
archive?: ArchiveStage;
|
||||
@@ -60,7 +55,7 @@ export interface ChangeStatus {
|
||||
taskProgress: { completed: number; total: number } | null;
|
||||
}
|
||||
|
||||
export const STAGES = ["discuss", "create", "plan", "build", "archive"] as const;
|
||||
export const STAGES = ["discuss", "plan", "build", "archive"] as const;
|
||||
export type Stage = (typeof STAGES)[number];
|
||||
|
||||
export const RUNE_DIR = ".rune";
|
||||
|
||||
108
tests/cli/create.test.ts
Normal file
108
tests/cli/create.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { runInit } from "../../src/commands/init.ts";
|
||||
import { loadConfig, getChangeDir } from "../../src/core/config.ts";
|
||||
import { assemblePlanPrompt } from "../../src/core/assembler.ts";
|
||||
import { CommandError } from "../../src/cli/errors.ts";
|
||||
|
||||
const TMP_DIR = join(import.meta.dir, "__tmp_create_test__");
|
||||
|
||||
beforeEach(async () => {
|
||||
await mkdir(TMP_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(TMP_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("create 命令(工具命令,非 SDD 阶段)", () => {
|
||||
it("创建变更目录成功", async () => {
|
||||
await runInit(TMP_DIR, ["opencode"]);
|
||||
const changeDir = getChangeDir(TMP_DIR, "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
expect(existsSync(changeDir)).toBe(true);
|
||||
});
|
||||
|
||||
it("重复创建同名变更目录应报错", async () => {
|
||||
await runInit(TMP_DIR, ["opencode"]);
|
||||
const changeDir = getChangeDir(TMP_DIR, "duplicate-test");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
expect(existsSync(changeDir)).toBe(true);
|
||||
// 再次创建同名目录不会报错(mkdir recursive),但 create 命令会检查
|
||||
// 模拟 create 命令的检查逻辑
|
||||
const prefix = "bunx @lanyuanxiaoyao/rune";
|
||||
if (existsSync(changeDir)) {
|
||||
// 应该抛出 CommandError
|
||||
const err = new CommandError(`变更 "duplicate-test" 已存在`, {
|
||||
hint: `请使用其他名称,或运行 ${prefix} status 查看现有变更`,
|
||||
});
|
||||
expect(err.message).toContain("duplicate-test");
|
||||
expect(err.message).toContain("已存在");
|
||||
}
|
||||
});
|
||||
|
||||
it("create 后不再输出阶段提示词内容", async () => {
|
||||
await runInit(TMP_DIR, ["opencode"]);
|
||||
// 验证 create 不在 stages 配置中
|
||||
const config = await loadConfig(TMP_DIR);
|
||||
expect(config.stages.create).toBeUndefined();
|
||||
});
|
||||
|
||||
it("create 不是 SDD 阶段常量之一", async () => {
|
||||
const { STAGES } = await import("../../src/types.ts");
|
||||
expect(STAGES).not.toContain("create");
|
||||
expect(STAGES).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plan 命令前置检查", () => {
|
||||
it("plan 在变更目录不存在时报错并提示 create", async () => {
|
||||
await runInit(TMP_DIR, ["opencode"]);
|
||||
const config = await loadConfig(TMP_DIR);
|
||||
|
||||
// 模拟 plan 命令检查:目录不存在
|
||||
const changeDir = getChangeDir(TMP_DIR, "nonexistent-change");
|
||||
expect(existsSync(changeDir)).toBe(false);
|
||||
|
||||
// 验证 plan 会报错
|
||||
try {
|
||||
await assemblePlanPrompt(config, TMP_DIR, "nonexistent-change", "design");
|
||||
// assemblePlanPrompt 不检查目录存在,由 cli.ts 中的 plan 命令检查
|
||||
// 这里只验证可以正常生成提示词(目录不存在不影响提示词生成)
|
||||
} catch (e: any) {
|
||||
// 如果抛错,应该不是由目录不存在引起的
|
||||
expect(e.message).not.toContain("不存在");
|
||||
}
|
||||
});
|
||||
|
||||
it("plan 在变更目录存在时能正常生成提示词", async () => {
|
||||
await runInit(TMP_DIR, ["opencode"]);
|
||||
const config = await loadConfig(TMP_DIR);
|
||||
|
||||
const changeDir = getChangeDir(TMP_DIR, "existing-change");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
|
||||
const prompt = await assemblePlanPrompt(config, TMP_DIR, "existing-change", "design");
|
||||
expect(prompt).toBeTruthy();
|
||||
expect(prompt).toContain("existing-change");
|
||||
expect(prompt).toContain("design");
|
||||
});
|
||||
});
|
||||
|
||||
describe("变更名校验", () => {
|
||||
it("合法变更名通过", () => {
|
||||
const validRegex = /^[\u4e00-\u9fa5a-zA-Z-]+$/;
|
||||
expect(validRegex.test("user-auth")).toBe(true);
|
||||
expect(validRegex.test("用户登录")).toBe(true);
|
||||
expect(validRegex.test("中文-english")).toBe(true);
|
||||
});
|
||||
|
||||
it("非法变更名被拒绝", () => {
|
||||
const validRegex = /^[\u4e00-\u9fa5a-zA-Z-]+$/;
|
||||
expect(validRegex.test("my change")).toBe(false);
|
||||
expect(validRegex.test("my_change")).toBe(false);
|
||||
expect(validRegex.test("")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -83,7 +83,7 @@ describe("runInit", () => {
|
||||
expect(content).toContain("tracked");
|
||||
});
|
||||
|
||||
it("config.yaml 模板包含 create 阶段", async () => {
|
||||
it("config.yaml 模板包含 create 命令说明", async () => {
|
||||
await runInit(TMP_DIR, ["opencode"]);
|
||||
|
||||
const content = await readFile(join(TMP_DIR, ".rune", "config.yaml"), "utf-8");
|
||||
|
||||
@@ -3,14 +3,12 @@ import { mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
assembleDiscussPrompt,
|
||||
assembleCreatePrompt,
|
||||
assemblePlanPrompt,
|
||||
assembleBuildPrompt,
|
||||
assembleArchivePrompt,
|
||||
} from "../../src/core/assembler.ts";
|
||||
import type { RuneConfig } from "../../src/types.ts";
|
||||
import { defaultConfig } from "../../src/defaults/config.ts";
|
||||
import { CommandError } from "../../src/cli/errors.ts";
|
||||
|
||||
const TMP_DIR = join(import.meta.dir, "__tmp_assembler_test__");
|
||||
|
||||
@@ -40,35 +38,6 @@ describe("assembleDiscussPrompt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("assembleCreatePrompt", () => {
|
||||
it("返回默认 create 提示词", () => {
|
||||
const prompt = assembleCreatePrompt(defaultConfig);
|
||||
expect(prompt).toBeTruthy();
|
||||
expect(prompt).toContain("变更名称");
|
||||
expect(prompt).toContain("/rune-plan");
|
||||
});
|
||||
|
||||
it("返回自定义 create 提示词", () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { create: { prompt: "自定义创建" } },
|
||||
};
|
||||
const prompt = assembleCreatePrompt(config);
|
||||
expect(prompt).toBe("自定义创建");
|
||||
});
|
||||
|
||||
it("create 阶段未配置时抛出 CommandError", () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { build: { prompt: "构建" } },
|
||||
};
|
||||
try {
|
||||
assembleCreatePrompt(config);
|
||||
expect.unreachable();
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(CommandError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("assemblePlanPrompt", () => {
|
||||
it("包含指定文档名称和提示词", async () => {
|
||||
const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design");
|
||||
|
||||
@@ -2,19 +2,13 @@ import { describe, it, expect } from "bun:test";
|
||||
import { defaultConfig } from "../../src/defaults/config.ts";
|
||||
|
||||
describe("defaultConfig", () => {
|
||||
it("包含所有五个阶段的配置", () => {
|
||||
it("包含所有四个阶段的配置", () => {
|
||||
expect(defaultConfig.stages.discuss).toBeDefined();
|
||||
expect(defaultConfig.stages.create).toBeDefined();
|
||||
expect(defaultConfig.stages.plan).toBeDefined();
|
||||
expect(defaultConfig.stages.build).toBeDefined();
|
||||
expect(defaultConfig.stages.archive).toBeDefined();
|
||||
});
|
||||
|
||||
it("包含 create 阶段的配置", () => {
|
||||
expect(defaultConfig.stages.create).toBeDefined();
|
||||
expect(defaultConfig.stages.create!.prompt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("discuss 阶段有 prompt", () => {
|
||||
expect(defaultConfig.stages.discuss!.prompt).toBeTruthy();
|
||||
expect(typeof defaultConfig.stages.discuss!.prompt).toBe("string");
|
||||
|
||||
Reference in New Issue
Block a user