Files
Rune-Spec/openspec/changes/configurable-pipeline/design.md
lanyuanxiaoyao a6b76b690a docs: 新增 configurable-pipeline 变更提案,探索可配置多阶段流水线模型
- proposal/design/specs/tasks:将固定 5 阶段重构为可配置 pipeline(discuss + pipeline + archive 三明治结构)
- 引入 stage 完成判定(文档落地基线 + 可选 validate 实质门禁)与 finish 硬门禁
- 保留 explore 会话记录 session-ses_1357.md 作为思考溯源
2026-06-15 19:04:03 +08:00

164 lines
8.6 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.
## 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 个固定 prompt
- `adapters/``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`)——用户手动删文档解决
- 向前兼容旧配置格式BREAKINGinit 重新生成)
- 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 在函数内可用。
```typescript
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: 数据模型
```typescript
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 失败等场景。