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,2 @@
schema: spec-driven
created: 2026-06-15

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 失败等场景。

View File

@@ -0,0 +1,46 @@
## Why
当前 Rune 把 5 个固定阶段discuss→plan→task→build→archive硬编码在 6 处源码里types/cli/assembler/scanner/defaults/adapters用户无法自定义阶段流程。不同项目需要不同的工作流有的要 code-review 阶段,有的不需要 design但框架不给出任何灵活性。更严重的是唯一的不可逆操作 `finish`(归档移动目录)**不校验前置阶段是否完成**——agent 可以跳过任意阶段直接归档,流程约束形同虚设。
需要一套**可配置但可强制**的流水线模型用户自定义阶段CLI 用"文档落地 + 可选 validate"作为完成判据,`finish` 硬门禁拦住跳过。
## What Changes
- **BREAKING** 移除固定的 STAGES 常量和 5 个 Stage 接口,替换为通用 StageConfig 结构
- **BREAKING** 移除 cli.ts 里 5 个硬编码 command替换为从配置动态生成的 stage 命令
- **BREAKING** 移除 assembler.ts 里 5 个专用 assembleXxxPrompt 函数,替换为 1 个通用 assembleStagePrompt
- **BREAKING** 移除 scanner.ts 里 planCompleted/buildUnlocked 等固定语义函数,替换为配置驱动的完成判定
- 引入三明治结构:`discuss`固定pipeline 之前,无门禁,不产文档)+ `pipeline`(可配置的线性阶段序列)+ `archive`固定pipeline 之后,终端收尾)
- 每个 pipeline 阶段产出且仅产出一个 `<stage-id>.md` 文档;文档存在 = 基线完成
- 每个阶段可配置可选的 `validate` 函数JS接收 changeDir通过 = 实质完成
- `finish` 升级为硬门禁:所有阶段实质完成才允许归档
- 默认 pipeline 拆成 5 个阶段:`[requirements, design, plan, task, build]`(把旧 plan 的 3 文档拆成 3 个线性阶段,消除多文档特例)
- `discuss` 命令保留,行为不变(输出讨论提示词)
- 新增配置 schema`stages` 数组,每项含 `id`/`prompt`/可选 `validate`
## Capabilities
### New Capabilities
- `pipeline`: 可配置的流水线模型——配置格式、stage 结构id/prompt/validate、默认流水线、stage id 命名规则
- `stage-completion`: 阶段完成判定机制——文档落地(基线 done+ validate 函数(实质 done、两档门禁软门禁挡 stage 命令、硬门禁挡 finish
- `flow-commands`: 流程驱动 CLI 命令——discuss固定前置、动态 stage 命令软门禁、finish硬门禁+归档、status审计可见性
### Modified Capabilities
无——openspec/specs/ 为空,全部为新建。)
## Impact
- **源码重构**6 处固定阶段渗透点全部替换):
- `src/types.ts`:移除 STAGES + 5 个 Stage 接口,新增 StageConfig/PipelineConfig 类型
- `src/cli.ts`5 个硬编码 command → 从配置动态注册 stage 命令
- `src/core/assembler.ts`5 个专用函数 → 1 个通用 assembleStagePrompt
- `src/core/scanner.ts`:固定语义 → 配置驱动的通用扫描+完成判定
- `src/core/config.ts`:新增 pipeline schema 校验stage id 唯一性、字符集、validate 语法)
- `src/defaults/config.ts`5 个固定 prompt → 默认 5 阶段 pipeline 配置
- `src/adapters/opencode.ts``src/adapters/claude-code.ts`for(stage of STAGES) → 从配置注入
- **validate 执行引擎**(新增):解析 JS 函数字符串 + 注入 changeDir + 执行 + 捕获结果/异常
- **finish 门禁**(新增/修复):归档前校验所有阶段实质完成
- **配置 schema 变更**:用户现有 `.rune/config.yaml` 的 stage 配置不兼容BREAKING需提供迁移指引或 init 重新生成
- **测试**scanner 完成判定、validate 执行、gate 逻辑、动态命令注册均需新测试覆盖

View File

