docs: 新增 configurable-pipeline 变更提案,探索可配置多阶段流水线模型

- proposal/design/specs/tasks:将固定 5 阶段重构为可配置 pipeline(discuss + pipeline + archive 三明治结构)
- 引入 stage 完成判定(文档落地基线 + 可选 validate 实质门禁)与 finish 硬门禁
- 保留 explore 会话记录 session-ses_1357.md 作为思考溯源
This commit is contained in:
2026-06-15 19:04:03 +08:00
parent 4e5c0805a2
commit a6b76b690a
8 changed files with 7844 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
## 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 失败等场景。