- proposal/design/specs/tasks:将固定 5 阶段重构为可配置 pipeline(discuss + pipeline + archive 三明治结构) - 引入 stage 完成判定(文档落地基线 + 可选 validate 实质门禁)与 finish 硬门禁 - 保留 explore 会话记录 session-ses_1357.md 作为思考溯源
164 lines
8.6 KiB
Markdown
164 lines
8.6 KiB
Markdown
## 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`)——用户手动删文档解决
|
||
- 向前兼容旧配置格式(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 在函数内可用。
|
||
|
||
```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 失败等场景。
|