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

8.6 KiB
Raw Blame History

Context

Rune 当前把 5 个固定阶段discuss→plan→task→build→archive硬编码在 6 处源码里:

  • types.tsSTAGES 字面量联合 + 5 个 Stage 接口
  • cli.ts5 个 cli.command() 硬编码
  • assembler.ts5 个 assembleXxxPrompt 专用函数
  • scanner.tsplanCompleted/buildUnlocked 等固定语义
  • defaults/config.ts5 个固定 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 派生(planplan.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.jsconfig 不自包含,增加文件管理负担。后续可加此形式作为增强。

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 工作。
  • 硬门禁finishrune 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>.mdgate 自然通过。validate 挡实质内容。
  • [配置 BREAKING老用户 config 不兼容]rune init 重新生成默认配置 + README 迁移指引。
  • [validate 签名只有 changeDir后续扩展是 breaking] → 0.2.0 大版本,可接受。或从 day-1 用对象包装({ changeDir })预留——倾向后者,零成本。
  • [scanner 逻辑变复杂(从固定语义到配置驱动)] → 充分测试覆盖空目录、部分完成、全完成、validate 失败等场景。