- proposal/design/specs/tasks:将固定 5 阶段重构为可配置 pipeline(discuss + pipeline + archive 三明治结构) - 引入 stage 完成判定(文档落地基线 + 可选 validate 实质门禁)与 finish 硬门禁 - 保留 explore 会话记录 session-ses_1357.md 作为思考溯源
8.6 KiB
Context
Rune 当前把 5 个固定阶段(discuss→plan→task→build→archive)硬编码在 6 处源码里:
types.ts:STAGES 字面量联合 + 5 个 Stage 接口cli.ts:5 个cli.command()硬编码assembler.ts:5 个assembleXxxPrompt专用函数scanner.ts:planCompleted/buildUnlocked 等固定语义defaults/config.ts:5 个固定 promptadapters/:for (stage of STAGES)注入
当前状态全从文件系统派生(plan done = 声明文档全存在;build done = task.md checkbox 全勾),无持久化状态文件。唯一不可逆操作 finish(移动目录到 archive/)不校验前置阶段——这是最大漏洞。
Goals / Non-Goals
Goals:
- 用通用 StageConfig 结构替换所有固定阶段渗透,实现"改配置即改流程"
- 建立"文档落地 + 可选 validate"两档完成判定,作为阶段推进的唯一依据
- 修复 finish 门禁:所有阶段实质完成才允许归档
- 保持 discuss 和 archive 作为固定端点,中间 pipeline 完全可配置
- 零特例:一个 stage = 一个文档,一条规则通吃所有阶段
Non-Goals:
- 多流水线 / 分支 / 条件跳过(0.2.0 不做,但数据结构不阻碍未来扩展)
- 回退命令(
rune back/rune reset)——用户手动删文档解决 - 向前兼容旧配置格式(BREAKING,init 重新生成)
- validate 沙箱——config 是可信内容,类比 package.json scripts
Decisions
D1: 三明治结构(discuss + pipeline + archive)
选择:discuss 固定在 pipeline 之前(无门禁、不产文档);archive 固定在 pipeline 之后(终端收尾、移动目录);中间 pipeline 完全可配置。
理由:discuss 本质是自由探索(无可强制产出),archive 本质是目录移动(语义通用)。把两端固定、中间放开,在"需要灵活性的地方给灵活性,需要锚点的地方给锚点"。
备选(否决):让 discuss/archive 也可配置。discuss 没有有意义的门禁(自由探索),archive 的移动语义是通用的,配置化无收益反而增复杂度。
D2: 每个 stage 产出且仅产出一个 <id>.md
选择:统一规则——每个 pipeline 阶段产出一个文档,文件名由 stage id 派生(plan → plan.md)。多文档需求拆成多个 stage。
理由:消除旧 plan 阶段的"一个 stage 多个文档 + 文档间 depend"特例。一条规则、一条代码路径、一个 done 检查。
备选(否决):outputs: [...] 数组允许一阶段多文档。重新引入部分完成、文档间依赖等复杂度,违背"统一"初衷。
D3: 两档完成判定(baseline done + substantive done)
选择:
- baseline done =
<id>.md存在(即使空文档)。CLI 永远检查,不可关闭。 - substantive done = validate 函数返回 truthy(若配置了 validate)。未配置 validate 则 baseline 即 substantive。
- stage fully done = baseline && substantive。
理由:文档存在是廉价、不可否认的"执行过"证据,挡"完全跳过"。validate 挡"做得很差"(例如 checkbox 瞎勾、测试没过)。分档让简单阶段零配置工作,复杂阶段获得实质验证(bun test && bun run lint)。
备选(否决):单档(只有 validate)。文档存在本身就有意义(执行证明),即使没配 validate 也该强制。
D4: validate 签名 (changeDir: string) => boolean | Promise<boolean>
选择:validate 是可选的 JS 函数字符串,签名 (changeDir: string) => boolean | Promise<boolean>。返回 truthy = 通过;falsy = 失败;throw = 失败并显示 message 给用户。
理由:changeDir 是读取阶段产出和项目文件的最小上下文。0.2.0 从最小签名开始,后续版本再扩展。Async 支持因为文件读取和命令执行都需要。
备选(否决):
- 对象参数
{ changeDir, projectRoot, stage, ... }:预留扩展,但 0.2.0 不需要。用 positional string 最简,后续要扩展时换对象参数(大版本变更可接受)。 - 独立 .js 文件引用(
validate: ./validate-build.js):config 不自包含,增加文件管理负担。后续可加此形式作为增强。
D5: validate 执行模型——AsyncFunction 动态构造
选择:CLI 读取 validate 字符串,用 new AsyncFunction('changeDir', body) 构造异步函数,传入 changeDir 执行。Bun 原生支持,Bun.file / Bun.write / fetch 等全局 API 在函数内可用。
async function runValidate(body: string, changeDir: string): Promise<ValidateResult> {
const fn = new AsyncFunction("changeDir", body);
try {
const result = await fn(changeDir);
return { passed: !!result, reason: null };
} catch (e) {
return { passed: false, reason: e instanceof Error ? e.message : String(e) };
}
}
理由:config 是项目可信内容(和 package.json scripts 同信任级),不需要 VM 沙箱。AsyncFunction 让 await 直接在函数体里用,validate 脚本可写 const c = await Bun.file(...).text(); return /pattern/.test(c);。
备选(否决):vm.runInNewContext 沙箱。增加复杂度无安全收益——开发工具的 config 本身就有完整执行权(package.json scripts 证明了这个模型可行)。
D6: 动态 CLI 命令注册
选择:CLI 启动时读取配置,为每个 pipeline stage 注册 rune <stage-id> <change> 命令。discuss、finish、status 为固定命令。
理由:直接替换硬编码命令。显式 stage 名比 rune next 更可审计,也允许用户直接获取特定阶段的提示词。
备选(否决):单一 rune next 命令自动推进。不可审计、不灵活(用户无法直接获取特定阶段提示词)。
D7: 门禁分级——软门禁 + 硬门禁
选择:
- 软门禁(stage 命令):
rune <stage> <change>检查前置阶段全 fully-done 才输出提示词。未通过则报错并指引。软——因为提示词只是文本,无法物理阻止 agent 工作。 - 硬门禁(finish):
rune finish检查所有阶段 fully-done 才执行目录移动。未通过则拒绝。
理由:rune 无运行时守护进程,agent 有文件写权限。唯一真正不可逆的杠杆是 finish(目录移动)。软门禁提供清晰反馈和引导,硬门禁是最终执行点。
D8: 默认 pipeline = [requirements, design, plan, task, build]
选择:5 个线性阶段,把旧 plan 阶段的 3 个文档拆成 3 个独立 stage。每个产出一个 .md。
理由:语义清晰,每个阶段一个审计点。和旧实现一一对应,老用户变更内容可直接迁移。遵循"一个 stage 一个文档"统一规则。
备选(否决):[plan, task, build](3 个,plan 单文档)。丢失 requirements/design 独立审计点。
D9: 数据模型
interface StageConfig {
id: string; // [a-z][a-z-]*,CLI 命令名 + 文件名 stem
prompt: string; // 阶段提示词
validate?: string; // 可选 JS 函数体
}
interface PipelineConfig {
stages: StageConfig[]; // 有序线性数组
}
interface StageStatus {
config: StageConfig;
docExists: boolean;
validated: boolean; // 无 validate 配置时恒为 true
done: boolean; // docExists && validated
}
interface ChangeStatus {
changeDir: string;
stages: StageStatus[];
currentIndex: number; // 第一个未 done 的阶段索引
allDone: boolean;
}
所有 Status 从文件系统 + 配置派生,不持久化。
D10: 配置 schema 校验
加载配置时校验:
- stage id 唯一(不可重复)
- stage id 字符集:
/^[a-z][a-z-]*$/ - stage id 不可为保留字(discuss、finish、status、archive)
- pipeline 至少 1 个 stage
- validate(若有)是合法可执行的 JS 字符串(
new AsyncFunction构造不抛异常)
Risks / Trade-offs
- [validate 脚本有完整执行权] → 文档明确警告 config 是可信内容(类比 package.json scripts),不盲目复制陌生项目 config。
- [空文档可骗过 baseline done] → 有意设计。"optional stage" = 手动建空
<id>.md,gate 自然通过。validate 挡实质内容。 - [配置 BREAKING,老用户 config 不兼容] →
rune init重新生成默认配置 + README 迁移指引。 - [validate 签名只有 changeDir,后续扩展是 breaking] → 0.2.0 大版本,可接受。或从 day-1 用对象包装(
{ changeDir })预留——倾向后者,零成本。 - [scanner 逻辑变复杂(从固定语义到配置驱动)] → 充分测试覆盖:空目录、部分完成、全完成、validate 失败等场景。