diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6adc783..ca89a32 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -92,7 +92,7 @@ CLI 通过子命令提供帮助和版本信息,不使用 `--help`/`--version` ### 阶段与配置 -- **四阶段固定**:discuss → plan → build → archive,不可自定义增删 +- **五阶段固定**:discuss → plan → task → build → archive,不可自定义增删 - **配置覆盖策略**:阶段级别全量覆盖,不支持字段级合并。自定义 plan 时需完整重写所有 documents - **配置文件名**:`.rune/config.yaml`,不是 `rune.yml` - **文档模板**:纯静态文本模板,不进行变量替换 @@ -101,6 +101,7 @@ CLI 通过子命令提供帮助和版本信息,不使用 `--help`/`--version` - **discuss**:不持久化讨论结果,完全依赖 AI 会话上下文传递;不设强制门控,通过提示词引导。讨论结束时引导用户运行 `rune create` 创建变更目录 - **plan**:命令只输出提示词,不写入文件;AI 负责根据提示词生成文档内容并写入。新变更时引导用户先运行 `rune create` 创建目录。重复调用同一文档的 plan 会追加已有内容用于增量修订。依赖未满足时有友好提示(非报错) +- **task**:根据规划文档内容拆分为 checkbox 任务清单;格式固定,不可自定义模板;规划文档必须全部完成才能进入 - **build**:按 task.md 的 checkbox 顺序执行;任务间无结构化依赖;可多次执行直到全部完成 - **archive**:输出归档提示词(含未完成任务的警告),引导 AI 汇总变更并确认。`finish` 命令执行实际的目录移动 diff --git a/README.md b/README.md index 8474aa5..c8707a1 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,13 @@ bunx @lanyuanxiaoyao/rune update claude-code # 更新 Claude Code 的命令 ### SDD 流程 -SDD 工作流包含固定的四个阶段,不可自定义增删: +SDD 工作流包含固定的五个阶段,不可自定义增删: 1. **讨论阶段** — `/rune-discuss`:与 AI 自由讨论需求和方案。讨论结果保留在 AI 会话上下文中传递到后续阶段,不持久化到文件。结束前会引导是否进入规划阶段。 -2. **规划阶段** — `/rune-plan <变更名> <文档名>`:按配置的文档模板生成规划文档。变更名仅支持中文、英文和短横线(`-`)。默认包含 `design`(设计文档)和 `task`(任务清单,依赖 design)两个文档。文档间支持 `depend` 字段声明前置依赖,依赖未满足时有友好提示。plan 命令自身不写入文件,只输出提示词供 AI 消费。 -3. **构建阶段** — `/rune-build <变更名>`:按 task.md 中的任务顺序逐个实现。每个任务完成后更新对应的 checkbox 为 `[x]`。可多次执行直到所有任务完成。 -4. **归档阶段** — `/rune-archive <变更名>`:输出归档阶段提示词,引导 AI 汇总变更内容并确认归档。归档前自动检查 task.md 的完成状态,如有未完成任务会注入警告提示词,引导 AI 询问用户是否确认。确认后执行 `rune finish <变更名>` 将变更目录移动到 `archive/`。 +2. **规划阶段** — `/rune-plan <变更名> <文档名>`:按配置的文档模板生成规划文档。变更名仅支持中文、英文和短横线(`-`)。默认包含 `design`(设计文档)一个文档。文档间支持 `depend` 字段声明前置依赖,依赖未满足时有友好提示。plan 命令自身不写入文件,只输出提示词供 AI 消费。 +3. **任务拆解阶段** — `/rune-task <变更名>`:根据规划阶段生成的文档内容,拆分为 checkbox 格式的任务清单(task.md)。规划阶段的所有文档必须已完成。格式固定为 checkbox 列表(`- [ ] 待完成`、`- [x] 已完成`),不可自定义模板。 +4. **构建阶段** — `/rune-build <变更名>`:按 task.md 中的任务顺序逐个实现。每个任务完成后更新对应的 checkbox 为 `[x]`。可多次执行直到所有任务完成。 +5. **归档阶段** — `/rune-archive <变更名>`:输出归档阶段提示词,引导 AI 汇总变更内容并确认归档。归档前自动检查 task.md 的完成状态,如有未完成任务会注入警告提示词,引导 AI 询问用户是否确认。确认后执行 `rune finish <变更名>` 将变更目录移动到 `archive/`。 > **辅助命令**:`rune create <变更名>` 用于在 `.rune/changes/` 下创建变更目录。它不是 SDD 阶段,而是在 discuss 结束后或 plan 开始前通过 CLI 运行的辅助命令。discuss 和 plan 的编辑器命令中已内嵌 create 的使用引导。 @@ -70,6 +71,7 @@ bunx @lanyuanxiaoyao/rune version # 显示版本号 | `bunx @lanyuanxiaoyao/rune discuss` | 输出讨论阶段提示词 | | `bunx @lanyuanxiaoyao/rune create <变更名>` | 创建变更目录(discuss 和 plan 之间的辅助命令) | | `bunx @lanyuanxiaoyao/rune plan <变更名> <文档名>` | 输出规划阶段提示词 | +| `bunx @lanyuanxiaoyao/rune task <变更名>` | 输出任务拆解阶段提示词 | | `bunx @lanyuanxiaoyao/rune build <变更名>` | 输出构建阶段提示词 | | `bunx @lanyuanxiaoyao/rune archive <变更名>` | 输出归档阶段提示词 | | `bunx @lanyuanxiaoyao/rune finish <变更名>` | 归档变更(将变更目录移动到 archive/) | @@ -79,7 +81,7 @@ bunx @lanyuanxiaoyao/rune version # 显示版本号 编辑 `.rune/config.yaml` 自定义各阶段的提示词和文档模板。配置合并采用阶段级别全量覆盖策略:自定义某个阶段时需完整重写该阶段的配置,未配置的阶段使用内置默认配置。 -规划阶段的文档支持 `depend` 字段声明前置依赖,如 `task` 依赖 `design`: +规划阶段的文档支持 `depend` 字段声明前置依赖: ```yaml stages: @@ -87,8 +89,8 @@ stages: documents: - name: design prompt: 生成设计文档,包含背景、目标、方案、接口和注意事项 - - name: task - prompt: 生成任务清单,将设计拆分为可独立执行的小任务 + - name: api + prompt: 生成 API 设计文档 depend: [design] ``` @@ -96,7 +98,7 @@ stages: ## 设计决策 -- **四阶段固定**:discuss → plan → build → archive 不可自定义增删 +- **五阶段固定**:discuss → plan → task → build → archive 不可自定义增删 - **配置覆盖策略**:阶段级别全量覆盖,不支持字段级合并 - **讨论结果不持久化**:完全依赖 AI 会话上下文传递 - **plan 不写文件**:plan 命令只输出提示词,由 AI 负责写入文档 diff --git a/src/adapters/claude-code.ts b/src/adapters/claude-code.ts index 3467e93..8247551 100644 --- a/src/adapters/claude-code.ts +++ b/src/adapters/claude-code.ts @@ -6,7 +6,7 @@ import { writeIfChanged } from "./utils.ts"; const COMMANDS_DIR = ".claude/commands"; -const STAGES_WITH_CHANGE_NAME = new Set(["plan", "build", "archive"]); +const STAGES_WITH_CHANGE_NAME = new Set(["plan", "task", "build", "archive"]); function buildSmartGuide(command: string): string { return `如果用户没有指定变更名称,请按以下步骤智能识别: @@ -33,6 +33,9 @@ export async function injectClaudeCode( if (stage === "plan") { content += `\n如果变更目录尚不存在(新变更),请先运行 \`${command} create <变更名>\` 创建目录,再开始规划。`; } + if (stage === "task") { + content += `\n任务拆解前请确认规划文档已全部完成,运行 \`${command} status <变更名>\` 检查。`; + } if (stage === "discuss") { content += `\n讨论结束后,如果确定了变更方向,请运行 \`${command} create <变更名>\` 创建变更目录,然后进入规划阶段。`; } @@ -63,6 +66,9 @@ export async function updateClaudeCode( if (stage === "plan") { newContent += `\n如果变更目录尚不存在(新变更),请先运行 \`${command} create <变更名>\` 创建目录,再开始规划。`; } + if (stage === "task") { + newContent += `\n任务拆解前请确认规划文档已全部完成,运行 \`${command} status <变更名>\` 检查。`; + } if (stage === "discuss") { newContent += `\n讨论结束后,如果确定了变更方向,请运行 \`${command} create <变更名>\` 创建变更目录,然后进入规划阶段。`; } @@ -76,11 +82,12 @@ export async function updateClaudeCode( function generateIntroCommand(command: string): string { return `Rune 是基于规格驱动开发(SDD)的 AI 开发辅助工具。SDD 工作流程: -discuss → plan → build → archive +discuss → plan → task → build → archive 可用命令: - /rune-discuss — 自由讨论需求和方案 -- /rune-plan — 生成设计文档和任务清单(新变更需先运行 \`${command} create <变更名>\` 创建目录) +- /rune-plan — 生成设计文档(新变更需先运行 \`${command} create <变更名>\` 创建目录) +- /rune-task — 根据设计文档生成任务清单 - /rune-build — 按任务清单逐步实现 - /rune-archive — 归档已完成的变更 diff --git a/src/adapters/opencode.ts b/src/adapters/opencode.ts index 0e5abb2..8988e8b 100644 --- a/src/adapters/opencode.ts +++ b/src/adapters/opencode.ts @@ -7,7 +7,7 @@ import { writeIfChanged } from "./utils.ts"; const COMMANDS_DIR = ".opencode/commands"; const SKILLS_DIR = ".opencode/skills"; -const STAGES_WITH_CHANGE_NAME = new Set(["plan", "build", "archive"]); +const STAGES_WITH_CHANGE_NAME = new Set(["plan", "task", "build", "archive"]); export async function injectOpenCode(projectRoot: string, command: string = "rune"): Promise { for (const stage of STAGES) { @@ -67,6 +67,10 @@ function generateSkill(stage: string, command: string): string { extraGuide = `\n规划阶段应先运行 \`${command} status <变更名>\` 获取当前有哪些文档需要编写,再按依赖顺序逐个生成。\n\n如果变更目录尚不存在(新变更),请先运行 \`${command} create <变更名>\` 创建目录,再开始规划。\n`; } + if (stage === "task") { + extraGuide = `\n任务拆解阶段应先运行 \`${command} status <变更名>\` 确认规划文档已全部完成,再生成任务清单。\n`; + } + if (stage === "discuss") { extraGuide = `\n讨论结束后,如果确定了变更方向,请运行 \`${command} create <变更名>\` 创建变更目录,然后进入规划阶段。\n`; } @@ -74,6 +78,7 @@ function generateSkill(stage: string, command: string): string { const descriptionMap: Record = { discuss: "Use when 需要进入 SDD 讨论阶段,自由讨论需求和架构方案", plan: "Use when 需要进入 SDD 规划阶段,生成设计文档和任务清单", + task: "Use when 需要进入 SDD 任务拆解阶段,根据设计文档生成任务清单", build: "Use when 需要进入 SDD 构建阶段,按任务清单逐步实现变更", archive: "Use when 需要进入 SDD 归档阶段,确认变更完成并归档", }; @@ -118,8 +123,8 @@ Rune 是基于规格驱动开发(SDD)的 AI 开发辅助工具。它通过 ## SDD 工作流程 \`\`\` -discuss → plan → build → archive - 探索 规划 构建 归档 +discuss → plan → task → build → archive + 探索 规划 任务 构建 归档 \`\`\` ## 可用命令 @@ -128,6 +133,7 @@ discuss → plan → build → archive |------|-----------|------| | discuss | /rune-discuss | 自由讨论需求和方案 | | plan | /rune-plan | 生成设计文档和任务清单 | +| task | /rune-task | 根据设计文档生成任务清单 | | build | /rune-build | 按任务清单逐步实现 | | archive | /rune-archive | 归档已完成的变更 | @@ -140,8 +146,9 @@ ${command} status ## 快速开始 1. 使用 /rune-discuss 进入讨论,自由探索需求 -2. 讨论结束后,运行 \`${command} create <变更名>\` 创建变更目录,然后用 /rune-plan 生成设计文档和任务清单 -3. 使用 /rune-build 按任务顺序实现功能 -4. 使用 /rune-archive 归档已完成的变更 +2. 讨论结束后,运行 \`${command} create <变更名>\` 创建变更目录,然后用 /rune-plan 生成设计文档 +3. 使用 /rune-task 根据设计文档生成任务清单 +4. 使用 /rune-build 按任务顺序实现功能 +5. 使用 /rune-archive 归档已完成的变更 `; } diff --git a/src/cli.ts b/src/cli.ts index fa09c00..c436018 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,6 +8,7 @@ import { findProjectRoot, loadConfig, getChangeDir, getArchiveDir } from "./core import { assembleDiscussPrompt, assemblePlanPrompt, + assembleTaskPrompt, assembleBuildPrompt, assembleArchivePrompt, } from "./core/assembler.ts"; @@ -83,15 +84,15 @@ export function suggestNextStep(change: ChangeStatus, config?: RuneConfig): stri return `完成前置依赖后再规划文档`; } - if (change.taskProgress && change.taskProgress.completed < change.taskProgress.total) { + if (!change.taskProgress) { + return `${prefix} task ${change.name}`; + } + + if (change.taskProgress.completed < change.taskProgress.total) { return `${prefix} build ${change.name}`; } - if (change.taskProgress && change.taskProgress.completed === change.taskProgress.total) { - return `${prefix} archive ${change.name}`; - } - - return `${prefix} build ${change.name}`; + return `${prefix} archive ${change.name}`; } const cli = cac("rune"); @@ -257,6 +258,21 @@ cli console.log(prompt); }); +cli.command("task ", "任务拆解阶段").action(async (changeName: string) => { + validateChangeName(changeName); + const root = requireProjectRoot(); + const changeDir = getChangeDir(root, changeName); + if (!existsSync(changeDir)) { + const prefix = getPmPrefix(); + throw new CommandError(`变更"${changeName}"不存在`, { + hint: `请先运行 ${prefix} create ${changeName} 创建变更`, + }); + } + const config = await loadConfig(root); + const prompt = await assembleTaskPrompt(config, root, changeName); + console.log(prompt); +}); + cli.command("build ", "构建阶段").action(async (changeName: string) => { validateChangeName(changeName); const root = requireProjectRoot(); diff --git a/src/cli/help.ts b/src/cli/help.ts index c20c329..55e5d7d 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -44,11 +44,21 @@ const COMMANDS: Record = { usageTemplate: "plan ", args: [ { name: "", desc: '变更名称,如 "add-login"' }, - { name: "", desc: '文档名称,如 "design"、"task"' }, + { name: "", desc: '文档名称,如 "design"' }, ], detail: "生成规划阶段指定文档的提示词。依赖的前置文档必须已完成。可用文档由配置中的 plan.documents 定义。", - exampleArgs: ["plan add-user-auth design", "plan add-user-auth task"], + exampleArgs: ["plan add-user-auth design", "plan add-user-auth design"], + }, + task: { + name: "task", + alias: "task <变更>", + description: "任务拆解:根据规划文档生成任务清单", + usageTemplate: "task ", + args: [{ name: "", desc: '变更名称,如 "add-login"' }], + detail: + "根据规划阶段的文档内容,生成 checkbox 格式的任务清单(task.md)。规划阶段的所有文档必须已完成。", + exampleArgs: ["task add-user-auth", "task fix-memory-leak"], }, build: { name: "build", @@ -56,7 +66,8 @@ const COMMANDS: Record = { description: "构建:生成构建阶段提示词", usageTemplate: "build ", args: [{ name: "", desc: '变更名称,如 "add-login"' }], - detail: "生成构建阶段的提示词。变更目录需已存在(通过 create 创建)。", + detail: + "按 task.md 中的任务顺序逐步实现。需先完成任务拆解阶段(task)。可多次执行直到全部完成。", exampleArgs: ["build add-user-auth", "build fix-memory-leak"], }, archive: { diff --git a/src/commands/init.ts b/src/commands/init.ts index cd0acce..f6f1c8a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -11,12 +11,13 @@ import { parse as parseYaml } from "yaml"; const CONFIG_TEMPLATE = `# Rune 配置文件 # # 未配置的阶段将使用内置默认配置。 -# SDD 四阶段:discuss -> plan -> build -> archive +# SDD 五阶段:discuss -> plan -> task -> build -> archive # 辅助命令:create(创建变更目录,plan 阶段的前置步骤) # # 可配置阶段: # discuss - 探索阶段:深度思考、调查代码库、对比方案 -# plan - 规划阶段:生成设计文档和任务清单 +# plan - 规划阶段:生成设计文档 +# task - 任务拆解阶段:根据设计文档生成 checkbox 任务清单 # build - 构建阶段:按任务清单逐步实现 # archive - 归档阶段:确认完成并归档变更 # @@ -34,15 +35,9 @@ const CONFIG_TEMPLATE = `# Rune 配置文件 # prompt: 生成设计文档 # template: | # # 设计文档 -# - name: task -# prompt: 生成任务清单 -# depend: [design] -# template: | -# # 任务清单 # # metadata 说明: # command - Rune CLI 执行命令(如 bunx @lanyuanxiaoyao/rune),init 时自动检测 -# tracked - 是否启用任务追踪(默认 true),开启时 plan.documents 必须包含 task 文档 `; export const SUPPORTED_TOOLS: Record Promise> = { diff --git a/src/core/assembler.ts b/src/core/assembler.ts index 623ba89..348da25 100644 --- a/src/core/assembler.ts +++ b/src/core/assembler.ts @@ -71,6 +71,60 @@ export async function assemblePlanPrompt( return applyCommandPrefix(parts.join("\n"), config); } +export async function assembleTaskPrompt( + config: RuneConfig, + projectRoot: string, + changeName: string, +): Promise { + const task = config.stages.task; + if (!task) { + throw new CommandError("任务拆解阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.task", + }); + } + + const plan = config.stages.plan; + if (!plan) { + throw new CommandError("规划阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.plan", + }); + } + + const changeDir = getChangeDir(projectRoot, changeName); + const missingDocs: string[] = []; + + for (const doc of plan.documents) { + const docPath = join(changeDir, `${doc.name}.md`); + if (!existsSync(docPath)) { + missingDocs.push(`${doc.name}.md`); + } + } + + if (missingDocs.length > 0) { + throw new CommandError(`变更"${changeName}"的规划文档尚未全部完成:${missingDocs.join("、")}`, { + hint: `请先完成规划阶段:rune plan ${changeName} ${missingDocs[0].replace(".md", "")}`, + }); + } + + const parts: string[] = []; + parts.push(`# 任务拆解阶段:${changeName}\n`); + parts.push(task.prompt); + + const planDocPaths = plan.documents.map((d) => join(changeDir, `${d.name}.md`)); + parts.push(`\n请先读取以下规划文档:`); + for (const p of planDocPaths) { + parts.push(`- ${p}`); + } + + const taskPath = join(changeDir, "task.md"); + if (existsSync(taskPath)) { + parts.push(`\ntask.md 已存在,请先读取 ${taskPath} 查看已有内容,在此基础上修订。`); + } + + parts.push(`\n请将任务列表写入 ${taskPath}`); + return applyCommandPrefix(parts.join("\n"), config); +} + export async function assembleBuildPrompt( config: RuneConfig, projectRoot: string, @@ -83,10 +137,6 @@ export async function assembleBuildPrompt( }); } - if (!config.metadata?.tracked) { - return applyCommandPrefix(build.prompt, config); - } - const changeDir = getChangeDir(projectRoot, changeName); const taskPath = join(changeDir, "task.md"); @@ -95,8 +145,8 @@ export async function assembleBuildPrompt( taskContent = await readFile(taskPath, "utf-8"); } catch { const prefix = getPmPrefix(config); - throw new CommandError(`变更"${changeName}"尚未完成规划,task.md 不存在`, { - hint: `请先完成规划阶段:${prefix} plan ${changeName} task 生成任务文档`, + throw new CommandError(`变更"${changeName}"尚未完成任务拆解,task.md 不存在`, { + hint: `请先完成任务拆解阶段:${prefix} task ${changeName}`, }); } @@ -142,30 +192,28 @@ export async function assembleArchivePrompt( const parts: string[] = []; parts.push(`# 归档阶段:${changeName}\n`); - if (config.metadata?.tracked) { - const changeDir = getChangeDir(projectRoot, changeName); - const taskPath = join(changeDir, "task.md"); - if (existsSync(taskPath)) { - try { - const taskContent = await readFile(taskPath, "utf-8"); - const tasks = parseTasks(taskContent); - const incompleteTasks = tasks.filter((t) => !t.checked); - if (incompleteTasks.length > 0) { - parts.push("## ⚠️ 警告:存在未完成的任务\n"); - parts.push("以下任务尚未完成:"); - for (const t of incompleteTasks) { - parts.push(`- [ ] ${t.text}`); - } - parts.push(""); - parts.push("询问用户是否确认在任务未全部完成的情况下归档。"); - parts.push("如用户确认,则继续执行归档操作;否则中止并返回构建阶段。"); - parts.push(""); - } - } catch (e) { - const code = (e as NodeJS.ErrnoException)?.code; - if (code !== "ENOENT" && code !== "ENOTDIR") { - throw e; + const changeDir = getChangeDir(projectRoot, changeName); + const taskPath = join(changeDir, "task.md"); + if (existsSync(taskPath)) { + try { + const taskContent = await readFile(taskPath, "utf-8"); + const tasks = parseTasks(taskContent); + const incompleteTasks = tasks.filter((t) => !t.checked); + if (incompleteTasks.length > 0) { + parts.push("## ⚠️ 警告:存在未完成的任务\n"); + parts.push("以下任务尚未完成:"); + for (const t of incompleteTasks) { + parts.push(`- [ ] ${t.text}`); } + parts.push(""); + parts.push("询问用户是否确认在任务未全部完成的情况下归档。"); + parts.push("如用户确认,则继续执行归档操作;否则中止并返回构建阶段。"); + parts.push(""); + } + } catch (e) { + const code = (e as NodeJS.ErrnoException)?.code; + if (code !== "ENOENT" && code !== "ENOTDIR") { + throw e; } } } diff --git a/src/core/config.ts b/src/core/config.ts index 478c528..c88802e 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -45,18 +45,6 @@ export function validateConfig(config: RuneConfig): void { const plan = config.stages.plan; if (!plan) return; - if (config.metadata?.tracked) { - const hasTaskDoc = plan.documents.some((d) => d.name === "task"); - if (!hasTaskDoc) { - throw new ConfigError( - "配置校验失败:开启了任务追踪(metadata.tracked)但规划阶段的文档列表中没有 task 文档", - { - hint: `请在 .rune/config.yaml 的 stages.plan.documents 中添加 task 文档:\n - name: task\n prompt: 生成任务清单\n depend: [design]`, - }, - ); - } - } - const docNames = new Set(plan.documents.map((d) => d.name)); for (const doc of plan.documents) { @@ -110,7 +98,7 @@ export function validateConfig(config: RuneConfig): void { function mergeConfig(userConfig: Partial): RuneConfig { const result: RuneConfig = { stages: {} }; - const stageKeys = ["discuss", "plan", "build", "archive"] as const; + const stageKeys = ["discuss", "plan", "task", "build", "archive"] as const; for (const stage of stageKeys) { if (userConfig.stages?.[stage]) { diff --git a/src/core/scanner.ts b/src/core/scanner.ts index 3db7877..c01dad0 100644 --- a/src/core/scanner.ts +++ b/src/core/scanner.ts @@ -44,16 +44,14 @@ export async function scanChanges( const buildUnlocked = planCompleted; let taskProgress: { completed: number; total: number } | null = null; - if (config?.metadata?.tracked) { - const taskFile = files.find((d) => d === "task.md"); - if (taskFile) { - const content = await readFile(join(entryPath, taskFile), "utf-8"); - const tasks = parseTasks(content); - taskProgress = { - completed: tasks.filter((t) => t.checked).length, - total: tasks.length, - }; - } + const taskFile = files.find((d) => d === "task.md"); + if (taskFile) { + const content = await readFile(join(entryPath, taskFile), "utf-8"); + const tasks = parseTasks(content); + taskProgress = { + completed: tasks.filter((t) => t.checked).length, + total: tasks.length, + }; } results.push({ diff --git a/src/defaults/config.ts b/src/defaults/config.ts index 5883251..8e38078 100644 --- a/src/defaults/config.ts +++ b/src/defaults/config.ts @@ -1,9 +1,6 @@ import type { RuneConfig } from "../types.ts"; export const defaultConfig: RuneConfig = { - metadata: { - tracked: true, - }, stages: { discuss: { prompt: `进入探索模式。深度思考,自由发散。跟随对话走向。 @@ -263,28 +260,23 @@ rune status ## 注意事项 `, }, - { - name: "task", - depend: ["design"], - prompt: `请先获取当前规划状态。 + ], + }, + task: { + prompt: `你是一位高级软件工程师,擅长将设计文档拆解为可执行的任务列表。 -建议执行: rune status <变更名> +请先读取变更目录下所有已有的规划文档(如 design.md),理解设计内容。 -请根据设计文档,生成一份任务列表。 +然后将设计拆分为一份任务列表,写入 task.md。 要求: - 将设计拆分为可独立执行的小任务 - 每个任务应该足够具体,能直接编码实现 - 任务之间有合理的依赖顺序 - 使用 checkbox 格式:- [ ] 待完成,- [x] 已完成 +- 格式固定为 checkbox 列表,不需要模板 -请将文档写入指定路径。`, - template: `# 任务列表 - -- [ ] -`, - }, - ], +请将任务列表写入指定路径的 task.md 文件。`, }, build: { prompt: `你是一位高级软件工程师。 @@ -295,7 +287,7 @@ rune status - 每完成一个任务,立即更新 task.md 中对应项为 [x] - 遵循项目现有的代码风格和约定 - 编写必要的测试 -- 完成所有任务后,提示用户可以使用 /archive <变更名> 归档`, +- 完成所有任务后,提示用户可以使用 /rune-archive <变更名> 归档`, }, archive: { prompt: `当前变更已进入归档阶段。 diff --git a/src/types.ts b/src/types.ts index a6133b9..f4436c4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,10 @@ export interface PlanStage { documents: DocumentConfig[]; } +export interface TaskStage { + prompt: string; +} + export interface BuildStage { prompt: string; } @@ -30,6 +34,7 @@ export interface ArchiveStage { export interface StagesConfig { discuss?: DiscussStage; plan?: PlanStage; + task?: TaskStage; build?: BuildStage; archive?: ArchiveStage; } @@ -38,7 +43,6 @@ export interface RuneConfig { stages: StagesConfig; metadata?: { command?: string; - tracked?: boolean; }; } @@ -55,7 +59,7 @@ export interface ChangeStatus { taskProgress: { completed: number; total: number } | null; } -export const STAGES = ["discuss", "plan", "build", "archive"] as const; +export const STAGES = ["discuss", "plan", "task", "build", "archive"] as const; export type Stage = (typeof STAGES)[number]; export const RUNE_DIR = ".rune"; diff --git a/tests/cli/create.test.ts b/tests/cli/create.test.ts index 697e222..14da872 100644 --- a/tests/cli/create.test.ts +++ b/tests/cli/create.test.ts @@ -53,7 +53,7 @@ describe("create 命令(工具命令,非 SDD 阶段)", () => { it("create 不是 SDD 阶段常量之一", async () => { const { STAGES } = await import("../../src/types.ts"); expect(STAGES).not.toContain("create"); - expect(STAGES).toHaveLength(4); + expect(STAGES).toHaveLength(5); }); }); diff --git a/tests/cli/status.test.ts b/tests/cli/status.test.ts index ab716ac..4b9d347 100644 --- a/tests/cli/status.test.ts +++ b/tests/cli/status.test.ts @@ -134,15 +134,12 @@ describe("suggestNextStep", () => { expect(suggestNextStep(status)).toContain("rune archive test-change"); }); - it("规划完成但无 taskProgress 时建议 build", () => { + it("规划完成但无 taskProgress 时建议 task", () => { const status = makeStatus({ - documents: [ - { name: "design", completed: true, dependMet: true }, - { name: "task", completed: true, dependMet: true }, - ], + documents: [{ name: "design", completed: true, dependMet: true }], planCompleted: true, taskProgress: null, }); - expect(suggestNextStep(status)).toContain("rune build test-change"); + expect(suggestNextStep(status)).toContain("rune task test-change"); }); }); diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts index c7f4612..3ca42a9 100644 --- a/tests/commands/init.test.ts +++ b/tests/commands/init.test.ts @@ -80,7 +80,7 @@ describe("runInit", () => { const content = await readFile(join(TMP_DIR, ".rune", "config.yaml"), "utf-8"); expect(content).toContain("metadata"); - expect(content).toContain("tracked"); + expect(content).toContain("command"); }); it("config.yaml 模板包含 create 命令说明", async () => { diff --git a/tests/core/assembler.test.ts b/tests/core/assembler.test.ts index f191761..1ab1823 100644 --- a/tests/core/assembler.test.ts +++ b/tests/core/assembler.test.ts @@ -6,6 +6,7 @@ import { assemblePlanPrompt, assembleBuildPrompt, assembleArchivePrompt, + assembleTaskPrompt, } from "../../src/core/assembler.ts"; import type { RuneConfig } from "../../src/types.ts"; import { defaultConfig } from "../../src/defaults/config.ts"; @@ -150,28 +151,18 @@ describe("assembleBuildPrompt", () => { await assembleBuildPrompt(defaultConfig, TMP_DIR, "nonexistent"); expect.unreachable(); } catch (e: any) { - expect(e.message).toContain("尚未完成规划"); + expect(e.message).toContain("尚未完成任务拆解"); expect(e.message).toContain("nonexistent"); - expect(e.hint).toContain("plan nonexistent"); + expect(e.hint).toContain("task"); } }); - it("tracked=false 时只输出通用提示词", async () => { - const config: RuneConfig = { - stages: { build: { prompt: "按规划文档逐步实现功能" } }, - metadata: { tracked: false }, - }; - const prompt = await assembleBuildPrompt(config, TMP_DIR, "user-auth"); - expect(prompt).toBe("按规划文档逐步实现功能"); - }); - - it("tracked=true 且 task.md 格式不合法时抛错", async () => { + it("task.md 格式不合法时抛错", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile(join(changeDir, "task.md"), "# 标题\n无 checkbox"); const config: RuneConfig = { stages: { build: { prompt: "构建阶段" } }, - metadata: { tracked: true }, }; try { await assembleBuildPrompt(config, TMP_DIR, "user-auth"); @@ -183,13 +174,12 @@ describe("assembleBuildPrompt", () => { } }); - it("tracked=true 且 task.md 有空 checkbox 文本时抛错", async () => { + it("task.md 有空 checkbox 文本时抛错", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile(join(changeDir, "task.md"), "- [ ] \n- [x] 有内容"); const config: RuneConfig = { stages: { build: { prompt: "构建阶段" } }, - metadata: { tracked: true }, }; try { await assembleBuildPrompt(config, TMP_DIR, "user-auth"); @@ -210,44 +200,110 @@ describe("assembleArchivePrompt", () => { expect(prompt).toContain("归档"); }); - it("tracked=false 时不读取 task.md,只输出通用提示词", async () => { - const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); - await mkdir(changeDir, { recursive: true }); - await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务"); - const config: RuneConfig = { - stages: { archive: { prompt: "确认归档" } }, - metadata: { tracked: false }, - }; - const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth"); - expect(prompt).toContain("确认归档"); - expect(prompt).not.toContain("未完成"); - }); - - it("tracked=true 时内嵌未完成任务列表并引导用户确认", async () => { - const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); - await mkdir(changeDir, { recursive: true }); - await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务"); + it("未完成任务时注入警告并引导用户确认", async () => { const config: RuneConfig = { stages: { archive: { prompt: "归档阶段" } }, - metadata: { tracked: true }, }; + const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); + await mkdir(changeDir, { recursive: true }); + await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务"); const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth"); expect(prompt).toContain("未完成"); expect(prompt).toContain("未完成任务"); expect(prompt).toContain("是否确认"); }); - it("tracked=true 且所有任务完成时不注入警告", async () => { + it("所有任务完成时不注入警告", async () => { + const config: RuneConfig = { + stages: { archive: { prompt: "归档阶段" } }, + }; const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile(join(changeDir, "task.md"), "- [x] 已完成任务"); - const config: RuneConfig = { - stages: { archive: { prompt: "归档阶段" } }, - metadata: { tracked: true }, - }; const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth"); expect(prompt).not.toContain("未完成"); }); + + it("task.md 不存在时不注入警告", async () => { + const config: RuneConfig = { + stages: { archive: { prompt: "归档阶段" } }, + }; + const prompt = await assembleArchivePrompt(config, TMP_DIR, "no-task-change"); + expect(prompt).toContain("归档阶段"); + expect(prompt).not.toContain("警告"); + }); +}); + +describe("assembleTaskPrompt", () => { + it("任务拆解阶段提示词包含变更名和文档路径", async () => { + const config: RuneConfig = { + stages: { + plan: { documents: [{ name: "design", prompt: "生成设计" }] }, + task: { prompt: "拆解任务" }, + }, + }; + const changeDir = join(TMP_DIR, ".rune", "changes", "feature-x"); + await mkdir(changeDir, { recursive: true }); + await writeFile(join(changeDir, "design.md"), "# 设计"); + const prompt = await assembleTaskPrompt(config, TMP_DIR, "feature-x"); + expect(prompt).toContain("feature-x"); + expect(prompt).toContain("design.md"); + }); + + it("task.md 已存在时提示修订", async () => { + const config: RuneConfig = { + stages: { + plan: { documents: [{ name: "design", prompt: "生成设计" }] }, + task: { prompt: "拆解任务" }, + }, + }; + const changeDir = join(TMP_DIR, ".rune", "changes", "revise-task"); + await mkdir(changeDir, { recursive: true }); + await writeFile(join(changeDir, "design.md"), "# 设计"); + await writeFile(join(changeDir, "task.md"), "- [ ] 已有任务"); + const prompt = await assembleTaskPrompt(config, TMP_DIR, "revise-task"); + expect(prompt).toContain("已有内容"); + }); + + it("task 阶段未配置时抛错", async () => { + const config: RuneConfig = { + stages: { plan: { documents: [{ name: "design", prompt: "生成设计" }] } }, + }; + try { + await assembleTaskPrompt(config, TMP_DIR, "test"); + expect.unreachable(); + } catch (e: any) { + expect(e.message).toContain("任务拆解阶段未配置"); + } + }); + + it("plan 阶段未配置时抛错", async () => { + const config: RuneConfig = { + stages: { task: { prompt: "拆解任务" } }, + }; + try { + await assembleTaskPrompt(config, TMP_DIR, "test"); + expect.unreachable(); + } catch (e: any) { + expect(e.message).toContain("规划阶段未配置"); + } + }); + + it("plan 文档未完成时抛错", async () => { + const config: RuneConfig = { + stages: { + plan: { documents: [{ name: "design", prompt: "生成设计" }] }, + task: { prompt: "拆解任务" }, + }, + }; + try { + await assembleTaskPrompt(config, TMP_DIR, "incomplete-plan"); + expect.unreachable(); + } catch (e: any) { + expect(e.message).toContain("规划文档"); + expect(e.message).toContain("尚未全部完成"); + } + }); }); describe("命令前缀替换", () => { @@ -273,13 +329,13 @@ describe("命令前缀替换", () => { it("assembleBuildPrompt 错误提示使用动态前缀", async () => { const config: RuneConfig = { stages: { build: { prompt: "构建阶段" } }, - metadata: { command: "pnpx @lanyuanxiaoyao/rune", tracked: true }, + metadata: { command: "pnpx @lanyuanxiaoyao/rune" }, }; try { await assembleBuildPrompt(config, TMP_DIR, "nonexistent-build"); expect.unreachable(); } catch (e: any) { - expect(e.hint).toContain("pnpx @lanyuanxiaoyao/rune plan nonexistent-build"); + expect(e.hint).toContain("pnpx @lanyuanxiaoyao/rune task nonexistent-build"); } }); diff --git a/tests/core/config.test.ts b/tests/core/config.test.ts index 9a18d9c..6db7ea5 100644 --- a/tests/core/config.test.ts +++ b/tests/core/config.test.ts @@ -39,6 +39,7 @@ describe("loadConfig", () => { const config = await loadConfig(TMP_DIR); expect(config.stages.discuss).toBeDefined(); expect(config.stages.plan).toBeDefined(); + expect(config.stages.task).toBeDefined(); expect(config.stages.build).toBeDefined(); expect(config.stages.archive).toBeDefined(); }); @@ -65,9 +66,7 @@ describe("loadConfig", () => { await mkdir(runeDir, { recursive: true }); await writeFile( join(runeDir, "config.yaml"), - `metadata: - tracked: false -stages: + `stages: plan: documents: - name: spec @@ -86,6 +85,7 @@ stages: const config = await loadConfig(TMP_DIR); expect(config.stages.discuss).toBeDefined(); expect(config.stages.plan).toBeDefined(); + expect(config.stages.task).toBeDefined(); expect(config.stages.build).toBeDefined(); expect(config.stages.archive).toBeDefined(); }); @@ -170,56 +170,6 @@ describe("validateConfig", () => { }; expect(() => validateConfig(config)).not.toThrow(); }); - - it("tracked=true 时 plan.documents 必须包含 task 文档", () => { - const config: RuneConfig = { - stages: { - plan: { - documents: [{ name: "design", prompt: "生成设计" }], - }, - }, - metadata: { tracked: true }, - }; - expect(() => validateConfig(config)).toThrow(ConfigError); - }); - - it("tracked=true 且 plan.documents 包含 task 时不报错", () => { - const config: RuneConfig = { - stages: { - plan: { - documents: [ - { name: "design", prompt: "生成设计" }, - { name: "task", prompt: "生成任务" }, - ], - }, - }, - metadata: { tracked: true }, - }; - expect(() => validateConfig(config)).not.toThrow(); - }); - - it("tracked=false 时 plan.documents 不包含 task 也不报错", () => { - const config: RuneConfig = { - stages: { - plan: { - documents: [{ name: "design", prompt: "生成设计" }], - }, - }, - metadata: { tracked: false }, - }; - expect(() => validateConfig(config)).not.toThrow(); - }); - - it("tracked 未配置时等同于 false,不强制要求 task 文档", () => { - const config: RuneConfig = { - stages: { - plan: { - documents: [{ name: "design", prompt: "生成设计" }], - }, - }, - }; - expect(() => validateConfig(config)).not.toThrow(); - }); }); describe("mergeConfig 保留 metadata", () => { @@ -242,52 +192,28 @@ describe("mergeConfig 保留 metadata", () => { } }); - it("无 metadata 时保留默认 metadata(tracked: true)", async () => { - const tmpDir = join(import.meta.dir, "__tmp_config_nometa_test__"); - await mkdir(tmpDir, { recursive: true }); - try { - const configPath = join(tmpDir, ".rune", "config.yaml"); - await mkdir(join(tmpDir, ".rune"), { recursive: true }); - await writeFile(configPath, `stages:\n discuss:\n prompt: "测试"\n`); - const config = await loadConfig(tmpDir); - expect(config.metadata?.tracked).toBe(true); - } finally { - await rm(tmpDir, { recursive: true, force: true }); - } + it("默认配置包含 task 阶段", async () => { + const config = await loadConfig(TMP_DIR); + expect(config.stages.task).toBeDefined(); + expect(config.stages.task!.prompt).toBeTruthy(); }); - it("用户 metadata 与默认 metadata 深合并", async () => { - const tmpDir = join(import.meta.dir, "__tmp_config_deep_merge__"); - await mkdir(tmpDir, { recursive: true }); - try { - const configPath = join(tmpDir, ".rune", "config.yaml"); - await mkdir(join(tmpDir, ".rune"), { recursive: true }); - await writeFile( - configPath, - `metadata:\n command: "rune"\nstages:\n discuss:\n prompt: "测试"\n`, - ); - const config = await loadConfig(tmpDir); - expect(config.metadata?.command).toBe("rune"); - expect(config.metadata?.tracked).toBe(true); - } finally { - await rm(tmpDir, { recursive: true, force: true }); - } + it("默认 metadata 不包含 tracked", async () => { + const config = await loadConfig(TMP_DIR); + expect((config.metadata as any)?.tracked).toBeUndefined(); }); - it("用户 metadata.tracked 显式覆盖默认值", async () => { - const tmpDir = join(import.meta.dir, "__tmp_config_tracked_override__"); - await mkdir(tmpDir, { recursive: true }); - try { - const configPath = join(tmpDir, ".rune", "config.yaml"); - await mkdir(join(tmpDir, ".rune"), { recursive: true }); - await writeFile( - configPath, - `metadata:\n tracked: false\nstages:\n discuss:\n prompt: "测试"\n`, - ); - const config = await loadConfig(tmpDir); - expect(config.metadata?.tracked).toBe(false); - } finally { - await rm(tmpDir, { recursive: true, force: true }); - } + it("用户配置 task 阶段覆盖默认", async () => { + const runeDir = join(TMP_DIR, ".rune"); + await mkdir(runeDir, { recursive: true }); + await writeFile( + join(runeDir, "config.yaml"), + `stages: + task: + prompt: 自定义任务提示词 +`, + ); + const config = await loadConfig(TMP_DIR); + expect(config.stages.task!.prompt).toBe("自定义任务提示词"); }); }); diff --git a/tests/core/scanner.test.ts b/tests/core/scanner.test.ts index 79c633a..5b92a83 100644 --- a/tests/core/scanner.test.ts +++ b/tests/core/scanner.test.ts @@ -21,31 +21,14 @@ describe("scanChanges", () => { expect(changes).toEqual([]); }); - it("扫描到变更及其文档", async () => { + it("有 task.md 时无条件计算 taskProgress", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "user-auth"), { recursive: true }); await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计"); await writeFile(join(changesDir, "user-auth", "task.md"), `- [x] 任务一\n- [ ] 任务二`); - const config: RuneConfig = { - stages: { - plan: { - documents: [ - { name: "design", prompt: "生成设计" }, - { name: "task", prompt: "生成任务" }, - ], - }, - }, - metadata: { tracked: true }, - }; - const changes = await scanChanges(TMP_DIR, config); + const changes = await scanChanges(TMP_DIR); expect(changes).toHaveLength(1); - expect(changes[0].name).toBe("user-auth"); - const docNames = changes[0].documents.map((d) => `${d.name}.md`); - expect(docNames).toContain("design.md"); - expect(docNames).toContain("task.md"); - expect(changes[0].planCompleted).toBe(true); - expect(changes[0].buildUnlocked).toBe(true); expect(changes[0].taskProgress).toEqual({ completed: 1, total: 2 }); }); @@ -156,59 +139,6 @@ describe("scanChanges", () => { expect(changes[0].planCompleted).toBe(false); expect(changes[0].buildUnlocked).toBe(false); }); - - it("tracked=false 时不扫描 task.md,taskProgress 为 null", async () => { - const changesDir = join(TMP_DIR, ".rune", "changes"); - await mkdir(join(changesDir, "test-change"), { recursive: true }); - await writeFile(join(changesDir, "test-change", "design.md"), "# 设计"); - await writeFile(join(changesDir, "test-change", "task.md"), "- [x] 完成\n- [ ] 未完成"); - const config: RuneConfig = { - stages: { - plan: { - documents: [{ name: "design", prompt: "生成设计" }], - }, - }, - metadata: { tracked: false }, - }; - const results = await scanChanges(TMP_DIR, config); - expect(results[0].taskProgress).toBeNull(); - }); - - it("tracked=true 时扫描 task.md,taskProgress 有值", async () => { - const changesDir = join(TMP_DIR, ".rune", "changes"); - await mkdir(join(changesDir, "test-change2"), { recursive: true }); - await writeFile(join(changesDir, "test-change2", "design.md"), "# 设计"); - await writeFile(join(changesDir, "test-change2", "task.md"), "- [x] 完成\n- [ ] 未完成"); - const config: RuneConfig = { - stages: { - plan: { - documents: [ - { name: "design", prompt: "生成设计" }, - { name: "task", prompt: "生成任务" }, - ], - }, - }, - metadata: { tracked: true }, - }; - const results = await scanChanges(TMP_DIR, config); - expect(results[0].taskProgress).toEqual({ completed: 1, total: 2 }); - }); - - it("tracked 未配置(undefined)时不扫描 task.md", async () => { - const changesDir = join(TMP_DIR, ".rune", "changes"); - await mkdir(join(changesDir, "test-change3"), { recursive: true }); - await writeFile(join(changesDir, "test-change3", "design.md"), "# 设计"); - await writeFile(join(changesDir, "test-change3", "task.md"), "- [x] 完成"); - const config: RuneConfig = { - stages: { - plan: { - documents: [{ name: "design", prompt: "生成设计" }], - }, - }, - }; - const results = await scanChanges(TMP_DIR, config); - expect(results[0].taskProgress).toBeNull(); - }); }); describe("scanArchives", () => { diff --git a/tests/defaults/config.test.ts b/tests/defaults/config.test.ts index b221ae7..094b9f4 100644 --- a/tests/defaults/config.test.ts +++ b/tests/defaults/config.test.ts @@ -2,9 +2,10 @@ 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.plan).toBeDefined(); + expect(defaultConfig.stages.task).toBeDefined(); expect(defaultConfig.stages.build).toBeDefined(); expect(defaultConfig.stages.archive).toBeDefined(); }); @@ -42,25 +43,11 @@ describe("defaultConfig", () => { expect(prompt).toContain("task.md"); }); - it("plan 阶段包含 design 和 task 两个文档配置", () => { + it("plan 阶段包含 design 文档配置", () => { const docs = defaultConfig.stages.plan!.documents; - expect(docs).toHaveLength(2); + expect(docs).toHaveLength(1); expect(docs[0].name).toBe("design"); - expect(docs[1].name).toBe("task"); - for (const doc of docs) { - expect(doc.prompt).toBeTruthy(); - } - }); - - it("plan 的 task 文档配置存在", () => { - const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task"); - expect(taskDoc).toBeDefined(); - expect(taskDoc!.prompt).toBeTruthy(); - }); - - it("task 文档依赖 design", () => { - const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task"); - expect(taskDoc!.depend).toEqual(["design"]); + expect(docs[0].prompt).toBeTruthy(); }); it("design 文档有 template", () => { @@ -69,10 +56,9 @@ describe("defaultConfig", () => { expect(designDoc!.template).toContain("设计文档"); }); - it("task 文档有 template", () => { - const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task"); - expect(taskDoc!.template).toBeTruthy(); - expect(taskDoc!.template).toContain("- [ ]"); + it("task 阶段有 prompt", () => { + expect(defaultConfig.stages.task).toBeDefined(); + expect(defaultConfig.stages.task!.prompt).toBeTruthy(); }); it("build 阶段有 prompt", () => { diff --git a/tests/integration/flow.test.ts b/tests/integration/flow.test.ts index 895d827..016eac1 100644 --- a/tests/integration/flow.test.ts +++ b/tests/integration/flow.test.ts @@ -7,6 +7,7 @@ import { loadConfig, getChangeDir, getArchiveDir } from "../../src/core/config.t import { assembleDiscussPrompt, assemblePlanPrompt, + assembleTaskPrompt, assembleBuildPrompt, assembleArchivePrompt, } from "../../src/core/assembler.ts"; @@ -23,7 +24,7 @@ afterEach(async () => { }); describe("完整 SDD 流程", () => { - it("init → discuss → plan → build → archive 完整流程", async () => { + it("init → discuss → plan → task → build → archive 完整流程", async () => { await runInit(TMP_DIR, ["opencode"]); expect(existsSync(join(TMP_DIR, ".rune", "config.yaml"))).toBe(true); expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"))).toBe(true); @@ -39,6 +40,12 @@ describe("完整 SDD 流程", () => { const changeDir = getChangeDir(TMP_DIR, changeName); await writeFile(join(changeDir, "design.md"), "# 用户认证设计\n\n## 背景\n需要用户登录功能"); + + const taskPrompt = await assembleTaskPrompt(config, TMP_DIR, changeName); + expect(taskPrompt).toContain("user-auth"); + expect(taskPrompt).toContain("task.md"); + expect(taskPrompt).toContain("design.md"); + await writeFile(join(changeDir, "task.md"), "- [ ] 实现登录 API\n- [ ] 编写登录测试"); const changes = await scanChanges(TMP_DIR, config); @@ -97,9 +104,7 @@ describe("完整 SDD 流程", () => { await writeFile( join(TMP_DIR, ".rune", "config.yaml"), - `metadata: - tracked: false -stages: + `stages: discuss: prompt: 自定义讨论 plan: @@ -121,6 +126,7 @@ stages: expect(planPrompt).toContain("规格文档"); expect(planPrompt).not.toContain("design"); + expect(config.stages.task).toBeDefined(); expect(config.stages.build).toBeDefined(); expect(config.stages.archive).toBeDefined(); }); @@ -139,11 +145,6 @@ stages: const designDoc = changes[0].documents.find((d) => d.name === "design"); expect(designDoc).toBeDefined(); expect(designDoc!.completed).toBe(true); - - const taskDoc = changes[0].documents.find((d) => d.name === "task"); - expect(taskDoc).toBeDefined(); - expect(taskDoc!.completed).toBe(false); - expect(taskDoc!.dependMet).toBe(true); }); });