Files
Rune-Spec/docs/superpowers/plans/2026-06-10-tracked-task.md

525 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 文档" 测试 FAILvalidateConfig 尚无此校验)
- [ ] **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<string> {
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<string> {
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: 提交最终状态(如有格式修复)**
如有修复则提交,否则跳过。