@@ -0,0 +1,129 @@
## ADDED Requirements
### Requirement: 三明治流程结构
系统 MUST 采用三明治结构:`discuss`(固定,流水线之前)→ `pipeline`(可配置的线性阶段序列)→ `finish`固定流水线之后终端收尾。discuss 和 finish MUST NOT 在 pipeline stages 中定义——它们是框架级固定命令。
#### Scenario: 完整流程顺序
- **WHEN** 用户执行一个变更的完整流程
- **THEN** 流程顺序为:先可选执行 discuss再按序执行 pipeline 中每个 stage最后执行 finish 归档
#### Scenario: discuss 不可作为 stage id
- **WHEN** 用户尝试在 stages 中定义 id 为 `discuss` 的阶段
- **THEN** 配置校验失败(被保留字规则拦截)
#### Scenario: finish 不可作为 stage id
- **WHEN** 用户尝试在 stages 中定义 id 为 `finish` 的阶段
- **THEN** 配置校验失败(被保留字规则拦截)
### Requirement: discuss 命令
系统 MUST 提供 `rune discuss <change>` 命令。该命令 MUST 输出讨论模式提示词文本。该命令 MUST NOT 检查任何前置条件(无门禁)。该命令 MUST NOT 要求产出任何文档。该命令的行为与当前 0.1.x 实现(输出探索模式 prompt保持一致。
#### Scenario: 输出讨论提示词
- **WHEN** 用户执行 `rune discuss my-feature`
- **THEN** 命令输出讨论模式提示词文本,退出码为 0
#### Scenario: discuss 无前置门禁
- **WHEN** 变更目录为空(无任何 stage 文档),用户执行 `rune discuss my-feature`
- **THEN** 命令正常输出提示词,不报任何前置条件错误
### Requirement: 动态 Stage 命令注册
系统 MUST 为配置中的每个 pipeline stage 注册对应的 CLI 命令 `rune <stage-id> <change>`。命令名由 stage id 决定运行时从配置动态生成MUST NOT 硬编码任何特定 stage。
#### Scenario: 从配置注册命令
- **WHEN** 配置定义 stages 为 `[requirements, design, plan, task, build]`
- **THEN** CLI 注册 5 个命令:`rune requirements``rune design``rune plan``rune task``rune build`
#### Scenario: 自定义 stage 注册命令
- **WHEN** 配置定义 stages 包含 `{ id: "code-review", prompt: "..." }`
- **THEN** CLI 注册 `rune code-review <change>` 命令
#### Scenario: 执行 stage 命令输出提示词
- **WHEN** 用户执行 `rune plan my-feature`,且前置阶段全完成
- **THEN** 命令输出 plan 阶段的 prompt 文本,退出码为 0
### Requirement: Stage 命令软门禁
`rune <stage-id> <change>` MUST 在输出提示词前检查该 stage 的所有前置 stage 是否完全完成fully done。若任一前置 stage 未完全完成,命令 MUST 拒绝输出提示词,报错指出第一个未完成的前置 stage并退出码非零。
#### Scenario: 前置全完成则放行
- **WHEN** 流水线为 [A, B, C]A 和 B 完全完成,用户执行 `rune C my-feature`
- **THEN** 命令输出 C 的提示词
#### Scenario: 前置有未完成则拒绝
- **WHEN** 流水线为 [A, B, C]A 完全完成但 B 未完成,用户执行 `rune C my-feature`
- **THEN** 命令拒绝输出提示词,报错指出 B 阶段未完成,退出码非零
#### Scenario: 第一个 stage 无前置门禁
- **WHEN** 流水线为 [A, B, C],变更目录为空,用户执行 `rune A my-feature`(第一个 stage
- **THEN** 命令输出 A 的提示词,不报前置条件错误
### Requirement: finish 命令硬门禁
`rune finish <change>` MUST 在执行归档操作前检查所有 pipeline stage 是否完全完成。若任一 stage 未完全完成,命令 MUST 拒绝归档,报错列出所有未完成的 stage 及其原因(文档缺失或 validate 失败),退出码非零。所有 stage 完全完成时,命令 MUST 执行归档操作(移动变更目录到 archive/)。
#### Scenario: 全完成则归档
- **WHEN** 流水线为 [A, B, C],所有 stage 完全完成,用户执行 `rune finish my-feature`
- **THEN** 命令将变更目录移动到 archive/,退出码为 0
#### Scenario: 有未完成则拒绝归档
- **WHEN** 流水线为 [A, B, C]A 和 C 完全完成但 B 文档缺失,用户执行 `rune finish my-feature`
- **THEN** 命令拒绝归档,报错指出 B 阶段文档缺失,退出码非零
#### Scenario: validate 失败则拒绝归档
- **WHEN** 所有 stage 文档存在,但 build 阶段的 validate 返回 falsy用户执行 `rune finish my-feature`
- **THEN** 命令拒绝归档,报错指出 build 阶段 validate 未通过(含失败原因),退出码非零
#### Scenario: finish 列出所有未完成项
- **WHEN** 流水线为 [A, B, C]A 缺文档且 B validate 失败,用户执行 `rune finish my-feature`
- **THEN** 报错同时列出 A文档缺失和 Bvalidate 失败),不只是第一个
### Requirement: status 命令
系统 MUST 提供 `rune status <change>` 命令,输出变更的流水线进度。输出 MUST 包含每个 stage 的状态文档是否存在、validate 是否通过、是否完全完成)和整体进度(当前阶段索引、是否全部完成)。该命令 MUST NOT 有任何门禁——纯只读审计。
#### Scenario: 显示全部阶段状态
- **WHEN** 流水线为 [A, B, C]A 完全完成B 文档存在但 validate 失败C 文档不存在
- **THEN** status 输出显示A done、B 文档存在但 validate 失败、C 文档缺失,当前进度为 B
#### Scenario: 全新变更状态
- **WHEN** 变更目录为空,用户执行 `rune status my-feature`
- **THEN** status 输出显示所有阶段未完成,当前进度为第一个阶段
#### Scenario: 全完成状态
- **WHEN** 所有阶段完全完成,用户执行 `rune status my-feature`
- **THEN** status 输出显示所有阶段 done可执行 finish 归档
### Requirement: 变更目录约定
所有 stage 文档 MUST 位于变更目录内(如 `changes/<change-name>/`。discuss 不产出文档。finish 执行后变更目录 MUST 被移动到 `archive/<change-name>/`
#### Scenario: stage 文档路径
- **WHEN** stage id 为 `design`,变更名为 `my-feature`
- **THEN** 文档路径为 `changes/my-feature/design.md`
#### Scenario: finish 后目录移动
- **WHEN** finish 成功执行
- **THEN** `changes/my-feature/` 目录移动到 `archive/my-feature/`

View File

@@ -0,0 +1,105 @@
## ADDED Requirements
### Requirement: 流水线配置格式
流水线 MUST 在配置文件中通过 `stages` 有序数组定义。每个 stage MUST 包含 `id`(字符串)和 `prompt`字符串MAY 包含 `validate`JS 函数体字符串。stages 数组的顺序即为阶段执行顺序。
#### Scenario: 最小合法配置
- **WHEN** 配置文件包含 `stages: [{ id: "plan", prompt: "..." }]`
- **THEN** 配置加载成功,流水线包含 1 个阶段
#### Scenario: 含 validate 的完整配置
- **WHEN** 配置文件包含 `stages: [{ id: "build", prompt: "...", validate: "return true;" }]`
- **THEN** 配置加载成功build 阶段关联了 validate 函数
#### Scenario: 缺少 id 的 stage
- **WHEN** 配置文件中某个 stage 缺少 `id` 字段
- **THEN** 配置加载失败,报错指出哪个 stage 缺少 id
#### Scenario: 缺少 prompt 的 stage
- **WHEN** 配置文件中某个 stage 缺少 `prompt` 字段
- **THEN** 配置加载失败,报错指出哪个 stage 缺少 prompt
### Requirement: Stage ID 命名规则
Stage id MUST 匹配 `^[a-z][a-z-]*$`小写字母开头后续只允许小写字母和短横线。Stage id MUST 在同一流水线内唯一。Stage id MUST NOT 使用保留字:`discuss``finish``status``archive``init`
#### Scenario: 合法 id
- **WHEN** stage id 为 `code-review`
- **THEN** 配置校验通过
#### Scenario: 包含大写字母的 id
- **WHEN** stage id 为 `CodeReview`
- **THEN** 配置校验失败,报错指出 id 必须为 `[a-z][a-z-]*` 格式
#### Scenario: 包含数字的 id
- **WHEN** stage id 为 `stage1`
- **THEN** 配置校验失败,报错指出 id 只允许小写字母和短横线
#### Scenario: 重复 id
- **WHEN** 流水线包含两个 id 为 `plan` 的 stage
- **THEN** 配置校验失败,报错指出 stage id 不可重复
#### Scenario: 保留字 id
- **WHEN** stage id 为 `finish`
- **THEN** 配置校验失败,报错指出 `finish` 是保留字不可用作 stage id
### Requirement: Stage 文档约定
每个 pipeline stage 产出且仅产出一个文档,文件名由 stage id 派生:`<stage-id>.md`。文档 MUST 位于变更目录内(如 `changes/my-feature/plan.md`。Stage id 到文件名的映射 MUST NOT 可配置——统一规则不可覆盖。
#### Scenario: 文档文件名派生
- **WHEN** stage id 为 `design`
- **THEN** 该阶段的产出文档路径为 `<changeDir>/design.md`
#### Scenario: 含短横线的 id 文件名
- **WHEN** stage id 为 `code-review`
- **THEN** 该阶段的产出文档路径为 `<changeDir>/code-review.md`
### Requirement: 默认流水线
当用户执行 `rune init` 或配置中未定义 `stages` 时,系统 MUST 提供默认流水线,包含 5 个阶段,按序为:`requirements``design``plan``task``build`。每个默认阶段 MUST 包含对应的默认 prompt。
#### Scenario: init 生成默认流水线
- **WHEN** 用户执行 `rune init`
- **THEN** 生成的配置文件包含 5 个阶段requirements、design、plan、task、build各含默认 prompt
#### Scenario: 配置中无 stages 时回退默认
- **WHEN** 配置文件加载后 stages 为空或未定义
- **THEN** 系统使用默认 5 阶段流水线
### Requirement: 流水线线性有序
流水线 MUST 是线性有序序列——不支持分支、条件跳过或并行阶段。阶段的执行顺序由配置数组顺序决定。Stage N 的前置条件是 Stage 0 至 Stage N-1 全部完成。
#### Scenario: 阶段顺序由数组决定
- **WHEN** 配置定义 stages 为 `[A, B, C]`
- **THEN** 执行顺序固定为 A→B→CB 的前置是 A 完成C 的前置是 A 和 B 完成
### Requirement: 配置 schema 校验
配置加载时 MUST 执行以下校验任一失败则拒绝加载并给出明确错误信息stage id 格式、唯一性、保留字检查pipeline 至少 1 个 stagevalidate若有是可构造为函数的合法 JS 字符串。
#### Scenario: validate 语法错误
- **WHEN** 某个 stage 的 validate 值为 `return {{{`
- **THEN** 配置加载失败,报错指出该 stage 的 validate 不是合法 JS
#### Scenario: 空流水线
- **WHEN** 配置文件中 stages 为空数组
- **THEN** 配置加载失败,报错指出流水线至少需要 1 个阶段

View File

@@ -0,0 +1,105 @@
## ADDED Requirements
### Requirement: 基线完成判定(文档落地)
Stage 的基线完成状态由文档是否存在决定:变更目录内 `<stage-id>.md` 存在(即使内容为空)即视为基线完成。此检查 MUST 始终执行,不可通过配置关闭。文档不存在则该 stage 基线未完成。
#### Scenario: 文档存在即基线完成
- **WHEN** 变更目录中存在 `plan.md` 文件(无论内容是否为空)
- **THEN** plan 阶段的基线完成状态为 true
#### Scenario: 文档不存在即基线未完成
- **WHEN** 变更目录中不存在 `plan.md` 文件
- **THEN** plan 阶段的基线完成状态为 false
### Requirement: 实质完成判定validate
Stage 的实质完成状态由 validate 函数决定。若 stage 未配置 validate则实质完成恒为 true基线完成即完全完成。若配置了 validate实质完成要求 validate 函数执行后返回 truthy 值。
#### Scenario: 无 validate 时基线即实质
- **WHEN** stage 未配置 validate且文档存在
- **THEN** 该 stage 实质完成状态为 true
#### Scenario: validate 返回 truthy
- **WHEN** stage 配置了 validate文档存在validate 执行返回 `true`
- **THEN** 该 stage 实质完成状态为 true
#### Scenario: validate 返回 falsy
- **WHEN** stage 配置了 validate文档存在validate 执行返回 `false`
- **THEN** 该 stage 实质完成状态为 false
#### Scenario: validate 抛出异常
- **WHEN** stage 配置了 validatevalidate 执行时抛出 `new Error("task.md 缺少 checkbox")`
- **THEN** 该 stage 实质完成状态为 false且错误信息 "task.md 缺少 checkbox" 被捕获并记录
### Requirement: 阶段完全完成定义
Stage 完全完成fully doneMUST 同时满足基线完成和实质完成:`done = docExists && validated`。所有门禁检查 MUST 使用 fully done 作为判定条件。
#### Scenario: 文档存在但 validate 失败
- **WHEN** stage 文档存在,但 validate 返回 falsy
- **THEN** 该 stage fully done 为 false
#### Scenario: 文档不存在
- **WHEN** stage 文档不存在(无论 validate 配置或结果如何)
- **THEN** 该 stage fully done 为 false
### Requirement: validate 函数执行模型
validate MUST 通过 `new AsyncFunction('ctx', body)` 构造执行,其中 `ctx` 为上下文对象。`ctx` MUST 包含 `changeDir` 字段变更目录绝对路径。validate 函数体 MAY 使用 `await` 语法。返回值 truthy 判定为通过;抛出异常判定为失败,异常 message MUST 作为失败原因传递给用户。
#### Scenario: 读取文档内容校验
- **WHEN** validate 函数体为 `const c = await Bun.file(\`${ctx.changeDir}/task.md\`).text(); return /- \\[x\\]/.test(c);`
- **THEN** 函数被正确构造和执行,能读取 task.md 内容并返回正则匹配结果
#### Scenario: 执行命令校验
- **WHEN** validate 函数体为 `const p = Bun.spawnSync(["bun", "test"], { cwd: ctx.changeDir }); return p.exitCode === 0;`
- **THEN** 函数被正确执行,`bun test` 的退出码决定验证结果
#### Scenario: ctx 包含 changeDir
- **WHEN** validate 函数被调用
- **THEN** 参数 `ctx` 是一个对象,且 `ctx.changeDir` 为变更目录的绝对路径字符串
### Requirement: 变更状态派生
系统 MUST 从文件系统文档存在性和配置validate 定义)实时派生变更状态,不持久化任何状态文件。变更状态 MUST 包含每个 stage 的 docExists、validated、done 状态,以及流水线的 currentIndex第一个未完成阶段的索引和 allDone所有阶段是否完全完成
#### Scenario: 全新变更目录
- **WHEN** 变更目录刚创建,无任何 stage 文档
- **THEN** 所有 stage 的 docExists 为 falsecurrentIndex 为 0allDone 为 false
#### Scenario: 部分完成的变更
- **WHEN** 流水线为 [A, B, C]A 文档存在且 validate 通过B 文档不存在
- **THEN** A 的 done 为 trueB 的 done 为 falsecurrentIndex 为 1allDone 为 false
#### Scenario: 全部完成的变更
- **WHEN** 流水线为 [A, B, C],所有阶段文档存在且 validate 通过
- **THEN** 所有 stage 的 done 为 truecurrentIndex 等于 stages 长度越界表示无待办allDone 为 true
### Requirement: validate 结果缓存限制
同一变更目录在同一 CLI 调用内validate 结果 MAY 被缓存以避免重复执行。但跨 CLI 调用 MUST NOT 缓存——每次 CLI 调用 MUST 重新执行 validate因为用户可能在两次调用间修改了文档。
#### Scenario: 同一次调用内不重复执行
- **WHEN** 同一次 CLI 调用中需要多次获取某 stage 的完成状态
- **THEN** validate 函数至多执行一次
#### Scenario: 不同调用重新执行
- **WHEN** 用户先执行 `rune status`,再执行 `rune finish`
- **THEN** finish 命令 MUST 重新执行所有 validate不使用 status 调用的缓存结果

View File

@@ -0,0 +1,56 @@
## 1. 类型与数据模型
- [ ] 1.1 在 `src/types.ts` 中定义 StageConfig、PipelineConfig、StageStatus、ChangeStatus 接口(按 design.md D9 数据模型)
- [ ] 1.2 从 `src/types.ts` 移除 STAGES 常量和 5 个旧 Stage 接口DiscussStage/PlanStage/TaskStage/BuildStage/ArchiveStage
- [ ] 1.3 清理文件路径常量:移除旧 stage 专用路径保留通用路径changes/、archive/
## 2. 配置系统
- [ ] 2.1 重写 `src/core/config.ts` 加载逻辑:解析新 `stages` 数组格式,返回 PipelineConfig
- [ ] 2.2 实现 config schema 校验stage id 格式(`^[a-z][a-z-]*$`、唯一性、保留字discuss/finish/status/archive/init、pipeline 至少 1 个 stage、validate 可构造为 AsyncFunction
- [ ] 2.3 重写 `src/defaults/config.ts`:默认 5 阶段 pipelinerequirements/design/plan/task/build每个 stage 含对应默认 prompt
- [ ] 2.4 实现配置合并:默认配置 + 用户 `.rune/config.yaml` 覆盖
## 3. 完成判定引擎
- [ ] 3.1 实现 validate 执行器:`new AsyncFunction('ctx', body)`,注入 `ctx.changeDir`,返回 `{ passed: boolean, reason: string | null }`
- [ ] 3.2 重写 `src/core/scanner.ts`:遍历 pipeline stages检查 `<id>.md` 存在性 + 执行 validate若有派生 StageStatus[]
- [ ] 3.3 派生 ChangeStatus计算 currentIndex第一个未 done 的索引)和 allDone
## 4. 提示词组装
- [ ] 4.1 用通用 `assembleStagePrompt(stage: StageConfig, changeDir: string)` 替换 `src/core/assembler.ts` 中 5 个专用 assembleXxxPrompt 函数
## 5. CLI 命令层
- [ ] 5.1 重写 `src/cli.ts`:启动时读取配置,为每个 pipeline stage 动态注册 `rune <stage-id> <change>` 命令
- [ ] 5.2 实现 stage 命令软门禁:检查该 stage 所有前置 fully-done 才输出提示词,否则报错指引
- [ ] 5.3 重写 finish 命令硬门禁:检查所有 stage fully-done列出全部未完成项及原因通过后移动变更目录到 archive/
- [ ] 5.4 保留 discuss 命令:行为与 0.1.x 一致(输出探索模式 prompt无门禁不产文档
- [ ] 5.5 实现 status 命令:输出每阶段状态(文档存在/validate 通过/done+ 整体进度currentIndex/allDone
## 6. 适配器层
- [ ] 6.1 更新 `src/adapters/opencode.ts`:从配置动态注入 stage 命令和 skill 文件(替换 `for (stage of STAGES)`
- [ ] 6.2 更新 `src/adapters/claude-code.ts`:同上,动态注入
## 7. init 命令
- [ ] 7.1 更新 `src/commands/init.ts`:生成新格式 `.rune/config.yaml`(默认 5 阶段 pipeline 配置)
## 8. 测试
- [ ] 8.1 配置校验测试:合法/非法 id、重复 id、保留字、空 pipeline、validate 语法错误
- [ ] 8.2 validate 执行器测试truthy 返回通过、falsy 返回失败、异常捕获含 message、异步函数、ctx.changeDir 正确注入
- [ ] 8.3 scanner 测试空目录全未完成、部分完成、全完成、validate 失败场景、currentIndex/allDone 计算
- [ ] 8.4 软门禁测试:前置全完成放行、前置有未完成拒绝并报错、首阶段无门禁
- [ ] 8.5 finish 硬门禁测试:全完成时归档移动目录、有未完成时拒绝并列出全部未完成项及原因
- [ ] 8.6 动态命令注册测试:从配置生成正确命令集、自定义 stage id 注册为命令
## 9. 质量保障与文档
- [ ] 9.1 运行 `bun test` 确保全量测试通过
- [ ] 9.2 运行 `oxlint` 确保无 lint 错误
- [ ] 9.3 运行 `oxfmt` 确保格式一致
- [ ] 9.4 更新 README.md新配置格式、三明治流程说明、命令列表
- [ ] 9.5 编写迁移指引:旧用户从 0.1.x 升级(重新 init + 手动迁移变更内容)

7238
session-ses_1357.md Normal file

File diff suppressed because one or more lines are too long