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

15 KiB
Raw Blame History

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,内置默认配置为 truetask-parser.ts 新增 validateTaskFormat 函数做格式校验。config.tsvalidateConfig 在 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.tsvalidateConfig describe 块内追加:

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.tsRuneConfig 接口中,修改 metadata 类型:

export interface RuneConfig {
  stages: StagesConfig;
  metadata?: {
    command?: string;
    tracked?: boolean;
  };
}
  • Step 4: 修改 config.ts — validateConfig 新增 tracked 校验

src/core/config.tsvalidateConfig 函数中,在现有校验逻辑之前追加:

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: 提交
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 块:

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 追加:

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: 提交
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.tsdefaultConfig describe 块内追加:

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 字段:

export const defaultConfig: RuneConfig = {
  metadata: {
    tracked: true,
  },
  stages: {
  • Step 4: 运行测试确认通过

Run: bun test tests/defaults/config.test.ts Expected: ALL PASS

  • Step 5: 提交
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.tsassembleBuildPrompt describe 块内追加:

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.tsassembleBuildPrompt 函数:

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

import { parseTasks, validateTaskFormat } from "./task-parser.ts";
  • Step 4: 运行测试确认通过

Run: bun test tests/core/assembler.test.ts Expected: ALL PASS

  • Step 5: 提交
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.tsassembleArchivePrompt describe 块内追加:

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.tsassembleArchivePrompt 函数:

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: 提交
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: 提交最终状态(如有格式修复)

如有修复则提交,否则跳过。