diff --git a/docs/superpowers/plans/2026-06-10-tracked-task.md b/docs/superpowers/plans/2026-06-10-tracked-task.md new file mode 100644 index 0000000..031c6a5 --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-tracked-task.md @@ -0,0 +1,524 @@ +# tracked 任务跟踪模式实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 metadata 中新增 tracked 布尔开关,控制 plan/build/archive 三阶段是否启用 task.md 任务跟踪。 + +**Architecture:** 在 `RuneConfig.metadata` 新增 `tracked` 字段,默认 `false`,内置默认配置为 `true`。`task-parser.ts` 新增 `validateTaskFormat` 函数做格式校验。`config.ts` 的 `validateConfig` 在 tracked=true 时校验 plan 必须包含 task 文档。`assembler.ts` 的 build/archive 阶段根据 tracked 分支处理。 + +**Tech Stack:** TypeScript, Bun, bun:test + +--- + +### Task 1: 类型变更 — metadata.tracked + +**Files:** + +- Modify: `src/types.ts:38-42` +- Test: `tests/core/config.test.ts` + +- [ ] **Step 1: 写失败测试 — validateConfig 在 tracked=true 时要求 plan.documents 包含 task** + +在 `tests/core/config.test.ts` 的 `validateConfig` describe 块内追加: + +```typescript +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(); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test tests/core/config.test.ts` +Expected: 新增的 "tracked=true 时 plan.documents 必须包含 task 文档" 测试 FAIL(validateConfig 尚无此校验) + +- [ ] **Step 3: 修改 types.ts — metadata 新增 tracked 字段** + +在 `src/types.ts` 的 `RuneConfig` 接口中,修改 `metadata` 类型: + +```typescript +export interface RuneConfig { + stages: StagesConfig; + metadata?: { + command?: string; + tracked?: boolean; + }; +} +``` + +- [ ] **Step 4: 修改 config.ts — validateConfig 新增 tracked 校验** + +在 `src/core/config.ts` 的 `validateConfig` 函数中,在现有校验逻辑之前追加: + +```typescript +if (config.metadata?.tracked && plan) { + const hasTaskDoc = plan.documents.some((d) => d.name === "task"); + if (!hasTaskDoc) { + throw new ConfigError('tracked 开启时 plan.documents 必须包含 name 为 "task" 的文档'); + } +} +``` + +- [ ] **Step 5: 运行测试确认通过** + +Run: `bun test tests/core/config.test.ts` +Expected: ALL PASS + +- [ ] **Step 6: 提交** + +```bash +git add src/types.ts src/core/config.ts tests/core/config.test.ts +git commit -m "feat: metadata.tracked 类型与 validateConfig 校验" +``` + +--- + +### Task 2: validateTaskFormat 函数 + +**Files:** + +- Modify: `src/core/task-parser.ts` +- Test: `tests/core/task-parser.test.ts` + +- [ ] **Step 1: 写失败测试** + +在 `tests/core/task-parser.test.ts` 追加新的 describe 块: + +```typescript +import { validateTaskFormat } from "../../src/core/task-parser.ts"; + +describe("validateTaskFormat", () => { + it("合法 task 内容通过校验", () => { + expect(() => validateTaskFormat("- [x] 已完成\n- [ ] 未完成")).not.toThrow(); + }); + + it("无 checkbox 项时抛错", () => { + expect(() => validateTaskFormat("# 标题\n一些描述")).toThrow(); + }); + + it("空内容抛错", () => { + expect(() => validateTaskFormat("")).toThrow(); + }); + + it("checkbox 文本为空时抛错", () => { + expect(() => validateTaskFormat("- [ ] \n- [x] 有内容")).toThrow(); + }); + + it("checkbox 文本仅空格时抛错", () => { + expect(() => validateTaskFormat("- [ ] ")).toThrow(); + }); + + it("有 checkbox 且文本非空时通过", () => { + expect(() => validateTaskFormat("- [ ] 实现功能A")).not.toThrow(); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test tests/core/task-parser.test.ts` +Expected: 新增 validateTaskFormat 测试 FAIL(函数不存在) + +- [ ] **Step 3: 实现 validateTaskFormat** + +在 `src/core/task-parser.ts` 追加: + +```typescript +export class TaskFormatError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export function validateTaskFormat(content: string): void { + const tasks = parseTasks(content); + if (tasks.length === 0) { + throw new TaskFormatError("task.md 必须包含至少一个 checkbox 项"); + } + for (const task of tasks) { + if (task.text.trim() === "") { + throw new TaskFormatError("task.md 中每个 checkbox 项必须有非空描述"); + } + } +} +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `bun test tests/core/task-parser.test.ts` +Expected: ALL PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/core/task-parser.ts tests/core/task-parser.test.ts +git commit -m "feat: 新增 validateTaskFormat 校验函数" +``` + +--- + +### Task 3: 默认配置新增 metadata.tracked + +**Files:** + +- Modify: `src/defaults/config.ts:3` +- Test: `tests/defaults/config.test.ts` + +- [ ] **Step 1: 写失败测试** + +在 `tests/defaults/config.test.ts` 的 `defaultConfig` describe 块内追加: + +```typescript +it("默认 metadata.tracked 为 true", () => { + expect(defaultConfig.metadata).toBeDefined(); + expect(defaultConfig.metadata!.tracked).toBe(true); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test tests/defaults/config.test.ts` +Expected: "默认 metadata.tracked 为 true" FAIL + +- [ ] **Step 3: 修改默认配置** + +在 `src/defaults/config.ts` 中,将 `defaultConfig` 增加 `metadata` 字段: + +```typescript +export const defaultConfig: RuneConfig = { + metadata: { + tracked: true, + }, + stages: { +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `bun test tests/defaults/config.test.ts` +Expected: ALL PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/defaults/config.ts tests/defaults/config.test.ts +git commit -m "feat: 内置默认配置新增 metadata.tracked: true" +``` + +--- + +### Task 4: build 阶段根据 tracked 分支处理 + +**Files:** + +- Modify: `src/core/assembler.ts:76-120` +- Test: `tests/core/assembler.test.ts` + +- [ ] **Step 1: 写失败测试** + +在 `tests/core/assembler.test.ts` 的 `assembleBuildPrompt` describe 块内追加: + +```typescript +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 () => { + 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"); + expect.unreachable(); + } catch (e: any) { + expect(e.message).toContain("task.md"); + expect(e.message).toContain("格式"); + } +}); + +it("tracked=true 且 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"); + expect.unreachable(); + } catch (e: any) { + expect(e.message).toContain("checkbox"); + } +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test tests/core/assembler.test.ts` +Expected: 新增测试 FAIL(当前 assembleBuildPrompt 不检查 tracked,始终读 task.md) + +- [ ] **Step 3: 修改 assembleBuildPrompt** + +修改 `src/core/assembler.ts` 的 `assembleBuildPrompt` 函数: + +```typescript +export async function assembleBuildPrompt( + config: RuneConfig, + projectRoot: string, + changeName: string, +): Promise { + const build = config.stages.build; + if (!build) { + throw new CommandError("构建阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.build", + }); + } + + if (!config.metadata?.tracked) { + return applyCommandPrefix(build.prompt, config); + } + + const changeDir = getChangeDir(projectRoot, changeName); + const taskPath = join(changeDir, "task.md"); + + let taskContent: string; + try { + taskContent = await readFile(taskPath, "utf-8"); + } catch { + const prefix = getPmPrefix(config); + throw new CommandError(`变更 "${changeName}" 尚未完成规划,task.md 不存在`, { + hint: `请先完成规划阶段:${prefix} plan ${changeName} 生成所有规划文档`, + }); + } + + validateTaskFormat(taskContent); + + const tasks = parseTasks(taskContent); + const pendingTasks = tasks.filter((t) => !t.checked); + + if (pendingTasks.length === 0) { + return `所有任务已完成。变更 "${changeName}" 可以归档。`; + } + + const parts: string[] = []; + parts.push(`# 构建阶段:${changeName}\n`); + parts.push(build.prompt); + parts.push(`\n## 任务列表\n`); + parts.push(taskContent); + parts.push(`\n## 待执行任务(共 ${pendingTasks.length} 项)`); + for (const task of pendingTasks) { + parts.push(`- [ ] ${task.text}`); + } + parts.push(`\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`); + + return applyCommandPrefix(parts.join("\n"), config); +} +``` + +同时更新 import,在文件顶部的 import 中加入 `validateTaskFormat`: + +```typescript +import { parseTasks, validateTaskFormat } from "./task-parser.ts"; +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `bun test tests/core/assembler.test.ts` +Expected: ALL PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/core/assembler.ts tests/core/assembler.test.ts +git commit -m "feat: build 阶段根据 tracked 分支处理" +``` + +--- + +### Task 5: archive 阶段根据 tracked 分支处理 + +**Files:** + +- Modify: `src/core/assembler.ts:122-160` +- Test: `tests/core/assembler.test.ts` + +- [ ] **Step 1: 写失败测试** + +在 `tests/core/assembler.test.ts` 的 `assembleArchivePrompt` describe 块内追加: + +```typescript +it("tracked=false 时不读取 task.md,只输出通用提示词", async () => { + 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 时读取 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: true }, + }; + const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth"); + expect(prompt).toContain("未完成"); + expect(prompt).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"), "- [x] 已完成任务"); + const config: RuneConfig = { + stages: { archive: { prompt: "归档阶段" } }, + metadata: { tracked: true }, + }; + const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth"); + expect(prompt).not.toContain("未完成"); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test tests/core/assembler.test.ts` +Expected: 新增 "tracked=false 时不读取 task.md" 测试 FAIL(当前 archive 始终读 task.md) + +- [ ] **Step 3: 修改 assembleArchivePrompt** + +修改 `src/core/assembler.ts` 的 `assembleArchivePrompt` 函数: + +```typescript +export async function assembleArchivePrompt( + config: RuneConfig, + projectRoot: string, + changeName: string, +): Promise { + const archive = config.stages.archive; + if (!archive) + throw new CommandError("归档阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.archive", + }); + + const parts: string[] = []; + parts.push(`# 归档阶段:${changeName}\n`); + + if (config.metadata?.tracked) { + const changeDir = getChangeDir(projectRoot, changeName); + const taskPath = join(changeDir, "task.md"); + 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(`以下 ${incompleteTasks.length} 个任务尚未完成:`); + for (const t of incompleteTasks) { + parts.push(`- [ ] ${t.text}`); + } + parts.push(""); + parts.push("请询问用户是否确认在任务未全部完成的情况下归档。"); + parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。"); + parts.push(""); + } + } catch { + // task.md 不存在时不追加警告 + } + } + + parts.push(archive.prompt); + return applyCommandPrefix(parts.join("\n"), config); +} +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `bun test tests/core/assembler.test.ts` +Expected: ALL PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/core/assembler.ts tests/core/assembler.test.ts +git commit -m "feat: archive 阶段根据 tracked 分支处理" +``` + +--- + +### Task 6: 全量测试验证 + +**Files:** 无新文件 + +- [ ] **Step 1: 运行全量测试** + +Run: `bun test` +Expected: ALL PASS + +- [ ] **Step 2: 运行代码质量检查** + +Run: `bun run check` +Expected: 无 lint 或格式错误 + +- [ ] **Step 3: 提交最终状态(如有格式修复)** + +如有修复则提交,否则跳过。