From 2248241634d12d76efc661467456d79b22b40c13 Mon Sep 17 00:00:00 2001 From: dafang Date: Tue, 14 Jul 2026 11:37:06 +0800 Subject: [PATCH 1/2] feat: add risk-tiered feature lanes --- .../risk-tiered-short-flow-analysis.md | 89 ++ .../risk-tiered-short-flow-fix-note.md | 80 + .../risk-tiered-short-flow-report.md | 45 + .../risk-tiered-short-flow-review.md | 235 +++ .codestable/reference/tools.md | 2 +- README.en.md | 6 +- README.md | 6 +- SKILL_CATALOG.en.md | 2 +- SKILL_CATALOG.md | 2 +- WORKFLOW.en.md | 2 + WORKFLOW.md | 6 + .../fixtures/routing/rt-f15.json | 27 + .../fixtures/routing/rt-f16.json | 20 + .../fixtures/routing/rt-f17.json | 21 + .../fixtures/routing/rt-f18.json | 26 + experiments/cs-feat-routing-001/results.md | 2 +- .../codestable/skills/cs-code-review/SKILL.md | 27 +- .../references/report-template.md | 12 +- plugins/codestable/skills/cs-feat/SKILL.md | 116 +- .../cs-feat/references/acceptance/protocol.md | 2 +- .../references/design-review/protocol.md | 15 +- .../cs-feat/references/design/protocol.md | 2 + .../cs-feat/references/design/reference.md | 7 +- .../references/fastforward/protocol.md | 12 +- .../references/implementation/protocol.md | 6 +- .../implementation/support/reference.md | 2 +- .../skills/cs-feat/references/qa/protocol.md | 2 + .../skills/cs-onboard/references/tools.md | 2 +- .../tools/codestable-workflow-next.py | 952 ++++++++++- plugins/codestable/skills/cs/SKILL.md | 4 +- tests/test_codestable_workflow_next.py | 1393 ++++++++++++++++- tests/test_skill_contracts.py | 5 + tests/test_skill_entry_simplification.py | 23 +- tests/test_skill_workflow_scenarios.py | 88 +- 34 files changed, 3095 insertions(+), 146 deletions(-) create mode 100644 .codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-analysis.md create mode 100644 .codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-fix-note.md create mode 100644 .codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-report.md create mode 100644 .codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-review.md create mode 100644 experiments/cs-feat-routing-001/fixtures/routing/rt-f15.json create mode 100644 experiments/cs-feat-routing-001/fixtures/routing/rt-f16.json create mode 100644 experiments/cs-feat-routing-001/fixtures/routing/rt-f17.json create mode 100644 experiments/cs-feat-routing-001/fixtures/routing/rt-f18.json diff --git a/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-analysis.md b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-analysis.md new file mode 100644 index 0000000..414a5a3 --- /dev/null +++ b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-analysis.md @@ -0,0 +1,89 @@ +--- +doc_type: issue-analysis +issue: 2026-07-13-risk-tiered-short-flow +status: confirmed +root_cause_type: logic +related: [risk-tiered-short-flow-report.md] +tags: [workflow-routing, feature-lanes, review-policy] +github_issue: 43 +--- + +# 风险分级与短流程根因分析 + +## 1. 问题定位 + +| 关键位置 | 说明 | +|---|---| +| `plugins/codestable/skills/cs/SKILL.md:76` | 根路由只区分 feature / issue / refactor 等工作类型,不判断 feature 的规模、边界清晰度和风险。 | +| `plugins/codestable/skills/cs-feat/SKILL.md:100` | 只有显式 `wantsFastForward(intent)` 才尝试短流程;未传 flag 时,缺 design 一律在第 102 行进入 Design。 | +| `plugins/codestable/skills/cs-feat/SKILL.md:109` | design approved 后只要没有 goal state 就无条件进入 GoalPackage。 | +| `plugins/codestable/skills/cs-onboard/tools/codestable-workflow-next.py:477` | 可执行恢复工具同样把“approved design + no goal state”固定解释为缺 goal package。 | +| `plugins/codestable/skills/cs-feat/references/fastforward/protocol.md:3` | 短流程协议明确说小功能应直接实现,和主入口只能显式 opt-in 的可达性矛盾。 | +| `plugins/codestable/skills/cs-feat/references/design-review/protocol.md:46` | 所有 design 修订都强制启动新一轮独立 reviewer,没有区分契约变化与文字/映射修正。 | +| `plugins/codestable/skills/cs-code-review/SKILL.md:128` | 任意 diff 变化都要求重新 review;第 220-225 行又要求 blocking 修复后完整重跑,没有 focused closure。 | +| `plugins/codestable/skills/cs-feat/references/acceptance/protocol.md:49` | 已有 accept-inline verification 能力,但 code-review 默认去向仍固定为独立 QA,普通单 feature 无法自然使用。 | +| `tests/test_skill_workflow_scenarios.py:546` | 场景测试把“单 feature 默认 goal package/driver”固化为正确行为,缺少小型明确任务的短流程 oracle。 | + +## 2. 失败路径还原 + +**期望路径**:用户提出明确局部功能 → `cs` 判定为 feature → `cs-feat` 根据风险和边界自动判为 Quick → 实现与目标验证 → 一次独立代码审查 → 简短 ff-note 闭环。 + +**实际路径**:用户提出明确局部功能 → `cs` 只判定为 feature → 没有显式 `--mode fastforward` → `cs-feat` 因 design 缺失进入 Design → 每次 design 修订都独立重审 → design approved 后强制 GoalPackage/driver → implementation → 任意 review-fix 都完整复审 → QA → 九节 acceptance 与 final audit。 + +**第一分叉点**:`plugins/codestable/skills/cs-feat/SKILL.md:100` — 短流程选择依赖用户事先知道 flag,而不是依赖任务事实。 + +**第二分叉点**:`plugins/codestable/skills/cs-feat/SKILL.md:109` 与 `plugins/codestable/skills/cs-onboard/tools/codestable-workflow-next.py:477` — 标准 feature 和长程 Goal 被绑定成同一条路径。 + +## 3. 根因 + +**根因类型**:逻辑错误。 + +**根因描述**:当前模型只有“工作类型”和“显式 fastforward”两个维度,没有独立的执行风险分级。所有未显式选择 fastforward 的 feature 都落入同一标准分支,而该分支又把 goal package、独立 QA 和完整 acceptance 作为默认后继。review 规则只看“文件是否变化”,不看变化是否改变行为或契约,因此轻微修正也会启动完整新轮次。 + +**是否有多个根因**:是。 + +1. 主因:Quick 是隐藏的 opt-in 例外,不是基于仓库事实的默认分类结果。 +2. 次因:Standard 与 Goal 没有分离,Goal 无条件接管 approved design。 +3. 放大器:design/code review 缺少 focused closure,任何变化都重启完整独立审查。 +4. 防回归缺口:测试只证明长程 goal 路径存在,没有证明小任务默认不会进入它,也没有覆盖用户反馈触发重新分级。 + +## 4. 影响面 + +- **影响范围**:所有未显式传 `--mode fastforward` 的单 feature;尤其影响需求清楚、复用既有接口、改动局部且已有目标测试的日常任务。 +- **潜在受害模块**:`cs` 路由说明、`cs-feat` 状态机、workflow-next 恢复工具、design/code review、README/skill catalog 和 workflow scenario tests。`cs-epic` 的长程 goal 路径应保持不变。 +- **数据完整性风险**:无业务数据风险;但 lane 迁移若处理不当会让已有 feature 恢复到错误阶段。兼容规则应为:已有 `goal-state.yaml` 始终按 Goal 恢复;epic child 始终交回 Epic;无 goal state 的 standalone approved design 才按 Standard 恢复。 +- **严重程度复核**:维持 P1。功能可完成且有显式 fastforward 绕过,但默认路径稳定制造高额时间和 agent 成本。 + +## 5. 修复方案 + +### 方案 A:只把 Quick 改为自动判定 + +- **做什么**:在 `cs-feat` 的 Design 分支前增加 `quickEligible`,满足“需求明确、局部、复用已有契约、有目标验证”时自动进入现有 FastForward;增加用户抱怨流程过重时重新判定。 +- **优点**:改动小,直接解决 #43 的主要案例,复用成熟 fastforward 协议。 +- **缺点 / 风险**:Standard 仍无条件生成 goal package;重复 review、QA/acceptance 膨胀仍存在,问题只关闭一部分。 +- **影响面**:`cs`、`cs-feat`、fastforward 文档和路由测试。 + +### 方案 B:建立 Quick / Standard / Goal 三条 lane + +- **做什么**: + - Quick 自动选择:需求明确、局部、复用既有契约、已有目标验证;产物仅 ff-note,保留首次独立代码审查。 + - Standard:存在新契约或跨模块决策,但适合当前 run 完成;走 design/review/implementation/code review/accept-inline,不默认生成 goal package 或独立 QA 报告。 + - Goal:仅在用户明确要求长程自主执行、显式 `--stage goal-package`、已有 goal state 或 Epic 批量上下文时进入;保留完整 QA/acceptance。 + - design/code review 首轮独立审查保留;只有行为、公开契约、安全、数据、并发或架构发生实质变化才完整复审。test/docs/type/metadata/nit-only 修正由主 agent 做 focused closure 并记录 diff 与目标验证;无法确定分类时 fail-closed 完整复审。 + - 用户说“这是小改动 / 流程太重 / 文档比代码多”时必须暂停并重新分类,若风险条件阻止降级则说明具体原因。 +- **优点**:同时修复入口失配、Goal 默认化和重复审查三个根因;lane 按风险而非模型名称判断;现有 accept-inline 能力可直接复用。 +- **缺点 / 风险**:要同步 skill 契约、workflow-next、设计元数据、README 和场景测试;需明确旧 artifact 的恢复兼容。 +- **影响面**:`cs`、`cs-feat`、`cs-code-review`、workflow-next、相关 reference、README/SKILL_CATALOG 和测试。 + +### 方案 C:引入可配置风险分数 + +- **做什么**:按文件数、模块数、契约、安全、迁移、验证等维度计分,再用阈值选择 lane;项目可配置阈值。 +- **优点**:判定可量化、可扩展,适合大型组织差异化治理。 +- **缺点 / 风险**:为当前问题引入新的配置和伪精度;模型容易围绕分数做形式化判断,维护成本高于收益。 +- **影响面**:除方案 B 的文件外,还需要 schema、配置文档和迁移逻辑。 + +### 推荐方案 + +**推荐方案 B**。它用少量明确的风险条件把三种执行语义分开,复用现有 fastforward、accept-inline 和 goal 协议,能关闭 #43 的全部主要失败点;同时保留 Epic/显式 Goal 的严格流程,不以降低质量门槛换速度。 + +用户于 2026-07-13 明确确认方案 B。 diff --git a/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-fix-note.md b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-fix-note.md new file mode 100644 index 0000000..4b25874 --- /dev/null +++ b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-fix-note.md @@ -0,0 +1,80 @@ +--- +doc_type: issue-fix +issue: 2026-07-13-risk-tiered-short-flow +path: standard +fix_date: 2026-07-13 +related: [risk-tiered-short-flow-analysis.md] +tags: [workflow-routing, feature-lanes, focused-review] +github_issue: 43 +review_status: passed +--- + +# 风险分级与短流程修复记录 + +## 1. 实际采用方案 + +采用已确认的方案 B:把单 feature 的执行语义拆成 Quick / Standard / Goal 三条 lane。 + +- Quick:需求明确、改动局部、复用既有契约、有目标验证且没有高风险边界时自动选择;只保留实现、验证、首次独立代码审查和简短 ff-note。 +- Standard:需要 design 或跨模块决策,但在当前 run 完成;approved design 不再默认创建 goal package,code review 后进入 accept-inline。 +- Goal:仅在用户显式要求长程执行、已有 goal state 或 Epic 上下文时启用;保留 goal driver、独立 QA 和完整 acceptance。 +- Review:首次独立审查不变;只有可精确归因的 test/docs/type/metadata/nit-only 修正可走 focused closure,实质变化或不确定情况完整独立复审。 +- 用户反馈:出现“这是小改动 / 流程太重 / 文档比代码多”等信号时必须暂停并重新分类。 + +兼容策略:旧 design 缺 `execution_lane` 且没有 goal state 时按 Standard;已有 `goal-state.yaml` 始终按 Goal;Epic child 仍由唯一 Epic goal owner 接管,forward/reverse claim 冲突 fail-closed。 + +## 2. 改动文件清单 + +- 路由与流程契约:`plugins/codestable/skills/cs/SKILL.md`、`cs-feat/SKILL.md`、`cs-code-review/SKILL.md`。 +- 阶段协议:fastforward、design、design-review、implementation、QA、acceptance 及 review report template。 +- 可执行恢复:`plugins/codestable/skills/cs-onboard/tools/codestable-workflow-next.py`,增加 Quick/Standard 状态恢复、Epic parent ownership、reviewer/QA/doc_type gate 与 lane 冲突 fail-closed。 +- Runtime reference:更新 package source 的 `tools.md`,并通过 runtime sync 同步 `.codestable/reference/tools.md`。 +- 用户文档:中英文 README、WORKFLOW、SKILL_CATALOG。 +- 回归覆盖:workflow-next、skill contract、workflow scenario 测试,以及 `rt-f15` 至 `rt-f18` routing fixtures。 +- 闭环记录:本 issue 的 report、analysis 和 fix-note。 + +没有修改 #45、#46、#47 对应的安装更新、standalone 版本和 current-session 实现。 + +## 3. 验证结果 + +### RED + +- 目标命令:6 个新增 lane/review 用例。 +- 结果:`6 failed`。 +- 失败证据:Standard 仍返回 `goal_package`;Goal evidence 没有 lane;skill 文本缺 Quick/Standard/Goal、用户反馈重分类和 focused closure 契约。 +- Round 1 review-fix:首批 16 个对抗用例全部失败,复现 Epic child 假完成、Quick 被恢复为 design、QA failed 越过、伪 reviewer 放行和重复 checklist I/O;追加的 lane 冲突/Goal 优先 2 个边界用例同样失败。 +- Round 2 review-fix:2 个新增用例均失败;batch=true 的 approved child 返回 `CS_FEATURE_STANDARD_COMPLETE`,roadmap-owned draft child 返回 `feature-design-confirmation`。 +- Round 3 review-fix:metadata-less legacy child 的未完成/已完成两态均走错 Standard,权威 SKILL Spec 同时缺 roadmap owner 契约。 +- Round 4 review-fix:可读但无关的 identity mismatch state 误阻塞 Standard/Quick,且 reverse ownership 的负向/fail-closed 分支缺测试与路径诊断。 +- Round 5 review-fix:15 个目标反例中 `14 failed, 1 passed`;复现 design metadata 指向缺失 state 时漏掉外部真 owner、forward owner 未发现第二 claim、Quick 吞掉 failed/blocked QA/acceptance,以及损坏 frontmatter/checklist/feature 或 Epic goal-state 输出 traceback。 +- Round 6 review-fix:pre-goal-package child 的 draft/approved 两态均失败,分别错误进入单 feature confirmation 与 `CS_FEATURE_STANDARD_COMPLETE`;`roadmap` / `roadmap_item` 仅写一个的两态也继续了 standalone 流程。 +- Round 7 review-fix:metadata-less pre-goal child 的显式 items pointer 两态均失败,draft 错误进入 `feature-design-confirmation`,approved 全产物错误输出 `CS_FEATURE_STANDARD_COMPLETE`;显式 pointer 目标缺失时还会错误回退到 slug glob。 +- Round 8 review-fix:Standard/Quick 的短 slug 后缀碰撞均被错误认领为 Epic;forward items/goal owner 均漏检同文件或跨 roadmap 第二 claim;合法 YAML 的错误 items 容器 fail-open,错误 feature 类型则输出 traceback 而非 JSON。 +- Round 9 review-fix:reverse/forward goal owner 遇到合法 YAML 转义 NUL 的 `feature_dir` 时均输出裸 traceback,stdout 不是 JSON。 +- Round 10 review-fix:design roadmap slug 含 NUL 时 traceback;reverse goal owner 对错误 `features` 容器/row fail-open;items 显式 pointer 含 NUL 时被当作目标缺失并产生假完成。 +- Round 11 review-fix:forward goal owner 排除整份 goal-state,静默漏掉同文件第二 row 对当前 feature 的 claim。 + +### GREEN + +- 初始目标用例:`6 passed`;Round 1 review-fix:`18 passed`;Round 2:`2 passed`;Round 3:`3 passed`;Round 4 reverse matrix:`8 passed`;Round 5:`15 passed`;Round 6 ownership:`5 passed`;Round 7 items ownership:`12 passed`;Round 8 owner hardening:`13 passed`;Round 9 goal-state path hardening:`2 passed`;Round 10 owner input hardening:`4 passed`;Round 11 row-level owner exclusion:`1 passed`;workflow-next 全组:`102 passed`。 +- Round 7 GREEN:metadata-less child 可由 parent items 的权威显式 pointer 或 `feature: null` + 命名回退反向唯一认领;同文件/跨 roadmap 多 claim、损坏 items fail-closed;无关 items 不误伤;forward item pointer 必须匹配当前 feature;Epic 与 feature 两个入口共享 pointer 解析语义。 +- Round 8 GREEN:目录回退按 `feature_slug_from_dir` 精确匹配且多精确目录 fail-closed;forward items/goal owner 排除本条目后反查剩余 items claim;错误 items 容器或 feature 类型在 reverse/forward/Epic 三入口均返回带路径的结构化 blocked JSON。 +- Round 9 GREEN:goal-state `feature_dir` 的类型、Path 构造与 resolve 在 reverse/forward 两入口统一 fail-closed;NUL 路径均 exit 1、JSON blocked、stderr 空且带 goal-state 路径。 +- Round 10 GREEN:共享 `checked_yaml_path` 覆盖 roadmap slug、items pointer、goal-state feature_dir;reverse goal owner 拒绝非 list-of-mappings 的 `features`;三类异常均结构化 blocked,不再 fail-open 或 traceback。 +- Round 11 GREEN:goal-state owner 排除键改为 `(goal_state_path, roadmap_item)`,同文件第二 claim 与跨文件 claim 对称 fail-closed。 +- 相关回归:`198 passed`,覆盖 workflow-next、entry simplification、workflow scenarios 和 skill contracts。 +- 正式测试目录全量:`pytest -q tests`,`434 passed in 6.24s`,600 秒进程组超时保护。 +- Plugin checker:`ok: true`,`findings: []`;ruff 使用 `--ignore E402`(工具 bootstrap 的既有 re-exec import 风格)通过。 +- Runtime sync:写入和 `--check --json` 均为 `status: ok`;package source 与 repo-local `tools.md` 内容一致。 +- JSON fixtures:`rt-f15` 至 `rt-f18` 均通过结构化解析;skill contract 测试通过。 +- 清洁度:`git diff --check` 通过;修改的 Markdown 均不超过 300 行;plugin 目录无 `__pycache__` / `*.pyc` 残留。 + +裸 `pytest` 会收集隔离实验种子并因当前环境没有 `taskhub` 出现 18 个 collection errors;仓库正式 `tests/` 全量已通过。本次未运行付费多模型 routing eval 或真实 agent E2E,fixtures 的 results 文档已明确标记尚未重新测量。 + +## 4. 遗留事项 + +- #45:skills update 删除已安装的 cs 系列技能。 +- #46:`cs-feedback --session current` 多候选定位。 +- #47:standalone 安装的 runtime 版本为 unknown。 +- 本次新增 routing fixtures 待后续统一付费多模型 campaign 测量;不阻塞规则、运行时状态机和机械回归落地。 +- Round 11 独立审查初始为 `changes-requested`(0 blocking,1 important);R11-001 完成 RED/GREEN、全量验证与 OCR 0 comments 后,原 reviewer focused closure `passed`(0 blocking,0 important),最终 review gate 已闭合。 diff --git a/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-report.md b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-report.md new file mode 100644 index 0000000..357b621 --- /dev/null +++ b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-report.md @@ -0,0 +1,45 @@ +--- +doc_type: issue-report +issue: 2026-07-13-risk-tiered-short-flow +status: confirmed +severity: P1 +summary: 小型明确功能被默认推进为完整 feature 和 goal 流程 +tags: [workflow, skill-routing, efficiency] +github_issue: 43 +--- + +# 风险分级与短流程 Issue Report + +## 1. 问题现象 + +一个需求明确、沿用既有桥接接口且代码改动局部的功能,执行 `cs-feat` 后耗时约两小时,经历五轮设计审查、goal driver、三轮代码审查、QA、浏览器烟测、验收及多份状态回写。实际业务代码约十分钟即可完成,流程成本与任务规模不成比例。 + +## 2. 复现步骤 + +1. 提出一个边界明确、沿用既有接口、有明确目标测试的局部功能改动。 +2. 使用 `cs-feat` 且不显式指定 fastforward 模式。 +3. 设计确认后继续默认工作流。 +4. 观察到:任务进入完整 design、goal package、implementation、重复 review、QA 和 acceptance 流程,并生成整套产物。 + +复现频率:按当前默认规则稳定复现。 + +## 3. 期望 vs 实际 + +**期望行为**:此类任务应自动选择短流程,完成实现、必要行为测试和构建、一次独立代码审查及简短交付记录;只有风险或范围升级时才进入标准或 goal 流程。 + +**实际行为**:除非用户事先知道并显式选择 fastforward,否则新功能默认进入完整 feature/goal 管线;用户中途指出流程过重时也不会重新分级。 + +## 4. 环境信息 + +- 涉及模块 / 功能:`cs` 路由、`cs-feat`、design review、code review、goal/QA/acceptance 编排 +- 相关文件 / 函数:`plugins/codestable/skills/cs/`、`plugins/codestable/skills/cs-feat/`、`plugins/codestable/skills/cs-code-review/` +- 运行环境:CodeStable 1.0.3 skills 工作流 +- 其他上下文:GitHub #43;安装更新、standalone 版本和 current-session 定位已分别拆到 #45、#47、#46,不在本次修复范围 + +## 5. 严重程度 + +**P1** — 核心功能仍可完成且可显式选择短流程绕过,但默认路径会显著放大日常开发成本,并降低用户继续使用工作流的意愿。 + +## 备注 + +本次按任务风险、边界清晰度和已有验证证据分级,不按模型名称或所谓“智商”分级。 diff --git a/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-review.md b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-review.md new file mode 100644 index 0000000..38b80ac --- /dev/null +++ b/.codestable/issues/2026-07-13-risk-tiered-short-flow/risk-tiered-short-flow-review.md @@ -0,0 +1,235 @@ +--- +doc_type: issue-review +issue: 2026-07-13-risk-tiered-short-flow +status: passed +reviewer: subagent+ocr +reviewed: 2026-07-14 +round: 11 +--- + +# 风险分级与短流程代码审查报告 + +## 1. Scope And Inputs + +- Issue: `.codestable/issues/2026-07-13-risk-tiered-short-flow/` +- Fix note: `risk-tiered-short-flow-fix-note.md` +- Implementation evidence: fix-note 的 RED/GREEN/全量验证记录 +- Diff basis: `git status --short`、完整工作区 `git diff` +- Baseline dirty files: 当前 diff 全部属于 GitHub #43;#45/#46/#47 仅建 issue,未混入实现 + +### Independent Review + +- Detection: Paseo Task agent 与 OCR CLI 均可用 +- 环节 A 独立隔离 Task agent: Paseo `claude-fable-5` / high / read-only,completed +- 环节 B OCR CLI: completed +- OCR severity mapping: High -> blocking/important,Medium -> nit/suggestion,Low -> discarded +- Merge policy: Paseo、OCR 与主 agent 本地发现已逐条按仓库事实核验 +- Gate effect: none;Round 11 + focused closure 合并结论为 0 blocking / 0 important + +## 2. Diff Summary + +- 新增:四个 routing fixtures、issue report/analysis/fix-note +- 修改:`cs` / `cs-feat` / `cs-code-review` 契约、阶段协议、workflow-next、测试与中英文文档 +- 删除:none +- 风险热点:lane 状态恢复、legacy artifact 兼容、review/QA gate 放行条件 + +## 3. Adversarial Pass + +- 假设的生产 bug:恢复工具把已有流程产物恢复到错误 lane,并发出假完成信号。 +- 主动攻击过的反例:legacy Epic child、forward/reverse owner 冲突、仅 ff-note 的 Quick、失败 QA/acceptance、伪造 passed review、损坏 YAML、已有 design 强制 quick。 +- 结果:Round 1 至 Round 5 的 blocking/important 均进入对应 review-fix;Round 6 独立复审前不定稿 passed。 + +## 4. Findings + +### blocking + +- [x] REV-001 `codestable-workflow-next.py:653` legacy Epic child 可被恢复为 Standard + - Evidence: child 的 goal-state 位于 roadmap;feature 恢复只检查 feature 级 `goal-state.yaml`,且 `--epic-child-batch` 只覆盖 design 批量阶段。 + - Impact: 可跳过 Epic 强制 QA,并输出与 roadmap 状态矛盾的 `CS_FEATURE_STANDARD_COMPLETE`。 + - Expected fix scope: 根据 design 的 roadmap 归属和 roadmap goal-state fail-closed 交回 `cs-epic`。 + +### important + +- [x] REV-002 `codestable-workflow-next.py:187` Quick 的 ff-note 不参与 artifact 恢复 + - Evidence: 仅有 `{slug}-ff-note.md` 时被判为缺 design。 + - Impact: 已闭环 Quick feature 会被重新复活为 design 待办。 +- [x] REV-003 `codestable-workflow-next.py:209` Standard 恢复忽略已有 QA 状态 + - Evidence: `{slug}-qa.md status: failed|blocked` 仍路由 accept-inline。 + - Impact: 与 acceptance 协议冲突,可能形成 acceptance/qa-fix 往返。 +- [x] REV-004 `codestable-workflow-next.py:240` Standard 的 passed review 不校验独立 reviewer 锚点 + - Evidence: `status: passed` 且 reviewer 缺失或为 self 时仍可完成。 + - Impact: Standard 没有 Goal audit 兜底,可绕过必需的独立 review gate。 +- [x] REV-005 `cs-feat/SKILL.md:53` 显式 Quick 可越过已有 design 的已记录 lane + - Evidence: Quick 判定在 `hasExistingDesign` 前,且未要求把降级结果持久化。 + - Impact: design 仍为 Standard、ff-note 却已生成,恢复状态互相矛盾。 + +### nit + +- [x] REV-006 `cs-feat/SKILL.md:136` `lane == Quick -> Design` 为不可达分支。 +- [x] REV-007 `codestable-workflow-next.py:689` `execution_lane` 未做 trim/case 归一。 +- [x] REV-008 `codestable-workflow-next.py:329` `acceptance-inline` 与公开 `--stage accept` 命名不一致,完成 marker 未登记。 +- [x] REV-009 `codestable-workflow-next.py:685` checklist 在 evidence 和 Standard resolver 重复解析。 +- [x] REV-010 `codestable-workflow-next.py:216` passed review/acceptance 未校验 `doc_type`。 + +### suggestion + +- 后续可为 Standard review 记录 reviewed commit,防止 passed 后 diff 漂移。 + +### learning + +- 状态机测试应优先锁定 artifact 组合和 next action,不能只断言 skill 文案存在。 + +### praise + +- Goal state 优先于 recorded lane、非法 lane fail-closed、runtime source/copy 同步均已有有效测试。 + +## 5. Test And QA Focus + +本地 closure:REV-001..REV-010、R2/R3/R4、R5-001..003、Round 6 至 Round 11 findings 均已有对应状态机/契约测试;workflow-next 102 passed、相关 198 passed、正式全量 434 passed;R11-001 focused closure 已通过。 + +- 必测:roadmap goal-state + approved legacy child 且不带 batch flag。 +- 必测:ff-note-only Quick 的 review pending/fixing/passed 三态。 +- 必测:Standard QA failed/blocked 与 reviewer 缺失/self。 +- 必测:已有 design 从 Standard 降级 Quick 后可跨进程恢复。 +- 不运行付费多模型 routing eval;fixtures 的真实模型测量继续明确标为待执行。 + +## 6. Residual Risk + +- Quick 自动分类仍是模型判断;本轮只验证契约与机械状态恢复,真实模型准确率留给后续付费 campaign。 + +## 7. Verdict + +- Status: passed +- Next: issue review gate 已闭合,可进入 `cs-issue` 收尾提交阶段。 + +## 8. Round 2 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `6dac3fd9-806b-484b-bc1f-1f8769475047` +- OCR: 9 files reviewed,0 comments +- Verdict: `changes-requested`(0 blocking,2 important) +- [x] R2-001 `codestable-workflow-next.py:653` `epic_child_batch` 未参与 lane 分类,可产生 Standard 假完成。 + - RED: approved child + passed review/acceptance + 无 roadmap goal-state + batch=true,实际返回 `CS_FEATURE_STANDARD_COMPLETE`。 + - GREEN: batch context 记录 `execution_lane=goal` / source=`epic-child-batch`,reviewed child 交回 `return-to-cs-epic-batch-loop`。 +- [x] R2-002 `codestable-workflow-next.py:849` roadmap goal-state 拥有的 draft child 先停单 feature ConfirmDesign。 + - RED: draft + passed design-review + 唯一 roadmap owner,实际返回 `feature-design-confirmation`。 + - GREEN: 显式 batch 分支优先,其次 roadmap owner;两者均在单 feature ConfirmDesign 前交回 Epic。 +- Verify: 目标 `2 passed`,workflow-next `40 passed`,相关 `135 passed`,正式全量 `371 passed in 4.96s`;plugin checker/runtime sync/diff check 均通过。 + +## 9. Round 3 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `92a5678c-5f1d-4623-abf5-e18fe5870216` +- OCR: 9 files reviewed,0 comments;报告部分文件有内部 warning,无 finding +- Verdict: `changes-requested`(0 blocking,2 important);R2-001/R2-002 与 REV-001..REV-010 均确认未回归 +- [x] IMP-1 metadata-less legacy child 无法被 parent `features[].feature_dir` 反向认领。 + - RED: 产物未齐时错误进入 Standard implementation;产物齐全时错误输出 `CS_FEATURE_STANDARD_COMPLETE`。 + - GREEN: metadata 缺失时结构化扫描 roadmap goal-state,按 resolved `feature_dir` 反向唯一认领;损坏或多 owner fail-closed。 +- [x] IMP-2 `cs-feat/SKILL.md` 权威 Spec 未建模 roadmap owner,模型路径与工具矛盾。 + - RED: Spec 缺 `roadmapOwner`、`hasRoadmapOwner(s)` 与 `features[].feature_dir` 反向认领契约。 + - GREEN: FeatureState、lane 分类、restore guard 和 Epic 恢复散文同步,roadmap owner 在单 feature ConfirmDesign 前交回 Epic。 +- Verify: 目标 `3 passed`,workflow-next `42 passed`,相关 `138 passed`,正式全量 `374 passed in 5.04s`;plugin checker/runtime sync/source-copy/diff check 均通过。 + +## 10. Round 4 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `91f47cd0-1a09-4271-a214-30a307deaf4f` +- OCR: 9 files reviewed,0 comments +- Verdict: `changes-requested`(0 blocking,2 important);IMP-1/IMP-2 及更早 findings 均确认未回归 +- [x] R4-001 可读但 identity mismatch 的无关 roadmap state 全局误阻塞 metadata-less Standard/Quick,且诊断缺具体路径。 + - RED: 无关 Standard/Quick 两态均 blocked;identity mismatch 真指向当前 feature、损坏 YAML、多 owner、缺 item 的 blocked/evidence 均无路径。 + - GREEN: 可读 mismatch 仅在 `feature_dir` 指向当前 feature 时阻塞;不可解析/非 mapping 继续全局 fail-closed;blocking 与 evidence 写具体 state 路径。 +- [x] R4-002 reverse ownership 负向/fail-closed 分支无固定测试。 + - GREEN: 固化合法 0 owner、无关 mismatch Standard/Quick、mismatch 真 claim、损坏 YAML、跨 roadmap/同 state 多 claim、匹配行缺 item 共 8 个状态断言。 +- Verify: reverse matrix `8 passed`,workflow-next `50 passed`,相关 `146 passed`,正式全量 `382 passed in 5.02s`;plugin checker/runtime sync/source-copy/diff check 均通过。 + +## 11. Round 5 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `133b4cc8-7420-4df7-93ac-8e410c5cab57` +- OCR: 9 files reviewed,0 comments +- Verdict: `changes-requested`(0 blocking,3 important);R4-001/R4-002 及更早 findings 均确认未回归 +- [x] R5-001 design metadata 指向的 roadmap 无 goal-state 时,外部 roadmap 的真实 claim 被忽略;forward owner 也未检测第二 claim。 + - RED: 两个 owner 冲突场景分别错误输出 Standard complete 或继续交回单一 Epic owner。 + - GREEN: forward state 缺失或已证明 owner 后都执行排除自身的反向扫描;实际 claim 与预期 owner 不一致时按具体 state 路径 fail-closed。 +- [x] R5-002 Quick 忽略同目录内 failed/blocked QA 或 acceptance,可带失败证据假完成。 + - RED: 四组 failed/blocked 质量证据均错误输出 `CS_FEATURE_QUICK_COMPLETE`。 + - GREEN: Quick 对已存在的 QA/acceptance 只兼容 `doc_type` 正确且 `status: passed` 的产物,其他状态返回 `resolve-quick-quality-conflict` 并给出路径。 +- [x] R5-003 损坏 frontmatter、checklist 或普通 goal-state 抛裸 traceback,破坏 `--json` 机器契约。 + - RED: feature 七类 artifact 与 Epic goal-state 共八类路径均抛解析异常,CLI 无合法 JSON。 + - GREEN: artifact parse error 统一转为含路径和异常类型的结构化 `blocked`;CLI exit 1 且 stderr 无 traceback。 +- Verify: 目标 `15 passed`,workflow-next `65 passed`,相关 `161 passed`,正式全量 `397 passed in 5.43s`;plugin checker/runtime sync/source-copy/JSON/Markdown/diff check 均通过。 + +## 12. Round 6 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `8f1c74b6-c764-48fc-b1ca-ac6e51bc0d64` +- OCR: 9 files reviewed,0 comments +- Verdict: `changes-requested`(0 blocking,1 important);R5-001/R5-002/R5-003 与更早 findings 均确认未回归 +- [x] IMP-R6-1 pre-goal-package 的 Epic child 可被当作 standalone Standard 推进,与 SKILL 的 design roadmap ownership 契约矛盾。 + - RED: parent roadmap/items 存在且 goal-state 尚未生成时,draft child 返回 `feature-design-confirmation`,approved 全产物 child 返回 `CS_FEATURE_STANDARD_COMPLETE`。 + - GREEN: 完整 design metadata 经 parent `items.yaml` 唯一条目证明后记录 `roadmap_owner_source: roadmap-items` 并交回 `cs-epic`;items 缺失、损坏、identity 不符或条目不唯一 fail-closed。 +- [x] LOCAL-R6-1 design 只写 `roadmap` 或只写 `roadmap_item` 时被当作 metadata-less feature,可误走 standalone 或错误反向 owner。 + - RED: 两种单字段形态均继续 standalone 流程。 + - GREEN: metadata 必须同时为空或成对存在;不完整时返回 `blocked / fix-feature-roadmap-metadata`。 +- Non-blocking nits accepted for this issue: forward 同 state 第二行 claim、合法非 mapping frontmatter/goal-state 诊断、lane conflict 提示方向、错名 review artifact fallback;不影响本轮 blocking/important gate。 +- Verify: Round 6 ownership `5 passed`,workflow-next `70 passed`,相关 `166 passed`,正式全量 `402 passed in 5.40s`;plugin checker/runtime sync/source-copy/JSON/ruff/Markdown/diff check 均通过。 + +## 13. Round 7 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `68744f37-f398-48a9-9da8-322105b0bfd2` +- Verdict: `changes-requested`(0 blocking,1 important);Round 1 至 Round 6 已关闭 findings 均确认未回归 +- [x] R7-001 metadata-less pre-goal-package Epic child 只查 roadmap goal-state,不查 parent items,可进入单 feature confirmation 或输出 Standard 假完成。 + - RED: items 以显式 `feature:` 指向当前目录时,draft 返回 `user_gate / feature-design-confirmation`,approved 全产物返回 `complete / CS_FEATURE_STANDARD_COMPLETE`;显式缺失 pointer 还会错误回退 slug glob。 + - GREEN: metadata-less 路径在无 goal-state owner/error 后反向扫描 parent items;显式 pointer 具有权威性,`feature: null` 才使用 metadata/glob 回退;唯一 owner 交回 Epic,多 claim/损坏 items 带路径 fail-closed,无关 items 不误伤 standalone。 + - Consistency: forward items owner 校验 pointer 必须匹配当前 feature;Epic `find_feature_dir` 与 feature 反向 owner 共用 pointer 路径解析,跨入口测试锁定相同结论。 +- Non-blocking nits retained as residual risk: NUL 路径值的结构化诊断、items artifact action 命名、items identity 缺失、其他诊断细节;不影响 R7-001 的 blocking/important gate。 +- Verify: Round 7 items ownership 新增 `12 passed`,workflow-next `82 passed`,相关 `178 passed`,正式全量 `414 passed in 5.56s`;plugin checker/runtime sync/source-copy/JSON/ruff/Markdown/diff check 均通过。 + +## 14. Round 8 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `6d3edc23-322b-4d31-b389-aca7e5d477b7` +- OCR: 9 files reviewed,0 comments +- Verdict: `changes-requested`(1 blocking,1 important);Round 1 至 Round 7 已关闭 findings 均确认未回归 +- [x] R8-001 未锚定 `*-{slug}` 回退把 `auth/user-auth`、`export/small-export` 后缀碰撞误认作 Epic owner。 + - RED: standalone Standard 错误 `return-to-cs-epic`;已完成 Quick 被复活为 `cs-feat design`。 + - GREEN: 用 `feature_slug_from_dir` 精确匹配目录 slug;多精确匹配经 Epic/feature 两入口结构化 fail-closed。 +- [x] R8-002 forward items/goal owner 证明后未按条目排除自身并反查第二 items claim。 + - RED: 同文件第二行与跨 roadmap 第二 claim 均静默放行。 + - GREEN: `(items_path, roadmap_item)` 作为排除键,其他行/roadmap 继续参与冲突扫描并保留路径。 +- [x] LOCAL-R8-003 合法 YAML 的错误 items/feature 类型会 fail-open 或破坏 `--json` 机器契约。 + - RED: items mapping exit 0 进入 Standard;`feature: []` 输出 traceback 且 stdout 非 JSON。 + - GREEN: 共享 list-of-mappings/string-or-null 校验,reverse/forward/Epic 三入口均 exit 1、JSON blocked、stderr 空且含具体路径。 +- Non-blocking: sibling 损坏 design 的 owner 诊断波及、review commit 锚点等保留为 residual risk;ruff 验证已明确实际 `--ignore E402` 调用。 +- Verify: Round 8 新增 `13 passed`,workflow-next `95 passed`,相关 `191 passed`,正式全量 `427 passed in 5.78s`;等待 Round 9 复审前不提前标 passed。 + +## 15. Round 9 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `dceb0881-02e7-4981-b36a-1f0dd612c0e2` +- OCR: 9 files reviewed,0 comments +- Verdict: `changes-requested`(0 blocking,1 important);R8-001/R8-002/LOCAL-R8-003 与历史 findings 均确认未回归 +- [x] LOCAL-R9-001 reverse/forward goal owner 对合法 YAML 转义 NUL 的 `features[].feature_dir` 调用 `Path.resolve()` 时裸 traceback。 + - RED: 两入口 CLI stdout 为空、stderr traceback,无法解析 JSON。 + - GREEN: 两处同时校验 string 类型并捕获 Path/resolve 的 `ValueError`/`OSError`;reverse 写入 invalid state,forward 返回 owner error,均含 goal-state 路径。 +- Non-blocking: items 重复同 slug、同文件错误文案、第二 items 文件、reviewer/commit 锚点保留为 nit/suggestion,不扩大本 issue。 +- Verify: Round 9 新增 `2 passed`,workflow-next `97 passed`,相关 `193 passed`,正式全量 `429 passed in 6.47s`;等待 Round 10 复审前不提前标 passed。 + +## 16. Round 10 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `1bb3eb52-777c-4688-be4c-9e986242496b` +- OCR: 9 files reviewed,0 comments +- Verdict: `changes-requested`(0 blocking,3 important);LOCAL-R9-001 与历史 findings 均确认未回归 +- [x] R10-001 design `roadmap` slug 含合法 YAML 转义 NUL 时进入 `glob()` 裸 traceback。 +- [x] R10-002 reverse goal owner 对 mapping `features` 或非-mapping row 静默跳过,可继续错误 owner/Standard 路径。 +- [x] R10-003 items 显式 pointer 含 NUL 时 `Path.exists()` 吞异常,reverse 当作无 claim 并可假完成。 + - RED: roadmap slug stdout 非 JSON;两种 goal-state features 形状 exit 0;NUL pointer exit 0 输出 Standard complete。 + - GREEN: `checked_yaml_path` 统一 string/NUL/Path 校验,显式 pointer 在 exists 前 resolve;reverse goal rows 统一 list-of-mappings 校验,三类均带 artifact 路径 blocked。 +- Non-blocking: items 重复同 slug、同路径重复文案、第二 items 文件与 reviewer/commit 锚点继续保留为 nit/suggestion。 +- Verify: Round 10 新增 `4 passed`,workflow-next `101 passed`,相关 `197 passed`,正式全量 `433 passed in 6.23s`;等待 Round 11 复审前不提前标 passed。 + +## 17. Round 11 Review + +- Reviewer: Paseo `claude-fable-5` / high / plan-read-only,agent `9375e60c-706d-4442-a72b-900997b43ace`;OCR 两轮均为 9 files / 0 comments。 +- Initial verdict: `changes-requested`(0 blocking,1 important,4 nit);Round 10 三项与全部历史 findings 确认未回归。 +- [x] R11-001 forward goal owner 以整文件排除自身,漏检同一 goal-state 第二 row 对同一 feature 的 claim。 + - RED: 完整 metadata + 同 state 两个 item 指向同 feature 时返回 `continue / return-to-cs-epic`。 + - GREEN: 排除键改为 `(goal_state_path, roadmap_item)`;只跳过已证明行,其余 rows 继续唯一性检查。 +- Non-blocking: NUL item slug 输入加固、诊断文案与 epic goal-state 最小 shape 校验保留为 nit/suggestion;plugin bytecode 残留已清理。 +- Verify: ownership focused `7 passed`,workflow-next `102 passed`,相关 `198 passed`,正式全量 `434 passed in 6.24s`;ruff/plugin checker/runtime sync/source-copy/fixtures/Markdown/diff check 均通过。 +- Focused closure: `passed`(0 blocking,0 important);六组对抗覆盖同文件不同/相同 item、跨文件 claim、无关 row 与 metadata-less reverse,R11-001 已关闭。 +- Combined verdict: `passed`;保留 4 个 nit / residual risk,不阻塞 #43。 diff --git a/.codestable/reference/tools.md b/.codestable/reference/tools.md index 3250f75..0277f88 100644 --- a/.codestable/reference/tools.md +++ b/.codestable/reference/tools.md @@ -101,7 +101,7 @@ python3 /tools/codestable-goal-consistency-gate.py --ro `roadmap-goal-gates.yaml` 是阶段配置入口;`codestable-scope-gate.py`、`codestable-dod-runner.py` 和 `codestable-evidence-pack.py` 是 implementation.before_review 的最小 runtime。`status: protocol-only` 的 gate 只表示协议占位,由 review / QA / acceptance / audit 技能读取证据后执行,不代表已有独立脚本。 `codestable-goal-consistency-gate.py` 是 roadmap_audit.before_complete 的 runtime,检查 goal-state、items、每个 feature 的 review/QA/acceptance/evidence/gate/DoD 产物和 checklist 状态,防止 goal-state 早于证据推进。 -`codestable-workflow-next.py` 是只读下一步解析器,输出 `next_action`、`must_continue` 和 `final_answer_allowed`;`cs-epic` / `cs-feat` 在 child design batch 边界必须按它的 JSON 继续或停 gate。 +`codestable-workflow-next.py` 是只读下一步解析器,输出 `next_action`、`must_continue` 和 `final_answer_allowed`;`cs-epic` / `cs-feat` 在 child design batch 边界必须按它的 JSON 继续或停 gate。单 feature 按仓库事实恢复:feature goal-state 优先为 Goal;design 的完整 roadmap metadata 经 parent items 唯一证明,或被 parent items / roadmap goal-state 反向唯一认领的 child 交回 Epic;显式 feature 指针具有权威性,目录回退按精确 feature slug,多 claim 与错误 owner 结构/路径 fail-closed;ff-note 或 design 的 `execution_lane: quick` 恢复 Quick;旧 design 缺 lane 时恢复 Standard。Quick/Standard 的 passed review 必须有独立 reviewer 锚点,Quick 不得吞掉既有非 passed QA/acceptance;损坏的 YAML/frontmatter 或合法 YAML 中错误的路径/容器在 `--json` 下返回含具体路径的结构化 `blocked`,不得输出 traceback。 如果 skill 包缺少这些 runtime 脚本,说明本机 CodeStable 安装不完整;先更新 / 重装 CodeStable。项目缺少 `gates/` 或 `reference/` 时运行 runtime sync。 --- diff --git a/README.en.md b/README.en.md index 71bd02c..f8d927e 100644 --- a/README.en.md +++ b/README.en.md @@ -161,7 +161,7 @@ CodeStable models real coding work as a set of **entities** and **flows**. | Flow | Recommended main entry | Notes | |------|------------|------| -| **Feature delivery** | `cs-feat` | End-to-end design → design review → user gate → long-range goal run of implementation → `cs-code-review` → QA → acceptance | +| **Feature delivery** | `cs-feat` | Risk-based lanes: Quick implements/tests/reviews directly; Standard stays in the current run with design/implementation/review/inline acceptance; Goal alone creates a long-range goal package with QA/acceptance | | **Epic delivery** | `cs-epic` | Plan a large demand, review it, design child features, prepare a goal package, then dispatch a visible goal driver (print `/goal` as fallback) | | **Goal achievement** | `cs-goal` | Bounded start/end → interview/grill + start report → autonomous implement/validate/iterate → subagent functional acceptance | | **Issue fixing** | `cs-issue` | End-to-end report → analyze → fix → `cs-code-review` | @@ -184,7 +184,7 @@ CodeStable models real coding work as a set of **entities** and **flows**. | Epic | `cs-epic` | Large demand planning, review, child feature design, and goal package | | Brainstorm | `cs-brainstorm` | Triage fuzzy ideas into feature, epic, or brainstorm notes | | Goal | `cs-goal` | Autonomous iteration from a bounded start state to acceptance | -| Feature | `cs-feat` | End-to-end feature workflow | +| Feature | `cs-feat` | Quick / Standard / Goal feature workflow; Goal is opt-in for long-range execution | | Issue | `cs-issue` | End-to-end issue workflow | | Refactor | `cs-refactor` | Behavior-preserving refactor workflow | | Review | `cs-code-review` | Cross-cutting read-only implementation review gate | @@ -230,7 +230,7 @@ cs How to read it: - `cs` classifies the intake mode before the target. Action requests dispatch to the target skill in the current run; advice requests only recommend. It never routes users to deprecated stage skills. -- `cs-feat`, `cs-issue`, and `cs-refactor` resume from repository facts. `cs-issue` and `cs-refactor` stop at review, blocking, or user-confirmation checkpoints; `cs-feat` stops only at the design gate, then runs impl, review, QA, and accept long-range via a visible goal driver. +- `cs-feat`, `cs-issue`, and `cs-refactor` resume from repository facts. `cs-feat` first selects Quick, Standard, or Goal from task risk: Quick stays minimal, Standard completes in the current run with inline acceptance, and only Goal uses a long-range driver plus standalone QA. `cs-issue` and `cs-refactor` stop at their review, blocking, or user-confirmation checkpoints. - `cs-epic` prepares planning and goal packages, then dispatches a visible goal driver; v1 still writes `.codestable/roadmap/`. - `cs-code-review` is the cross-cutting gate; `cs-docs-neat` handles hygiene; `cs-docs` writes outward docs. - `cs-feedback` explicitly captures a local-private current-session evidence package; public issue upload remains separately confirmed. diff --git a/README.md b/README.md index a784990..35db133 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ CodeStable 顺着软件编码的真实流程来设计,把开发活动建模成 | 流程 | 推荐主入口 | 说明 | |------|------------|------| -| **特性引入** | `cs-feat` | 一个入口端到端推进 design → design-review → 用户确认 → goal 包长程执行 implementation → `cs-code-review` → QA → acceptance | +| **特性引入** | `cs-feat` | 按风险自动选择:Quick 直接实现/验证/review;Standard 在当前 run 走 design/impl/review/accept-inline;Goal 才生成 goal 包并执行 QA/acceptance | | **大需求端到端** | `cs-epic` | 大需求规划 → 规划审查 → 用户确认 → 子 feature design/review → goal 执行包 → 派发可见 goal driver(失败则输出 `/goal` 指令) | | **目标达成** | `cs-goal` | 限定起点/终点 → grill 写起点报告 → 自主实现/验证/迭代 → subagent 功能验收 | | **问题修改** | `cs-issue` | 一个入口端到端推进 report → analyze → fix → `cs-code-review` | @@ -187,7 +187,7 @@ CodeStable 顺着软件编码的真实流程来设计,把开发活动建模成 | Epic | `cs-epic` | 大需求端到端:规划、review、子 feature design、goal 包 | | 讨论入口 | `cs-brainstorm` | 想法模糊时分诊到 feature、epic 或 brainstorm note | | 目标 | `cs-goal` | 限定起点/终点后自主迭代到验收 | -| 特性流程 | `cs-feat` | 新特性端到端:design、review、impl、code review、QA、accept | +| 特性流程 | `cs-feat` | 新特性按 Quick / Standard / Goal lane 推进,Goal 仅显式长程执行时启用 | | 问题流程 | `cs-issue` | 问题修复端到端:report、analyze、fix、review | | 重构流程 | `cs-refactor` | 行为等价重构,含标准模式和 fastforward mode | | 横切审查 | `cs-code-review` | 实现完成后、commit 前的只读代码审查 gate | @@ -213,7 +213,7 @@ CodeStable 顺着软件编码的真实流程来设计,把开发活动建模成 ## 工作流与运行时 -CodeStable 是分层、事件驱动的:`cs` 先判入口模式,行动请求同轮直转,咨询请求只给建议;`cs-feat` / `cs-issue` / `cs-refactor` 按仓库事实恢复阶段并经过 `cs-code-review`,其中 issue / refactor 在 review、blocking 或用户确认 checkpoint 停下;`cs-epic` 编排 planning、批量子 design 和 goal driver;旧阶段技能只保留为兼容入口。 +CodeStable 是分层、事件驱动的:`cs` 先判入口模式,行动请求同轮直转,咨询请求只给建议;`cs-feat` 再按任务风险选择 Quick / Standard / Goal,并按仓库事实恢复阶段;`cs-issue` / `cs-refactor` 同样经过 `cs-code-review`,并在 review、blocking 或用户确认 checkpoint 停下,不默认进入 feature QA。`cs-epic` 继续编排 planning、批量子 design 和 goal driver;旧阶段技能只保留为兼容入口。 `cs-onboard` 在项目根生成 `.codestable/`,集中保存 requirements、roadmap、goals、features、issues、refactors、audits、feedback、compound、gates 与共享 reference。Python 工具脚本从已安装的 `cs-onboard` skill 包运行,不再复制到每个 repo。 diff --git a/SKILL_CATALOG.en.md b/SKILL_CATALOG.en.md index 3d1230e..28668b7 100644 --- a/SKILL_CATALOG.en.md +++ b/SKILL_CATALOG.en.md @@ -13,7 +13,7 @@ Main entries accept optional stage / mode flags (for example `/cs-feat --stage q | Epic | `cs-epic` | Large-demand entry: planning, review, child feature design, goal package, and visible driver dispatch | | Goal | `cs-goal` | Autonomous iteration from a defined start state to accepted end state | | Brainstorm | `cs-brainstorm` | Triage unclear ideas into feature, epic, or brainstorm notes | -| Feature flow | `cs-feat` | End-to-end feature entry: design, review, goal package, impl, code review, QA, accept | +| Feature flow | `cs-feat` | Risk-based Quick / Standard / Goal entry; Goal is reserved for explicit long-range execution or Epic | | Issue flow | `cs-issue` | End-to-end issue entry: report, analyze, fix, review | | Refactor flow | `cs-refactor` | Behavior-preserving refactor entry with standard and fastforward modes | | Cross-cutting review | `cs-code-review` | Read-only implementation review gate | diff --git a/SKILL_CATALOG.md b/SKILL_CATALOG.md index 0b115e4..fedc2b9 100644 --- a/SKILL_CATALOG.md +++ b/SKILL_CATALOG.md @@ -13,7 +13,7 @@ | Epic | `cs-epic` | 大需求端到端入口:规划、review、子 feature design、goal 包和可见 driver 派发 | | 目标驱动 | `cs-goal` | 给定起点与期望终态后自主迭代到验收 | | 讨论入口 | `cs-brainstorm` | 想法模糊时分诊到 feature、epic 或 brainstorm note | -| 特性流程 | `cs-feat` | 新特性端到端入口:design、review、goal 包、impl、code review、QA、accept | +| 特性流程 | `cs-feat` | 按风险自动选择 Quick / Standard / Goal;Goal 仅用于显式长程执行或 Epic | | 问题流程 | `cs-issue` | 问题修复端到端入口:report、analyze、fix、review | | 重构流程 | `cs-refactor` | 行为等价重构入口:标准模式或 fastforward mode | | 横切审查 | `cs-code-review` | 实现完成后的只读代码审查 gate | diff --git a/WORKFLOW.en.md b/WORKFLOW.en.md index b1f286d..49e9a73 100644 --- a/WORKFLOW.en.md +++ b/WORKFLOW.en.md @@ -28,6 +28,8 @@ The vertical layout is layering, not strict time order. Long-lived records are r The event entries are `cs-feat` for new capability, `cs-issue` for bugs, `cs-refactor` for behavior-preserving cleanup, and `cs-docs` for outward documentation. `cs-code-review` remains the cross-cutting implementation review gate. +`cs-feat` selects a lane from task risk, never from model identity: Quick is for clear local changes that reuse existing contracts and have targeted verification; Standard adds design and inline acceptance while staying in the current run; Goal is opt-in for explicit long-range execution, existing goal state, or Epic children. + The knowledge and feedback loop remains cross-cutting: `cs-keep` compounds knowledge; explicit `cs-feedback` calls produce local-private incidents/triage and require separate preview confirmation before upload; `cs-docs-neat` handles milestone hygiene. Old stage skills remain long-term compatibility entries: diff --git a/WORKFLOW.md b/WORKFLOW.md index ec87192..f4593f5 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -28,6 +28,12 @@ cs 第 3 层是事件入口:新需求走 `cs-feat`,bug 走 `cs-issue`,腐化走 `cs-refactor`,对外文档走 `cs-docs`。`cs-code-review` 是横切代码审查 gate,feature / issue / refactor 链路都经它产 `{slug}-review.md`。 +`cs-feat` 内部按风险而不是模型名称分三条 lane: + +- Quick:需求明确、改动局部、复用既有契约且有目标验证入口;直接实现、验证、首次独立 review,只写 ff-note。 +- Standard:需要 design 或跨模块决策,但适合当前 run 完成;不默认建 goal package,review 后用 accept-inline 聚合验证。 +- Goal:用户明确要求长程自主执行、已有 goal state 或来自 Epic;保留 goal driver、独立 QA 和完整 acceptance。 + 横切层是知识与反馈飞轮:`cs-keep` 沉淀 compound;`cs-feedback` 仅在显式调用后生成 local-private incident/triage,public preview 经确认后才可上报;`cs-docs-neat` 在里程碑收尾时同步文档与记忆。 旧阶段技能仍是长期兼容入口,但不再作为主路径展示: diff --git a/experiments/cs-feat-routing-001/fixtures/routing/rt-f15.json b/experiments/cs-feat-routing-001/fixtures/routing/rt-f15.json new file mode 100644 index 0000000..61480ab --- /dev/null +++ b/experiments/cs-feat-routing-001/fixtures/routing/rt-f15.json @@ -0,0 +1,27 @@ +{ + "id": "rt-f15", + "answerType": "routing-decision", + "task": { + "kind": "routing", + "state": { + "designStatus": "Missing", + "designReviewStatus": "ReviewMissing", + "goalRunState": "GoalMissing" + }, + "intent": { + "requirementClear": true, + "localChange": true, + "reusesExistingPublicContract": true, + "targetedVerificationKnown": true, + "changesRequirementOrAdr": false, + "highRisk": false + }, + "utterance": "在现有页面保存后调用已经存在的桥接方法,并补对应目标测试" + }, + "expect": { + "result_type": "RoutedTo", + "target": "FastForward", + "must_not_target": "Design" + }, + "_provenance": "GitHub #43:需求明确、局部、复用既有契约且有目标验证的小功能应默认进入 Quick。" +} diff --git a/experiments/cs-feat-routing-001/fixtures/routing/rt-f16.json b/experiments/cs-feat-routing-001/fixtures/routing/rt-f16.json new file mode 100644 index 0000000..ee508c7 --- /dev/null +++ b/experiments/cs-feat-routing-001/fixtures/routing/rt-f16.json @@ -0,0 +1,20 @@ +{ + "id": "rt-f16", + "answerType": "routing-decision", + "task": { + "kind": "routing", + "state": { + "executionLane": "Standard", + "designStatus": "Approved", + "designReviewStatus": "ReviewPassed", + "standardRunState": "StandardImplementationPending", + "goalRunState": "GoalMissing" + } + }, + "expect": { + "result_type": "RoutedTo", + "target": "Implementation", + "must_not_target": "GoalPackage" + }, + "_provenance": "GitHub #43:普通单 feature 的 approved design 应在当前 run 实现,不默认创建 goal package。" +} diff --git a/experiments/cs-feat-routing-001/fixtures/routing/rt-f17.json b/experiments/cs-feat-routing-001/fixtures/routing/rt-f17.json new file mode 100644 index 0000000..7807b70 --- /dev/null +++ b/experiments/cs-feat-routing-001/fixtures/routing/rt-f17.json @@ -0,0 +1,21 @@ +{ + "id": "rt-f17", + "answerType": "routing-decision", + "task": { + "kind": "routing", + "state": { + "executionLane": "Goal", + "designStatus": "Approved", + "designReviewStatus": "ReviewPassed", + "goalRunState": "GoalMissing" + }, + "intent": { + "requestedStage": "goal-package" + } + }, + "expect": { + "result_type": "RoutedTo", + "target": "GoalPackage" + }, + "_provenance": "GitHub #43:用户明确要求长程 Goal 时仍保留完整 goal package 路径。" +} diff --git a/experiments/cs-feat-routing-001/fixtures/routing/rt-f18.json b/experiments/cs-feat-routing-001/fixtures/routing/rt-f18.json new file mode 100644 index 0000000..0c964c0 --- /dev/null +++ b/experiments/cs-feat-routing-001/fixtures/routing/rt-f18.json @@ -0,0 +1,26 @@ +{ + "id": "rt-f18", + "answerType": "routing-decision", + "task": { + "kind": "routing", + "state": { + "designStatus": "Missing", + "designReviewStatus": "ReviewMissing", + "goalRunState": "GoalMissing" + }, + "intent": { + "requirementClear": true, + "localChange": true, + "reusesExistingPublicContract": true, + "targetedVerificationKnown": true, + "highRisk": false + }, + "utterance": "这是小改动,流程太重了,文档已经比代码多;请重新分类后直接完成" + }, + "expect": { + "result_type": "RoutedTo", + "target": "FastForward", + "must_not_target": "Design" + }, + "_provenance": "GitHub #43:用户明确反馈流程过重必须触发重新分类,而不是继续生成全量产物。" +} diff --git a/experiments/cs-feat-routing-001/results.md b/experiments/cs-feat-routing-001/results.md index b58e306..9ee052c 100644 --- a/experiments/cs-feat-routing-001/results.md +++ b/experiments/cs-feat-routing-001/results.md @@ -3,4 +3,4 @@ 七 skill routing 三方 verdict 汇总见 `../cs-issue-routing-001/results.md`(主实验文档)。 本实验 evidence:`artifacts/runs/rt-*.json`。 -> 现有 artifacts 是旧 9-fixture 快照。2026-07-10 runtime-alignment 改造后的 14 个 fixtures 尚未重新做付费多模型测量,不沿用旧分数作为当前 verdict。 +> 现有 artifacts 是旧 9-fixture 快照。2026-07-13 加入 #43 风险分级回归后共有 18 个 fixtures,尚未重新做付费多模型测量,不沿用旧分数作为当前 verdict。 diff --git a/plugins/codestable/skills/cs-code-review/SKILL.md b/plugins/codestable/skills/cs-code-review/SKILL.md index 396d631..583e841 100644 --- a/plugins/codestable/skills/cs-code-review/SKILL.md +++ b/plugins/codestable/skills/cs-code-review/SKILL.md @@ -1,6 +1,6 @@ --- name: cs-code-review -description: "Code review gate。触发:实现完成后、QA/验收/commit 前审本轮 diff。这是只读横切审查 gate;不要用于实现或修改代码(cs-feat/cs-issue/cs-refactor 的实现动作)、也不用于主动扫一片代码找潜在问题的仓库级审查(cs-audit)。" +description: "Code review gate。实现完成后做首次独立审查;review-fix 后按变化风险选择 focused closure 或完整独立复审。这是只读横切 gate,不实现代码、不替代 cs-audit。" argument-hint: "[--range ] [scope]" contracts: - grep: "{slug}-review.md" @@ -8,6 +8,7 @@ contracts: - grep: "只读" - grep: "blocking" - grep: "references/independent-review/protocol.md" + - grep: "focused closure" - grep: "progressive reference loading" - not-grep: "git push" - not-grep: "read all references" @@ -19,7 +20,7 @@ contracts: 动作前先跑 CodeStable preflight:读 `.codestable/attention.md`(缺失先 `cs-onboard`);不要用 `AGENTS.md`/`CLAUDE.md` 等外部入口代替它;细则见 `.codestable/reference/execution-conventions.md`。 -本技能是**横切代码审查 gate**:任何流程实现完成后、commit / QA / 验收前,对当前改动做独立只读 review。它只读代码和产物,只写 `{slug}-review.md`,不直接修代码、不更新 checklist、不改 spec、不替代 QA 或 acceptance。 +本技能是**横切代码审查 gate**:任何流程实现完成后、commit / QA / 验收前做首次独立只读 review;已有 review 的修复 diff 先分类,再做 focused closure 或完整独立复审。它只读代码和产物,只写 `{slug}-review.md`,不直接修代码、不更新 checklist、不改 spec。 审查目标不是追求完美代码,而是确认本次改动没有降低系统代码健康,并且确实朝对应 spec(design / fix-note / refactor-design / 用户确认范围)的目标前进。能自动格式化或 lint 的问题不要手工阻塞;会影响正确性、维护性、安全、性能、可测试性、需求满足或后续验收可信度的问题必须指出。 @@ -48,6 +49,7 @@ data ReviewState = ReviewState -- 从来源 spec 产物 + git diff 恢复 , laneA : LaneStatus -- 环节 A 独立 Task agent review(gate 必需) , laneB : LaneStatus -- 环节 B OCR 行级扫描(装了就跑) , priorReview : Maybe Verdict -- 已有 {slug}-review.md 的 status + , changeClass : Initial | ClosureOnly | Material | Unknown } data LaneStatus = NotStarted | Pending | Completed | Failed | Skipped @@ -55,6 +57,7 @@ data Verdict = Passed | ChangesRequested | Blocked data ReviewOutcome = ReviewWritten Verdict -- {slug}-review.md 落盘,reviewer 字段按已完成环节写 + | FocusedClosure Verdict -- 复用已完成的首次 reviewer,追加可归因 closure evidence | HumanCheckpoint CheckpointReason -- 停下等用户确认,不越过继续 | NeedsHuman Reason -- 无法开审 @@ -83,7 +86,8 @@ selectReviewOutcome(s) | 来源 | 进入点 | spec 产物 | 通过后去向 | |---|---|---|---| -| `cs-feat` implementation 阶段 | impl 完成、QA 前 | design + checklist | `cs-feat` QA 阶段 | +| `cs-feat` Standard lane | impl 完成、accept-inline 前 | design + checklist | `cs-feat` acceptance(Inline Verification Matrix) | +| `cs-feat` Goal lane | impl 完成、QA 前 | design + checklist + goal evidence | `cs-feat` QA 阶段 | | `cs-feat` fastforward mode | ff-note 落盘、commit 前 | ff-note + 用户原始需求 | 收尾提交 | | `cs-issue` fix 阶段 | fix-note 落盘、commit 前 | report + analysis + fix-note | 收尾提交 | | `cs-refactor` standard mode | apply-notes 完成、commit 前 | scan + refactor-design + checklist | 收尾提交 | @@ -128,13 +132,14 @@ ad-hoc 参数如果含 `--range`,审查范围来自 `git diff {range}`,不 4. 如果已有 `{slug}-review.md`: - `status: passed` 且 diff 未变化:提示按表进入「通过后去向」。 - `status: changes-requested` / `blocked`:读取旧 findings,确认是否处于 review-fix 后的复审。 - - diff 已变化:重新 review,并在报告里记录这是第几轮。 + - diff 已变化:先分类。仅 `test/docs/type/metadata/nit-only`,且不改变行为、公开契约、安全、数据、并发或架构的可归因修正,走 focused closure;其余属于实质变化并增加完整独立复审轮次。 + - 无法确定变化类别、跨会话无法还原 review 后增量,或 closure diff 混入生产行为修改:fail-closed 做完整独立复审。 --- ## 独立 reviewer 编排 -本阶段必须按 `references/independent-review/protocol.md` 执行双环节 review:环节 A 是独立 Task agent review(gate 必需),环节 B 是 OCR 行级扫描(装了就跑)。主 agent 只负责启动、等待、事实核验、合并和落盘,不把本地审查当成环节 A。 +首次审查和实质变化后的完整复审必须按 `references/independent-review/protocol.md` 执行双环节 review:环节 A 是独立 Task agent review,环节 B 是 OCR 行级扫描(装了就跑)。focused closure 不重启双环节,但必须满足启动检查里的窄条件并保留首次 reviewer 锚点。 进入审查流程第 2 步前先读取该 reference;没有读取它就不能启动 reviewer,也不能写 `reviewer` 字段。 @@ -222,7 +227,9 @@ ad-hoc 参数如果含 `--range`,审查范围来自 `git diff {range}`,不 1. 报告 `status: changes-requested`。 2. 告诉用户下一步触发来源实现技能的 review-fix 模式。 3. review-fix 只修 blocking findings;important 是否修由用户或实现者判断,但不能顺手扩大范围。 -4. review-fix 完成后必须重跑本审查,不能跳过直接进入来源的通过后去向。 +4. review-fix 完成后必须回到本审查;由本技能判断 focused closure 或完整独立复审,不能直接进入下游。 + +focused closure 只在首次独立审查已完成、当前主 agent 能精确归因 review 后增量,并且增量仅为 test/docs/type/metadata/nit-only 时成立。主 agent 必须逐条核对原 finding、检查增量 diff、运行目标验证,并在同一报告追加 `Focused Closure`(关闭的 REV、文件/行、命令与结果、为何未改变行为/契约);保留原 `reviewer` 和 `round`。任一条件不满足或无法确定,就完整独立复审。 如果只有 `important`: @@ -231,7 +238,7 @@ ad-hoc 参数如果含 `--range`,审查范围来自 `git diff {range}`,不 如果没有 blocking,且 important 已处理或被明确接受: - 报告 `status: passed`。 -- 告诉用户下一步是「进入来源」表的通过后去向(feature→`cs-feat` QA 阶段)。 +- 告诉用户下一步是「进入来源」表的通过后去向(Standard feature→accept-inline;Goal feature→QA)。 --- @@ -262,14 +269,14 @@ ad-hoc 参数如果含 `--range`,审查范围来自 `git diff {range}`,不 - [ ] 已读取 attention、来源 spec 产物、实现证据、git status、git diff 和相关代码。 - [ ] 已确认来源 spec 产物已定稿(feature 看 checklist steps 全 done);否则退回来源实现技能。 - [ ] 主 agent 已自检 Task agent 能力(Paseo subagent / 原生 Codex/Claude Task/Agent)和 `ocr` CLI,记录可用情况。 -- [ ] 环节 A(独立隔离 agent)和环节 B(OCR,可用时)均已启动,或记录跳过原因。 +- [ ] 首次/完整复审已启动环节 A 与可用的环节 B;focused closure 已证明首次 reviewer 完成、增量可归因且类别合格。 - [ ] 所有已启动的环节均已返回并逐条本地核验合并 / 驳回;否则报告 `status: blocked`,没有进入 QA。 - [ ] 已做整体审查和行级审查。 - [ ] 已明确区分 blocking / important / nit / suggestion / learning / praise / residual-risk。 - [ ] 已写来源 spec 目录下的 `{slug}-review.md`(feature 即 `.codestable/features/{feature}/{slug}-review.md`)。 - [ ] `status: passed` 时 frontmatter `reviewer` 已按双环节实际完成写 `subagent+ocr` / `subagent`(或确属无 Task agent 能力的 `ocr` / `self` fallback)——这是下游 gate 的放行锚点。 - [ ] 有 blocking 时没有进入下游,而是指向来源实现技能的 review-fix。 -- [ ] 无 blocking 时明确告诉用户「进入来源」表的通过后去向(feature→`cs-feat` QA 阶段)。 +- [ ] 无 blocking 时明确告诉用户「进入来源」表的 lane-aware 通过后去向。 --- @@ -284,5 +291,5 @@ ad-hoc 参数如果含 `--range`,审查范围来自 `git diff {range}`,不 - OCR High 直接映射成 blocking,跳过本地核验。 - 某路 reviewer 还没返回,就把本地 review 定稿为 passed。 - 某路 reviewer 卡住时不问用户就默默降级成 local-only。 -- blocking 修完后跳过复审,直接验收。 +- blocking 修完后跳过本 gate,或把不确定/行为性修改误写成 focused closure。 - review 报告没有落盘,导致 acceptance 没有可追溯输入。 diff --git a/plugins/codestable/skills/cs-code-review/references/report-template.md b/plugins/codestable/skills/cs-code-review/references/report-template.md index 744e319..c557d04 100644 --- a/plugins/codestable/skills/cs-code-review/references/report-template.md +++ b/plugins/codestable/skills/cs-code-review/references/report-template.md @@ -10,7 +10,7 @@ | issue | `issue-review` | `issue: YYYY-MM-DD-slug` | | refactor / refactor-ff | `refactor-review` | `refactor: YYYY-MM-DD-slug` | -`status` / `reviewed` / `round` 各来源通用。`reviewer` 是 gate 锚点字段,按本轮实际启动的独立 Task agent reviewer / OCR 组合写: +`status` / `reviewed` / `round` 各来源通用。`reviewer` 是首次或最近一次完整独立审查的 gate 锚点;focused closure 保留它,不伪造新 reviewer: | 值 | 含义 | |---|---| @@ -42,6 +42,7 @@ round: 1 - DoD results: {path / none} - Implementation evidence: {实现汇报 / 对话 / 文件} - Diff basis: {git status / git diff 摘要} +- Review mode: initial | full-rereview | focused-closure - Baseline dirty files: {none / 列表 + 归因} ### Independent Review @@ -112,7 +113,14 @@ round: 1 ## 7. Verdict - Status: passed|changes-requested|blocked -- Next: 按「进入来源」表的通过后去向(feature→`cs-feat` QA 阶段,其余→各自验收/提交) | 来源实现技能 review-fix | 等独立 Task agent reviewer 完成 / 用户确认降级后重跑本审查 | 补齐输入后重跑本审查 +- Next: 按「进入来源」表的 lane-aware 去向(Standard feature→accept-inline;Goal feature→QA;其余→验收/提交) | 来源实现技能 review-fix | 等 reviewer / 补输入后重跑 + +## 8. Focused Closure(无则写 none) + +- Closed findings: {REV ids} +- Attributed delta: {files / hunks} +- Targeted verification: {commands + results} +- Classification: {为什么是 test/docs/type/metadata/nit-only,且未改变行为、公开契约、安全、数据、并发或架构} ``` 没有某类 finding 时写 `none`,不要删除章节;下一轮复审要能对比。 diff --git a/plugins/codestable/skills/cs-feat/SKILL.md b/plugins/codestable/skills/cs-feat/SKILL.md index 5a324a3..b5d84fe 100644 --- a/plugins/codestable/skills/cs-feat/SKILL.md +++ b/plugins/codestable/skills/cs-feat/SKILL.md @@ -1,9 +1,10 @@ --- name: cs-feat -description: "Feature 主入口。用于新功能或功能改造,从需求恢复并推进 design、design-review、goal package、implementation、code review、QA、acceptance。不要用于单纯 bug 修复(cs-issue)、行为等价重构(cs-refactor)、对外文档(cs-docs)、大需求拆解(cs-epic)。" -argument-hint: "[--stage design|design-review|impl|qa|accept|goal-package] [--mode fastforward] " +description: "Feature 主入口。用于新功能或功能改造,先按风险自动选择 Quick、Standard 或 Goal lane,再恢复并推进对应流程。不要用于单纯 bug 修复(cs-issue)、行为等价重构(cs-refactor)、对外文档(cs-docs)、大需求拆解(cs-epic)。" +argument-hint: "[--stage design|design-review|impl|qa|accept|goal-package] [--mode quick|standard|goal|fastforward] " contracts: - grep: "restoreFeatureStage" + - grep: "classifyExecutionLane" - grep: "DispatchGoalDriver" - grep: "progressive reference loading" - grep: "must not auto-approve design" @@ -20,7 +21,7 @@ contracts: 动作前先跑 CodeStable preflight:读 `.codestable/attention.md`(缺失先 `cs-onboard`);不要用 `AGENTS.md`/`CLAUDE.md` 等外部入口代替它;细则见 `.codestable/reference/execution-conventions.md`。 -`cs-feat` 是 feature 的唯一推荐入口,是一个 workflow skill:从仓库事实恢复当前阶段、加载对应阶段协议、在人工 checkpoint 停下。用户只需持续调用本技能;本技能在 design gate 停下来等用户确认。确认 design 后默认生成单 feature goal 包,并尝试通过可见 Task agent goal driver 长程执行;派发失败则打印 `/goal` 指令让用户粘贴执行。真正的 design/QA/acceptance 怎么做由各阶段 protocol 负责(见下方 Progressive Reference Loading)。 +`cs-feat` 是 feature 的唯一推荐入口。它先按任务事实选择执行 lane,再从仓库事实恢复阶段:Quick 直接实现;Standard 在当前 run 完成 design、implementation、review 和 accept-inline;Goal 才生成 goal package 并尝试通过可见 Task agent goal driver 长程执行。Standard / Goal 仍在 design gate 停下来等用户确认。真正的各阶段动作由对应 protocol 负责(见 Progressive Reference Loading)。 ## 入口意图 @@ -36,12 +37,39 @@ contracts: | `--stage qa` | `requested_stage: qa` | | `--stage accept` | `requested_stage: acceptance` | | `--stage goal-package` | `requested_stage: goal-package` | -| `--mode fastforward` | `requested_mode: fastforward` | +| `--mode quick` / `--mode fastforward` | `requested_mode: quick`(fastforward 是兼容别名) | +| `--mode standard` | `requested_mode: standard` | +| `--mode goal` | `requested_mode: goal` | 入口意图只是偏好;**仓库事实优先**(`restoreFeatureStage`),也优先于聊天历史。 无参数默认行为:没有 flag / 需求描述时,不猜阶段;扫描 `.codestable/features/`、目标产物与当前 git diff,用状态机恢复下一步。没有可恢复 feature 且用户原话也无新功能目标时,返回 `NeedsHuman` 问处理哪个 feature。 +## 风险分级 + +```haskell +data ExecutionLane = Quick | Standard | Goal + +classifyExecutionLane :: FeatureState -> EntryIntent -> ExecutionLane +classifyExecutionLane(s, intent) + | s.epicChildBatch || hasGoalState(s) || hasRoadmapOwner(s) = Goal + | hasExistingDesign(s) = recordedLaneOrConfirmedReclassification(s, intent) + | explicitlyRequestsGoal(intent) = Goal + | explicitlyRequestsStandard(intent) = Standard + | explicitlyRequestsQuick(intent) && quickEligible(s, intent) = Quick + | explicitlyRequestsQuick(intent) = Standard + | quickEligible(s, intent) = Quick + | otherwise = Standard +``` + +`quickEligible` 必须同时满足:需求与验收行为明确;改动局部且挂载点已知;复用既有公开契约,不新增或改变跨系统协议;目标验证入口已知;不涉及 requirement/ADR 边界、迁移、权限/安全、并发或高风险数据语义。任一项未知就选 Standard,不按模型名称、推理档位或所谓“智商”选 lane。 + +Standard 用于需要新契约、跨模块决策或正式 design,但适合当前 run 完成的单 feature。Goal lane 只在用户明确要求长程自主执行、显式 `--stage goal-package` / `--mode goal`、已有 `goal-state.yaml`,或 Epic 批量上下文时选择;任务较大本身不自动等于 Goal。 + +用户说“这是小改动”“流程太重”“文档比代码多”或同义反馈时,必须暂停继续建产物并重新分类。满足 Quick 就立即降级;仍有风险条件时逐条说明为什么不能降级,不得沿既定流程无视反馈。 + +已有 design 时不允许入口参数静默越过已记录 lane。用户明确要求降级且重新核对仍满足 `quickEligible` 时,先把 `execution_lane: quick` 和降级原因写回 design,再进入 FastForward;已有 `goal-state.yaml` 时不得原地降级,必须先按 Goal 协议安全 handoff,再由 owner 决定是否重分类。 + ## Spec ```haskell @@ -49,7 +77,7 @@ csFeat :: FeatureRequest -> FeatureOutcome data FeatureRequest = FeatureRequest { requestedStage : Maybe Stage - , requestedMode : Maybe Mode -- fastforward + , requestedMode : Maybe Mode -- quick | standard | goal;fastforward -> quick , userGoal : Maybe Text , repoFacts : RepoFacts -- 优先于 args / 聊天历史 } @@ -58,6 +86,13 @@ data Stage = Design | DesignReview | GoalPackage | Implementation | CodeReview | data DesignReviewStatus = ReviewMissing | ReviewPassed | ChangesRequested | ReviewBlocked +data QuickRunState = QuickImplementationPending | QuickReviewReady | QuickReviewFixing | QuickComplete + +data StandardRunState + = StandardImplementationPending + | StandardReviewReady | StandardReviewFixing + | StandardAcceptanceReady | StandardComplete + data GoalRunState -- 从 goal-state.yaml 的 stage/status/driver 字段恢复 = GoalMissing | GoalReadyToDispatch -- implementation / ready-to-dispatch @@ -70,11 +105,15 @@ data GoalRunState -- 从 goal-state.yaml 的 stage/status | GoalHandoffBlocked Reason -- handoff / blocked;优先于残留 driver 元数据 | GoalUnknown Text -data FeatureState = FeatureState -- 全部从 .codestable/features/{slug}/ 恢复 +data FeatureState = FeatureState -- 从 feature 目录及 parent roadmap items / goal-state 恢复 { featureDir : Maybe Path + , executionLane : Maybe ExecutionLane , designStatus : Missing | Draft | Approved , designReviewStatus : DesignReviewStatus + , quickRunState : QuickRunState + , standardRunState : StandardRunState , goalRunState : GoalRunState + , roadmapOwner : Maybe EpicOwnership , epicChildBatch : Bool -- cs-epic 批量上下文,非公开参数 } @@ -97,29 +136,40 @@ restoreFeatureStage :: FeatureState -> EntryIntent -> FeatureOutcome restoreFeatureStage(s, intent) | ambiguousTarget(s, intent) -> NeedsHuman "which feature?" | changesApprovedScope(s, intent) -> HumanCheckpoint ConfirmScopeChange - | wantsFastForward(intent) && ffEligible(s) -> RoutedTo FastForward - | wantsFastForward(intent) && not ffEligible(s) -> RoutedTo Design -- 不合格(含跨公开契约/大范围):结果是 RoutedTo Design(不是 NeedsHuman、不是 checkpoint),在推进 design 的同时说明降级原因 + | lane == Quick && s.quickRunState == QuickImplementationPending -> RoutedTo FastForward + | lane == Quick && s.quickRunState == QuickReviewReady -> RoutedTo CodeReview + | lane == Quick && s.quickRunState == QuickReviewFixing -> RoutedTo FastForward + | lane == Quick && s.quickRunState == QuickComplete -> Completed summary | s.designStatus == Missing -> RoutedTo Design | s.designStatus == Approved && s.designReviewStatus /= ReviewPassed -> NeedsHuman "approved design lacks passed design-review" | s.designReviewStatus in [ChangesRequested, ReviewBlocked] -> RoutedTo Design | s.designStatus == Draft && s.designReviewStatus == ReviewMissing -> RoutedTo DesignReview + | hasRoadmapOwner(s) && s.designReviewStatus == ReviewPassed -> RoutedTo | s.designReviewStatus == ReviewPassed && s.designStatus /= Approved -> if s.epicChildBatch then RoutedTo else HumanCheckpoint ConfirmDesign - | s.goalRunState == GoalMissing -> RoutedTo GoalPackage - | s.goalRunState == GoalComplete -> Completed summary - | s.goalRunState is GoalHandoffBlocked reason -> GoalHandoff (handoffCommand reason) - | s.goalRunState is GoalDriverActive driver -> ReportDriver driver - | s.goalRunState == GoalReadyToDispatch -> DispatchGoalDriver "/goal" - | s.goalRunState == GoalImplementationRunning -> RoutedTo Implementation - | s.goalRunState == GoalReviewReady -> RoutedTo CodeReview - | s.goalRunState == GoalReviewFixing -> RoutedTo Implementation -- review-fix - | s.goalRunState == GoalQAReady -> RoutedTo QA - | s.goalRunState == GoalQAFixing -> RoutedTo Implementation -- qa-fix,修完重跑 review+QA - | s.goalRunState == GoalAcceptanceReady -> RoutedTo Acceptance - | s.goalRunState is GoalUnknown raw -> NeedsHuman ("unknown goal-state: " <> raw) + | lane == Standard && s.standardRunState == StandardImplementationPending -> RoutedTo Implementation + | lane == Standard && s.standardRunState == StandardReviewReady -> RoutedTo CodeReview + | lane == Standard && s.standardRunState == StandardReviewFixing -> RoutedTo Implementation + | lane == Standard && s.standardRunState == StandardAcceptanceReady -> RoutedTo Acceptance + | lane == Standard && s.standardRunState == StandardComplete -> Completed summary + | lane == Goal && s.goalRunState == GoalMissing -> RoutedTo GoalPackage + | lane == Goal && s.goalRunState == GoalComplete -> Completed summary + | lane == Goal && s.goalRunState is GoalHandoffBlocked reason -> GoalHandoff (handoffCommand reason) + | lane == Goal && s.goalRunState is GoalDriverActive driver -> ReportDriver driver + | lane == Goal && s.goalRunState == GoalReadyToDispatch -> DispatchGoalDriver "/goal" + | lane == Goal && s.goalRunState == GoalImplementationRunning -> RoutedTo Implementation + | lane == Goal && s.goalRunState == GoalReviewReady -> RoutedTo CodeReview + | lane == Goal && s.goalRunState == GoalReviewFixing -> RoutedTo Implementation + | lane == Goal && s.goalRunState == GoalQAReady -> RoutedTo QA + | lane == Goal && s.goalRunState == GoalQAFixing -> RoutedTo Implementation + | lane == Goal && s.goalRunState == GoalAcceptanceReady -> RoutedTo Acceptance + | lane == Goal && s.goalRunState is GoalUnknown raw -> NeedsHuman ("unknown goal-state: " <> raw) + where lane = classifyExecutionLane(s, intent) ``` +QuickRunState 从 `{slug}-ff-note.md` 和 `{slug}-review.md` 恢复:无 ff-note→FastForward,review 缺失/blocked→code review,changes-requested→Quick review-fix,有独立 reviewer 锚点的 passed→complete。StandardRunState 从 checklist、review、可选 QA 和 acceptance 恢复;failed/blocked QA 先 qa-fix,passed review 必须有独立 reviewer 锚点。旧 design 缺 `execution_lane` 且没有 goal state 时按 Standard;已有 goal state 始终按 Goal。 + ## Workflow 主执行主线(每次调用按序走;各 stage "怎么做" 的厚规则见对应 protocol,本节只定顺序与边界): @@ -144,8 +194,9 @@ exitRecoverable -- artifact 已落盘 / next stage 明确 / checkpoint reaso .codestable/features/{YYYY-MM-DD}-{slug}/ ├── {slug}-design.md / {slug}-checklist.yaml ├── {slug}-design-review.md -├── goal-plan.md / goal-state.yaml / goal-protocol.md -├── {slug}-review.md / {slug}-qa.md / {slug}-acceptance.md +├── goal-plan.md / goal-state.yaml / goal-protocol.md # 仅 Goal +├── {slug}-review.md / {slug}-acceptance.md +├── {slug}-qa.md # Goal 或用户显式要求 └── {slug}-ff-note.md # 仅 fastforward ``` @@ -168,7 +219,7 @@ stageProtocol FastForward = "references/fastforward/protocol.md" -- 禁止:启动即读全部 references;用 implementation 协议做 design;code review 未过就进 QA ``` -design-review **gate 必需独立 Task agent reviewer**:主 agent 本地审查不得定稿 review、不得给 `passed`;design 修订后的**每一轮重审同样适用**(round 2+ 不得以"本地重审"代替),降级须 approval-report + 用户明确授权(细则见 protocol)。 +design-review 首轮和实质变化后的复审必须使用独立 Task agent reviewer;只改文字、编号、链接、格式或不改变契约的映射时走 focused closure,不启动新 reviewer。无法确定是否实质变化时完整独立复审,细则见 protocol。 ## Human Checkpoints @@ -182,7 +233,7 @@ onCheckpoint ConfirmScopeChange = 停等用户确认范围变更 -- 入口阶段的 fastforward 不合格 / 大范围需求不触发本 checkpoint,直接 RoutedTo Design ``` -implementation / code review / QA / acceptance 的普通阻塞优先由 goal driver 按协议循环修复,不在每个阶段默认打断用户。 +Goal lane 的 implementation / code review / QA / acceptance 阻塞由 goal driver 按协议循环修复。Standard 在当前 run 继续,review passed 后直接进入带 Inline Verification Matrix 的 acceptance;独立 QA 报告不是默认阶段。 driver 不可见、派发失败或 driver 返回 handoff 时走 `GoalHandoff`,不是 `HumanCheckpoint` / `NeedsHuman` 的第二种写法。 ## Failure Behavior @@ -211,15 +262,17 @@ mustStop (ReportDriver _) = True mustStop (Completed _) = True ``` -退出或交接时必须报告:feature 目录、恢复出的 `goalRunState`、本轮写入文件、下一动作或 checkpoint、已运行验证。`DispatchGoalDriver` 不得直接退化成 `/goal`:先按 agent conventions 尝试可见 driver,只有不可用或派发失败才输出 `GoalHandoff`。 +退出或交接时必须报告:feature 目录、`executionLane`、对应 run state、本轮写入文件、下一动作或 checkpoint、已运行验证。Goal 的 `DispatchGoalDriver` 不得直接退化成 `/goal`:先按 agent conventions 尝试可见 driver,只有不可用或派发失败才输出 `GoalHandoff`。 -## Fastforward +完成 marker:Quick 为 `CS_FEATURE_QUICK_COMPLETE`,Standard 为 `CS_FEATURE_STANDARD_COMPLETE`,Goal 为 `CS_FEATURE_GOAL_COMPLETE`;Standard 的 accept-inline 模式通过公开入口 `cs-feat --stage accept` 进入。 -`requested_mode: fastforward` 只是模式请求,不是跳过安全的许可。进入前确认范围小、需求清楚、无跨系统术语/契约风险;不合格则解释原因并回标准 design 流程。产物固定 `{slug}-ff-note.md`,不生成标准 design/checklist/QA/acceptance 套件。`cs-feat-ff` 是兼容入口,不再单独维护快速模式规则。 +## Quick / Fastforward + +Quick 默认按任务事实自动选择;`requested_mode: quick|fastforward` 只是显式偏好,不是跳过安全的许可。不合格则解释命中的风险条件并进入 Standard design。新 Quick 的业务产物只有 `{slug}-ff-note.md`;从已有 design 降级时保留历史 design 并记录 `execution_lane: quick`,不再维护 checklist/QA/acceptance,但已存在的 QA/acceptance 只有 `passed` 可兼容保留,其他状态必须先解决冲突。横切 review 仍写 `{slug}-review.md`。`cs-feat-ff` 是兼容入口。 ## Epic 子 Feature 批量上下文 -`cs-epic` 批量生成子 feature design 时以内部上下文 `epicChildBatch: true` 调用(非公开参数,不写入 argument-hint)。**该上下文还表示 CONTEXT / adrs / compound 等全局输入已由 `cs-epic` 在批量开始时统一加载,design 阶段复用、不重复读取这些全局输入**(幂等,省掉 N 个子 feature 各扫一遍)。此时:design-review passed 后 design 保持 `draft`、**不执行单 feature 的人工整体 review checkpoint**、不改 approved;design-review passed 但未 approved 时**不在这里停,回到 `cs-epic` 继续下一个子 feature**,等所有 design 统一确认;回写 design/checklist/design-review/items.yaml 后返回 `cs-epic`,**不得用 final answer 要用户确认单个 child**;退出前运行 `python3 /tools/codestable-workflow-next.py feature --feature .codestable/features/YYYY-MM-DD-{slug} --epic-child-batch --json`,若输出 `final_answer_allowed: false` 按 `next_action` 交回 `cs-epic`。单独调用 `cs-feat` 或无该上下文时,仍按普通 checkpoint 停。 +`cs-epic` 批量生成子 feature design 时以内部上下文 `epicChildBatch: true` 调用;该上下文强制 Goal lane,并表示 CONTEXT / adrs / compound 已由 `cs-epic` 统一加载,design 阶段不重复读取这些全局输入。design-review passed 后 design 保持 `draft`,不执行单 feature 的人工整体 review checkpoint;不在这里停,回到 `cs-epic` 继续下一个子 feature,最终由 Epic 批量确认。不得用 final answer 要用户确认单个 child。恢复 child 时,design 的 `roadmap` / `roadmap_item` 必须同时为空或成对存在;成对 metadata 经 parent `items.yaml` 唯一条目及其 `feature` 指针证明,或 parent items 按同一指针/精确目录 slug、roadmap goal-state 按 `features[].feature_dir` 反向唯一认领当前目录时,即使没有 batch flag 也必须交回 `cs-epic`;错误 items/goal-state 结构或路径、forward/reverse 不一致、任意第二 claim 都 fail-closed。退出前运行 `codestable-workflow-next.py feature --epic-child-batch`,按 `next_action` 交回 `cs-epic`。 ## 兼容入口 @@ -232,8 +285,11 @@ mayExit :: State -> Bool mayExit s = artifactPersistedAndRecoverable s -- 当前阶段产物已落盘,状态可由 restoreFeatureStage 从仓库事实恢复 && nextClearlyStated s -- 阻塞项、HumanCheckpoint 或下一阶段已明确说明 -fullyDone :: FeatureState -> Bool -- 标准流程最终门槛 -fullyDone s = designApproved s && reviewPassed s && qaPassed s && acceptancePassed s +fullyDone :: FeatureState -> Bool +fullyDone s = case executionLane s of + Quick -> ffNoteWritten s && reviewPassed s + Standard -> designApproved s && reviewPassed s && acceptancePassed s + Goal -> designApproved s && reviewPassed s && qaPassed s && acceptancePassed s ``` 需要外部文档提示 `cs-docs`;阶段收尾/记忆同步提示 `cs-docs-neat`。 diff --git a/plugins/codestable/skills/cs-feat/references/acceptance/protocol.md b/plugins/codestable/skills/cs-feat/references/acceptance/protocol.md index 51ddaaa..8bb215e 100644 --- a/plugins/codestable/skills/cs-feat/references/acceptance/protocol.md +++ b/plugins/codestable/skills/cs-feat/references/acceptance/protocol.md @@ -48,7 +48,7 @@ 5. **Gate / DoD evidence**——goal / gate 模式下读取 `{slug}-evidence-pack.md`、`{slug}-gate-results.json`、`{slug}-dod-results.json`;缺失或 blocking DoD 无 pass evidence 时退回 implementation gate。 6. **验证证据来源**——独立 `cs-feat` QA 阶段 不是 standalone accept 的硬前置;但验收必须有同等强度的验证证据: - 已有 `{slug}-qa.md`:读取并复核。frontmatter 必须 `doc_type=feature-qa`、`status=passed`;failed / blocked → 退回 `cs-feat` implementation 阶段 qa-fix;status 不是 passed → 不进入验收。 - - 没有 `{slug}-qa.md`:不要强制切去跑 `cs-feat` QA 阶段。在本次 accept 里建立 `Inline Verification Matrix`,对照 design 第 3 节、checklist checks、review Test And QA Focus / residual risk 和项目测试入口,现场运行验证,并把证据写入 acceptance 第 3 节和第 10 节最终审计。这个模式等价于“accept-inline QA”,但不额外生成 QA 报告。 + - Standard lane 没有 `{slug}-qa.md`:不要强制切去跑 `cs-feat` QA 阶段。在本次 accept 里建立 `Inline Verification Matrix`,对照 design 第 3 节、checklist checks、review Test And QA Focus / residual risk 和项目测试入口,现场运行验证,并把证据写入 acceptance 第 3 节和第 10 节最终审计。这个模式等价于“accept-inline QA”,但不额外生成 QA 报告。 - Goal 模式例外:`cs-feat` / `cs-epic` 的 goal 协议都明确要求生成 QA 报告;goal 模式缺 `{slug}-qa.md` 时按 goal protocol 停止,不走 accept-inline。 7. **核心证据复核**——不管证据来自 QA 报告还是 accept-inline,都按同一标准复核: - 功能性或 mixed feature:design 第 3 节、checklist checks、review QA focus 中的核心功能路径必须有运行证据。若核心路径未运行、真实用户/API/运行时路径未验证、必跑命令未执行,acceptance 必须写 `status=blocked`。下一步按原因选择:代码/测试缺口 → `cs-feat` implementation 阶段 qa-fix 后重跑 review 和 accept-inline;环境缺口 → 先补环境;用户希望独立 QA 报告 → 跑 `cs-feat` QA 阶段。 diff --git a/plugins/codestable/skills/cs-feat/references/design-review/protocol.md b/plugins/codestable/skills/cs-feat/references/design-review/protocol.md index 2a9b366..975efed 100644 --- a/plugins/codestable/skills/cs-feat/references/design-review/protocol.md +++ b/plugins/codestable/skills/cs-feat/references/design-review/protocol.md @@ -37,15 +37,15 @@ 5. 如果已有 `{slug}-design-review.md`: - `status: passed` 且 design/checklist 未变化:提示可进入用户整体 review。 - `status: changes-requested` / `blocked`:读取旧 findings,确认是否复审。 - - design/checklist 已变化:重新 review,并在报告里记录轮次。 + - design/checklist 已变化:先按下方“实质变化 vs focused closure”分类,再决定是否增加 review 轮次。 --- ## 独立 Task agent reviewer gate -本阶段必须优先启动独立 Task agent reviewer;本规则对**每一轮 review 都成立**——设计/规划修订后的 round 2+ 重审不得以主 agent 本地重审代替独立审查;当前 agent 的本地审查只能作为合并与事实核验,不能替代独立审查。只有运行时确实没有 Task agent 能力、provider 不可用且无法配置,或用户在看到降级风险后明确授权,才允许 `local-only` / `skipped-by-user`。批量 design、赶时间、主 agent 自认为风险低,都不是降级理由;需要授权降级时,先按 `.codestable/reference/approval-conventions.md` 写 `approval-report.md`,再让用户选择。 +首次 design review 必须优先启动独立 Task agent reviewer。修订后先分类:行为、公开契约、架构边界、验收语义或范围发生**实质变化**时,启动新的完整独立复审;只改文字、编号、链接、格式,或修正不改变上述契约的映射时走 `focused closure`,由主 agent 核对本轮可归因 diff、目标校验和原 finding,追加 closure evidence,不启动新 reviewer、不增加 round。无法确定分类、跨会话无法归因修改,或修订夹带实现/范围变化时,fail-closed 做完整独立复审。 -一旦本轮应该启动或已经启动独立 Task agent reviewer,它就是本轮 review gate 的输入。主 agent 可以先做本地审查草稿,但不能在 reviewer 返回前定稿 `{slug}-design-review.md`、不能给出 `passed`、不能把 design 交给用户确认。reviewer 卡住、失败、权限阻塞或耗时过长时,只能把本轮标成 `blocked` / `independent-review-pending`,让用户决定继续等待、重试 reviewer,或明确降级为 local-only review。 +完整独立审查一旦应该启动或已经启动,reviewer 就是 gate 输入;返回前不能定稿 `passed`。focused closure 只适用于首次独立 reviewer 已完成且原 finding 可窄验证的同一修订链,不得借它绕过首次审查或关闭实质 finding。运行时确实没有 Task agent 能力时,仍须 approval-report + 用户明确授权才可降级。 **检测由主 agent 在运行时自检自己的工具**,不靠脚本猜环境——主 agent 最清楚自己手上有哪些工具。按 Task agent 选择规则启动独立 Task agent reviewer(**plan / read-only 等价 mode 只读启动**,mode 名按 provider capability 发现、一步到位不要先默认 mode 再重起,细则见 agent-conventions「启动 mode」;优先 Paseo subagent,否则当前宿主原生 Codex/Claude Task/Agent): @@ -247,6 +247,13 @@ Summary: E={n}, C={n}, H={n}, H-only core checks={列表或 none}。 - Status: passed|changes-requested|blocked - Next: 交给用户整体 review | 回 `cs-feat` design 阶段 修订后重跑 `cs-feat` design-review 阶段 | 等独立 Task agent reviewer 完成 / 用户确认降级后重跑 + +## 8. Focused Closure(无则写 none) + +- Closed findings: {finding ids} +- Attributed delta: {只含文字、编号、链接、格式或非契约映射的文件/位置} +- Verification: {目标检查与结果} +- Classification: {为什么没有改变行为、公开契约、架构边界、验收语义或范围} ``` 没有某类 finding 时写 `none`,不要删除章节;下一轮复审要能对比。 @@ -258,7 +265,7 @@ Summary: E={n}, C={n}, H={n}, H-only core checks={列表或 none}。 - [ ] 已读取 attention、design、checklist、相关 intent / brainstorm / roadmap / req / arch / compound。 - [ ] 已按 design 声明核验必要代码、接口、类型、组件或命令事实。 - [ ] 已确认 checklist 可解析,steps/checks 都可追溯。 -- [ ] 已按 Task agent 选择规则启动独立 reviewer;若未启动,已记录确无能力 / provider 不可用 / 用户授权降级。 +- [ ] 首次/实质复审已按规则启动独立 reviewer;focused closure 已证明首次 reviewer 完成、增量可归因且类别合格。 - [ ] 如果启动了独立 Task agent reviewer,已等到 completed 并逐条本地核验合并 / 驳回 findings;否则报告 `status: blocked`,没有进入用户 review。 - [ ] 已审查需求边界、术语、名词层、编排层、挂载点、结构健康度、验收契约、steps/checks、基线、交付物、清洁度。 - [ ] 已检查 Acceptance Coverage Matrix、Feature Design Review Invariants 和 Evidence Confidence Ledger。 diff --git a/plugins/codestable/skills/cs-feat/references/design/protocol.md b/plugins/codestable/skills/cs-feat/references/design/protocol.md index f74d9a2..b3fa980 100644 --- a/plugins/codestable/skills/cs-feat/references/design/protocol.md +++ b/plugins/codestable/skills/cs-feat/references/design/protocol.md @@ -18,6 +18,8 @@ 退出前必须运行 `codestable-workflow-next.py feature --epic-child-batch`,用 hook 输出确认 本轮只能交回 `cs-epic`,不能停在单个 child design 确认。 +本阶段初次起草只接收 `cs-feat` 已判定的 Standard 或 Goal lane,并把它写入 design frontmatter 的 `execution_lane`。普通单 feature 默认 `standard`;用户明确要求长程自主执行、显式 goal 模式或 Epic 上下文才写 `goal`。`quick` 只用于已有 design 经 owner 明确要求、重新核对风险后降级的持久化记录;同时写 `execution_lane_reason`,保留历史 design,不再继续维护 checklist/QA/acceptance。已有 goal-state 时必须先安全 handoff,不得仅改 frontmatter 绕过 driver。执行 lane 只描述推进方式,不改变 approved public contract;Standard 不创建 goal package。 + **设计原则**:design 不是实现前的散文说明,而是后续实现和验收的可执行契约。它必须把"做好"翻译成可观察证据,把 steps 切成独立可验证单元,把风险和依赖前置暴露,并在交给用户前做一次自我批判,修掉含糊标准、混合步骤和薄弱依赖。 **推进原则**:design 要把后续执行需要的"操作手册"写到位:当前基线怎么确认、每步完成后看什么证据、哪些命令必须重跑、哪些交付物要真实落盘、失败时回到哪个契约修。否则 implement 只能临场判断,review 没有稳定契约可审,QA 不知道该跑什么证据,acceptance 也只能相信实现汇报。 diff --git a/plugins/codestable/skills/cs-feat/references/design/reference.md b/plugins/codestable/skills/cs-feat/references/design/reference.md index 7b54f80..512f49a 100644 --- a/plugins/codestable/skills/cs-feat/references/design/reference.md +++ b/plugins/codestable/skills/cs-feat/references/design/reference.md @@ -11,16 +11,21 @@ feature: 2026-04-12-user-auth requirement: user-auth-email roadmap: permission-system # 可选:本 feature 从某 roadmap 条目起头时填 roadmap_item: permission-rbac-core # 可选:对应 roadmap items.yaml 里的 slug +execution_lane: standard # standard | goal;已有 design 降级时可记录 quick +execution_lane_reason: "" # 可选;重分类时必填原因 status: draft summary: 支持用户通过邮箱验证码登录后台 tags: [auth, email, login] --- ``` -必填:`doc_type` / `feature` / `status` / `summary` / `tags`。 +必填:`doc_type` / `feature` / `execution_lane` / `status` / `summary` / `tags`。 - `requirement`:填对应 req 的 slug;纯重构 / 技术债允许留空 - `roadmap` / `roadmap_item`:从 roadmap 条目起头时才填,两个一起填或一起空 +- `execution_lane`:普通单 feature 默认 `standard`;只有用户明确要求长程执行或 Epic 上下文才写 `goal`;`quick` 仅表示已有 design 已被明确降级 +- `execution_lane_reason`:初始 Standard/Goal 可省略;重分类 Quick 时记录 owner 信号与风险复核结论 +- 兼容旧 artifact:字段缺失且没有 `goal-state.yaml` 时按 `standard` 恢复;已有 goal state 始终按 `goal` 恢复 ## 2. 顶层节锚点 diff --git a/plugins/codestable/skills/cs-feat/references/fastforward/protocol.md b/plugins/codestable/skills/cs-feat/references/fastforward/protocol.md index ec942d7..d487d3c 100644 --- a/plugins/codestable/skills/cs-feat/references/fastforward/protocol.md +++ b/plugins/codestable/skills/cs-feat/references/fastforward/protocol.md @@ -1,6 +1,6 @@ # Feature Fastforward Protocol -用户让你做小功能时本来 AI 就会直接动手——这个技能**不改变这件事**。它只做一件事:动手前把项目里已沉淀的 CodeStable 知识指给你,按需搜一下,写出来的代码就比裸写多一层保护;动手后回写一份**最简的 `{slug}-ff-note.md`** 让这次工作可追溯、可被 cs-req / cs-domain backfill 看到、能纳入 scoped-commit 提交。 +用户让你做小功能时本来 AI 就会直接动手——Quick 默认按任务事实自动选择,不要求用户事先知道 `--mode fastforward`。本协议只多做两件事:动手前按需读取已沉淀的 CodeStable 知识;动手后回写一份**最简的 `{slug}-ff-note.md`**,让实现与验证可追溯。 很轻:没有 design doc / checklist / 验收清单 / 动手前的用户确认。看完指引,该读代码读、该写代码写、写完回写一段话。 @@ -37,7 +37,9 @@ Glob `.codestable/` 发现可用目录和文档,按需取用: fastforward 直接按当前检出环境改项目源码;CodeStable 不决定分支或检出策略。动手前先确认当前 dirty scope,只把和本次小功能相关的改动纳入结果。 -ff-note 落盘、收尾提交前进入 `cs-code-review` 做独立 diff 评审;Critical/Important 未清零不算完成。需要 commit 时按仓库既有提交规范或 owner 指示执行。 +ff-note 落盘、收尾提交前做首次独立代码审查;Critical/Important 未清零不算完成。需要 commit 时按仓库既有提交规范或 owner 指示执行。 + +断点恢复以 ff-note + review 为准:没有 ff-note 就继续 Quick 实现;ff-note 已有但 review 缺失/blocked 就回 `cs-code-review`;`changes-requested` 时读取 findings 做窄 review-fix、更新 ff-note 验证记录,再回 review;只有 `reviewer: subagent|subagent+ocr` 的 passed review 才打印 `CS_FEATURE_QUICK_COMPLETE`。已有 design 的显式降级必须先按 `cs-feat` 主契约持久化 `execution_lane: quick`,不能只生成 ff-note。 --- @@ -149,8 +151,9 @@ tags: [...] ## 不做什么 - **不写 design doc / checklist / acceptance**——这就是 fastforward 的意义。要写就去 `cs-feat` design 阶段 +- **不生成独立 QA / acceptance 报告**——目标测试、构建和必要烟测写进 ff-note,首次独立代码审查仍保留 - **不跟用户确认方案**——用户让你做小功能就是不想等你开会 -- **不在 `.codestable/` 里留 `{slug}-ff-note.md` 之外的新文件**——除非发现值得沉淀的坑 / 技巧,另起对话用 `cs-keep` 写 +- **不生成标准 feature 套件**——业务流程只写 `{slug}-ff-note.md`;横切 `cs-code-review` 仍按 gate 写 `{slug}-review.md`,知识沉淀另走 `cs-keep` --- @@ -171,6 +174,7 @@ tags: [...] - [ ] 代码写完且用户确认效果 OK - [ ] `{slug}-ff-note.md` 已落盘且四节填齐(顺手发现可省) +- [ ] `{slug}-review.md` 已由独立 reviewer 审查通过 - [ ] 没有未对齐的"顺手发现"(都进 ff-note 末节,留给后续) --- @@ -182,7 +186,7 @@ tags: [...] - **提交范围**:本次代码改动 + `{slug}-ff-note.md` - ff-note 落盘后告诉用户"已就绪,是否代为 commit?",用户明确同意才执行 -收尾 commit 前先进入 `cs-code-review` 做一轮独立 diff 评审,Critical / Important 未清零不进 commit;scoped-commit 发起权归 `cs-code-review`。 +收尾 commit 前先进入 `cs-code-review` 做首次独立 diff 评审,Critical / Important 未清零不进 commit;scoped-commit 发起权归 `cs-code-review`。 按 `shared-conventions.md` 第 3 节"feature-ff"收尾推荐顺序逐项一句话提示(用户"不用"立即跳过): diff --git a/plugins/codestable/skills/cs-feat/references/implementation/protocol.md b/plugins/codestable/skills/cs-feat/references/implementation/protocol.md index dabfbb6..383233d 100644 --- a/plugins/codestable/skills/cs-feat/references/implementation/protocol.md +++ b/plugins/codestable/skills/cs-feat/references/implementation/protocol.md @@ -229,7 +229,7 @@ audit 发现问题就回到对应 step 处理,不把问题留给 acceptance 2. 每个 REV 编号都要留下修复证据:改动文件、验证命令、为什么阻塞已解除。 3. 如果修 blocking 需要改变 design 契约、扩大 feature 范围或触碰 roadmap item 边界,停下来回 `cs-feat` design 阶段 / 用户确认。 4. 修完后跑相关验证和清洁度检查,输出 review-fix 汇报。 -5. 下一步必须重跑 `cs-code-review`;不能直接进入 `cs-feat` acceptance 阶段。 +5. 下一步必须回到 `cs-code-review`;由 review gate 按增量类别选择 focused closure 或完整独立复审,不能直接进入 acceptance。 review-fix 不要求 checklist 新增普通 step,除非用户明确要求把修复动作纳入 checklist 追踪。默认把 REV 编号和证据写在汇报里,保留 review 报告作审查输入。 @@ -249,7 +249,7 @@ qa-fix 不要求 checklist 新增普通 step,除非用户明确要求把修复 ## 写完后输出统一汇报 -所有步骤完成后,按 `support/reference.md` 的"实现完成汇报"模板输出并停等用户 review。模板必须列出真实 `git status`、按步骤归类的改动、方案外触碰、新概念、step 证据、清洁度、交付物、知识候选、最后一轮本地审计和验收场景自检。 +所有步骤完成后,按 `support/reference.md` 的"实现完成汇报"模板输出。Standard lane 在当前 run 继续进入 `cs-code-review`,不额外停一个“是否开始 review” checkpoint;模板仍须列出真实 `git status`、按步骤归类的改动、step 证据、清洁度、交付物和验收场景自检。 Goal 模式例外(本轮按 feature / roadmap 目录下 `goal-protocol.md` 长程执行):汇报照常落盘留档,但不停等用户 review,直接按 goal 协议进入 before_review gates 和 `cs-code-review`;只有命中 goal handoff 条件才停下交还用户。 @@ -276,7 +276,7 @@ Goal 模式例外(本轮按 feature / roadmap 目录下 `goal-protocol.md` 长 ## 退出后 -告诉用户下一步:普通实现完成后触发 `cs-code-review`;review-fix 后重跑 `cs-code-review`;qa-fix 后重跑 `cs-code-review` 和 `cs-feat` QA 阶段。不要顺手进入验收报告;完整话术见 `support/reference.md`。 +告诉用户下一步:普通实现和 review-fix 后都回到 `cs-code-review`;Standard review passed 后进入 accept-inline,Goal review passed 后进入 QA;qa-fix 只适用于 Goal 或用户显式 QA。不要跳过 review gate;完整话术见 `support/reference.md`。 --- diff --git a/plugins/codestable/skills/cs-feat/references/implementation/support/reference.md b/plugins/codestable/skills/cs-feat/references/implementation/support/reference.md index 5f9ac3f..7a20ac6 100644 --- a/plugins/codestable/skills/cs-feat/references/implementation/support/reference.md +++ b/plugins/codestable/skills/cs-feat/references/implementation/support/reference.md @@ -90,7 +90,7 @@ coding feature / coding step 改变代码行为且可自动化观察时,默认 Goal 模式不要停在这句汇报;按 goal 协议更新 `goal-state.yaml` 后进入 before_review gates 和 `cs-code-review`。如果需要改变 approved design、feature 范围、公开契约或 roadmap item,打印 `CS_FEATURE_GOAL_HANDOFF` / `CS_ROADMAP_GOAL_HANDOFF` 并交还用户。 -如果本轮是 review-fix,告诉用户:"review blocking 已按范围修复。下一步重跑 cs-code-review;复审通过后再进入 `cs-feat` QA 阶段。" +如果本轮是 review-fix,告诉用户:"review blocking 已按范围修复。下一步回到 cs-code-review,由 gate 判断 focused closure 或完整独立复审;Standard 通过后 accept-inline,Goal 通过后 QA。" 如果本轮是 qa-fix,告诉用户:"QA 失败项已按范围修复。下一步重跑 cs-code-review;review 通过后重跑 `cs-feat` QA 阶段,再进入 `cs-feat` acceptance 阶段。" diff --git a/plugins/codestable/skills/cs-feat/references/qa/protocol.md b/plugins/codestable/skills/cs-feat/references/qa/protocol.md index ea5d6bc..e691233 100644 --- a/plugins/codestable/skills/cs-feat/references/qa/protocol.md +++ b/plugins/codestable/skills/cs-feat/references/qa/protocol.md @@ -2,6 +2,8 @@ 本阶段是 review 通过后、acceptance 前的 QA gate。它只读代码和产物、运行验证命令 / 浏览器 / API / 手工检查,并写 `{slug}-qa.md`。默认不改代码、不改 checklist、不改 design;发现失败后回到 `cs-feat` implementation 阶段的 qa-fix。 +本阶段默认只用于 Goal lane,或用户在 Standard lane 明确要求独立 QA 报告的情况。Standard 默认把同等强度验证合并进 accept-inline;Quick 不生成独立 QA 报告。 + QA 的目标不是再做一遍 code review,也不是最终归档验收报告。它回答一个问题:在当前工作区里,design 承诺的关键行为是否有足够的运行证据,review 指出的测试焦点和 residual risk 是否被实际覆盖。 > 共享路径与命名约定看 `.codestable/reference/shared-conventions.md` 第 0 节。 diff --git a/plugins/codestable/skills/cs-onboard/references/tools.md b/plugins/codestable/skills/cs-onboard/references/tools.md index 3250f75..0277f88 100644 --- a/plugins/codestable/skills/cs-onboard/references/tools.md +++ b/plugins/codestable/skills/cs-onboard/references/tools.md @@ -101,7 +101,7 @@ python3 /tools/codestable-goal-consistency-gate.py --ro `roadmap-goal-gates.yaml` 是阶段配置入口;`codestable-scope-gate.py`、`codestable-dod-runner.py` 和 `codestable-evidence-pack.py` 是 implementation.before_review 的最小 runtime。`status: protocol-only` 的 gate 只表示协议占位,由 review / QA / acceptance / audit 技能读取证据后执行,不代表已有独立脚本。 `codestable-goal-consistency-gate.py` 是 roadmap_audit.before_complete 的 runtime,检查 goal-state、items、每个 feature 的 review/QA/acceptance/evidence/gate/DoD 产物和 checklist 状态,防止 goal-state 早于证据推进。 -`codestable-workflow-next.py` 是只读下一步解析器,输出 `next_action`、`must_continue` 和 `final_answer_allowed`;`cs-epic` / `cs-feat` 在 child design batch 边界必须按它的 JSON 继续或停 gate。 +`codestable-workflow-next.py` 是只读下一步解析器,输出 `next_action`、`must_continue` 和 `final_answer_allowed`;`cs-epic` / `cs-feat` 在 child design batch 边界必须按它的 JSON 继续或停 gate。单 feature 按仓库事实恢复:feature goal-state 优先为 Goal;design 的完整 roadmap metadata 经 parent items 唯一证明,或被 parent items / roadmap goal-state 反向唯一认领的 child 交回 Epic;显式 feature 指针具有权威性,目录回退按精确 feature slug,多 claim 与错误 owner 结构/路径 fail-closed;ff-note 或 design 的 `execution_lane: quick` 恢复 Quick;旧 design 缺 lane 时恢复 Standard。Quick/Standard 的 passed review 必须有独立 reviewer 锚点,Quick 不得吞掉既有非 passed QA/acceptance;损坏的 YAML/frontmatter 或合法 YAML 中错误的路径/容器在 `--json` 下返回含具体路径的结构化 `blocked`,不得输出 traceback。 如果 skill 包缺少这些 runtime 脚本,说明本机 CodeStable 安装不完整;先更新 / 重装 CodeStable。项目缺少 `gates/` 或 `reference/` 时运行 runtime sync。 --- diff --git a/plugins/codestable/skills/cs-onboard/tools/codestable-workflow-next.py b/plugins/codestable/skills/cs-onboard/tools/codestable-workflow-next.py index 9727370..e51262b 100644 --- a/plugins/codestable/skills/cs-onboard/tools/codestable-workflow-next.py +++ b/plugins/codestable/skills/cs-onboard/tools/codestable-workflow-next.py @@ -17,9 +17,23 @@ if os.environ.get("PYTHONDONTWRITEBYTECODE") != "1": sys.dont_write_bytecode = True from codestable_gate_common import load_yaml, load_yaml_text +from codestable_common import SUBAGENT_REVIEWERS, review_has_subagent_evidence NON_BLOCKING_STATUSES = {"continue", "user_gate", "goal_package", "dispatch_goal", "report_driver", "complete"} +REVIEW_FALLBACK_REVIEWERS = {"ocr", "self"} +VALID_EXECUTION_LANES = {"quick", "standard", "goal"} + + +class ArtifactParseError(Exception): + def __init__(self, path: Path, cause: Exception) -> None: + self.path = path + self.error_type = type(cause).__name__ + super().__init__(f"{path} ({self.error_type})") + + +class FeatureLookupError(Exception): + pass def as_list(value: Any) -> list[Any]: @@ -29,16 +43,29 @@ def as_list(value: Any) -> list[Any]: def frontmatter(path: Path) -> dict[str, Any]: if not path.exists() or path.suffix != ".md": return {} - text = path.read_text(encoding="utf-8") + try: + text = path.read_text(encoding="utf-8") + except Exception as exc: + raise ArtifactParseError(path, exc) from exc if not text.startswith("---"): return {} end = text.find("\n---", 3) if end == -1: return {} - parsed = load_yaml_text(text[3:end].strip()) + try: + parsed = load_yaml_text(text[3:end].strip()) + except Exception as exc: + raise ArtifactParseError(path, exc) from exc return parsed if isinstance(parsed, dict) else {} +def load_yaml_artifact(path: Path) -> Any: + try: + return load_yaml(path) + except Exception as exc: + raise ArtifactParseError(path, exc) from exc + + def project_root(path: Path) -> Path: resolved = path.resolve() for parent in (resolved, *resolved.parents): @@ -82,6 +109,15 @@ def status_of(path: Path | None) -> str: return str(frontmatter(path).get("status", "missing")) +def review_gate_passed(path: Path | None, meta: dict[str, Any]) -> bool: + if path is None: + return False + reviewer = str(meta.get("reviewer") or "").strip().lower() + if reviewer in SUBAGENT_REVIEWERS and review_has_subagent_evidence(path): + return True + return os.environ.get("CODESTABLE_ALLOW_SELF_REVIEW_FALLBACK") == "1" and reviewer in REVIEW_FALLBACK_REVIEWERS + + def feature_slug_from_dir(feature: Path) -> str: match = re.match(r"^\d{4}-\d{2}-\d{2}-(.+)$", feature.name) return match.group(1) if match else feature.name @@ -114,6 +150,27 @@ def decision( } +def artifact_parse_decision( + workflow: str, + target: Path, + error: ArtifactParseError, +) -> dict[str, Any]: + root = project_root(target.resolve()) + invalid_artifact = rel(root, error.path) or error.path.as_posix() + detail = f"{invalid_artifact} ({error.error_type})" + return decision( + workflow=workflow, + status="blocked", + next_action=f"fix-{workflow}-artifact-yaml", + reason=f"cannot parse {workflow} artifact: {detail}", + blocking=[f"fix invalid YAML artifact: {detail}"], + evidence={ + "invalid_artifact": detail, + "artifact_error_type": error.error_type, + }, + ) + + def find_roadmap_file(roadmap: Path) -> Path | None: return first_existing(roadmap / f"{roadmap.name}-roadmap.md") or single_glob(roadmap, "*-roadmap.md") @@ -126,27 +183,72 @@ def find_roadmap_review(roadmap: Path) -> Path | None: return first_existing(roadmap / f"{roadmap.name}-roadmap-review.md") or single_glob(roadmap, "*-roadmap-review.md") +def roadmap_item_rows(data: Any) -> list[dict[str, Any]] | None: + if not isinstance(data, dict): + return None + rows = data.get("items") + if rows is None: + rows = data.get("features") + if rows is None: + rows = [] + if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows): + return None + if any(row.get("feature") is not None and not isinstance(row.get("feature"), str) for row in rows): + return None + return rows + + def load_items(items_path: Path | None) -> list[dict[str, Any]]: if items_path is None or not items_path.exists(): return [] - data = load_yaml(items_path) + data = load_yaml_artifact(items_path) + rows = roadmap_item_rows(data) + if rows is None: + raise ArtifactParseError( + items_path, + ValueError("roadmap items must be a list of mappings with string or null feature pointers"), + ) + return rows + + +def all_checklist_steps_done(checklist: Path | None) -> bool: + if checklist is None or not checklist.exists(): + return False + data = load_yaml_artifact(checklist) if not isinstance(data, dict): - return [] - rows = data.get("items") or data.get("features") or [] - return [row for row in as_list(rows) if isinstance(row, dict)] + return False + steps = as_list(data.get("steps")) + return bool(steps) and all( + isinstance(step, dict) and step.get("status") == "done" for step in steps + ) + + +def checked_yaml_path(value: Any, field: str) -> Path: + if not isinstance(value, str) or "\x00" in value: + raise ValueError(f"{field} must be a string without NUL characters") + return Path(value) + + +def feature_pointer_path(root: Path, feature_value: Any) -> Path | None: + if feature_value is None or feature_value == "" or feature_value == "null": + return None + feature_path = checked_yaml_path(feature_value, "feature pointer") + if feature_path.is_absolute(): + return feature_path + if feature_path.parts and feature_path.parts[0] == ".codestable": + return root / feature_path + return root / ".codestable" / "features" / feature_path def find_feature_dir(root: Path, roadmap_slug: str, item: dict[str, Any]) -> Path | None: feature_value = item.get("feature") - if feature_value not in {None, "", "null"}: - feature_path = Path(str(feature_value)) - if not feature_path.is_absolute(): - if feature_path.parts and feature_path.parts[0] == ".codestable": - feature_path = root / feature_path - else: - feature_path = root / ".codestable" / "features" / feature_path - if feature_path.exists(): - return feature_path + try: + feature_path = feature_pointer_path(root, feature_value) + if feature_path is not None: + feature_path.resolve() + return feature_path if feature_path.exists() else None + except (ValueError, OSError) as exc: + raise FeatureLookupError(f"invalid feature pointer: {exc}") from exc item_slug = str(item.get("slug") or "") features_root = root / ".codestable" / "features" @@ -156,7 +258,16 @@ def find_feature_dir(root: Path, roadmap_slug: str, item: dict[str, Any]) -> Pat meta = frontmatter(design) if meta.get("roadmap") == roadmap_slug and meta.get("roadmap_item") == item_slug: return design.parent - matches = sorted(features_root.glob(f"*-{item_slug}")) + matches = sorted( + path + for path in features_root.iterdir() + if path.is_dir() and feature_slug_from_dir(path) == item_slug + ) + if len(matches) > 1: + match_paths = ", ".join(str(rel(root, path)) for path in matches) + raise FeatureLookupError( + f"multiple feature directories match roadmap item {item_slug}: {match_paths}" + ) return matches[0] if matches else None @@ -165,15 +276,562 @@ def feature_artifacts(feature: Path, item_slug: str | None = None) -> dict[str, design = first_existing(feature / f"{slug}-design.md") or single_glob(feature, "*-design.md") checklist = first_existing(feature / f"{slug}-checklist.yaml") or single_glob(feature, "*-checklist.yaml") design_review = first_existing(feature / f"{slug}-design-review.md") or single_glob(feature, "*-design-review.md") + code_review = first_existing(feature / f"{slug}-review.md") + qa = first_existing(feature / f"{slug}-qa.md") + acceptance = first_existing(feature / f"{slug}-acceptance.md") + ff_note = first_existing(feature / f"{slug}-ff-note.md") or single_glob(feature, "*-ff-note.md") goal_state = feature / "goal-state.yaml" return { "design": design, "checklist": checklist, "design_review": design_review, + "code_review": code_review, + "qa": qa, + "acceptance": acceptance, + "ff_note": ff_note, "goal_state": goal_state if goal_state.exists() else None, } +def standard_feature_next( + *, + checklist_steps_done: bool, + code_review: Path | None, + code_review_meta: dict[str, Any], + code_review_gate_passed: bool, + qa: Path | None, + qa_meta: dict[str, Any], + acceptance: Path | None, + acceptance_meta: dict[str, Any], + evidence: dict[str, Any], +) -> dict[str, Any]: + code_review_status = str(code_review_meta.get("status") or "missing") + qa_status = str(qa_meta.get("status") or "missing") + acceptance_status = str(acceptance_meta.get("status") or "missing") + if not checklist_steps_done: + return decision( + workflow="feature", + status="continue", + next_action="cs-feat implementation", + reason="standard feature still has pending implementation steps", + evidence=evidence, + ) + if code_review is not None and code_review_meta.get("doc_type") != "feature-review": + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-code-review-evidence", + reason="standard feature code-review has an invalid doc_type", + blocking=["code review evidence must use doc_type: feature-review"], + evidence=evidence, + ) + if code_review_status == "changes-requested": + return decision( + workflow="feature", + status="continue", + next_action="cs-feat implementation review-fix", + reason="standard feature code review requested changes", + evidence=evidence, + ) + if code_review_status != "passed": + return decision( + workflow="feature", + status="continue", + next_action="cs-code-review", + reason=f"standard feature code-review status is {code_review_status}", + evidence=evidence, + ) + if not code_review_gate_passed: + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-code-review-evidence", + reason="passed code review lacks independent Task agent evidence", + blocking=["passed code review must have reviewer: subagent or subagent+ocr"], + evidence=evidence, + ) + if qa is not None and qa_meta.get("doc_type") != "feature-qa": + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-qa-evidence", + reason="standard feature QA artifact has an invalid doc_type", + blocking=["QA evidence must use doc_type: feature-qa"], + evidence=evidence, + ) + if qa_status in {"failed", "blocked"}: + return decision( + workflow="feature", + status="continue", + next_action="cs-feat --stage impl qa-fix", + reason=f"standard feature QA is {qa_status} and requires qa-fix", + evidence=evidence, + ) + if qa is not None and qa_status != "passed": + return decision( + workflow="feature", + status="continue", + next_action="cs-feat --stage qa", + reason=f"standard feature QA status is {qa_status}", + evidence=evidence, + ) + if acceptance is not None and acceptance_meta.get("doc_type") != "feature-acceptance": + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-acceptance-evidence", + reason="standard feature acceptance has an invalid doc_type", + blocking=["acceptance evidence must use doc_type: feature-acceptance"], + evidence=evidence, + ) + if acceptance_status == "passed": + return decision( + workflow="feature", + status="complete", + next_action="CS_FEATURE_STANDARD_COMPLETE", + reason="standard feature review and acceptance are passed", + evidence=evidence, + ) + return decision( + workflow="feature", + status="continue", + next_action="cs-feat --stage accept", + reason="standard feature review passed and needs inline verification and acceptance", + evidence=evidence, + ) + + +def quick_feature_next( + *, + ff_note: Path | None, + ff_note_meta: dict[str, Any], + code_review: Path | None, + code_review_meta: dict[str, Any], + code_review_gate_passed: bool, + qa: Path | None, + qa_meta: dict[str, Any], + acceptance: Path | None, + acceptance_meta: dict[str, Any], + evidence: dict[str, Any], +) -> dict[str, Any]: + quality_artifacts = ( + ("QA", qa, qa_meta, "feature-qa", evidence.get("qa")), + ("acceptance", acceptance, acceptance_meta, "feature-acceptance", evidence.get("acceptance")), + ) + for label, path, meta, expected_doc_type, artifact_path in quality_artifacts: + if path is None: + continue + status = str(meta.get("status") or "missing") + doc_type = meta.get("doc_type") + if doc_type == expected_doc_type and status == "passed": + continue + detail = f"doc_type={doc_type or 'missing'}, status={status}" + return decision( + workflow="feature", + status="blocked", + next_action="resolve-quick-quality-conflict", + reason=f"Quick feature has conflicting existing {label} evidence", + blocking=[f"reconcile {artifact_path} before Quick can complete ({detail})"], + evidence=evidence, + ) + if ff_note is None: + return decision( + workflow="feature", + status="continue", + next_action="cs-feat --mode quick", + reason="quick feature implementation and ff-note are still pending", + evidence=evidence, + ) + if ff_note_meta.get("doc_type") != "feature-ff-note": + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-ff-note", + reason="quick feature ff-note has an invalid doc_type", + blocking=["Quick evidence must use doc_type: feature-ff-note"], + evidence=evidence, + ) + if code_review is not None and code_review_meta.get("doc_type") != "feature-review": + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-code-review-evidence", + reason="quick feature code-review has an invalid doc_type", + blocking=["code review evidence must use doc_type: feature-review"], + evidence=evidence, + ) + code_review_status = str(code_review_meta.get("status") or "missing") + if code_review_status == "changes-requested": + return decision( + workflow="feature", + status="continue", + next_action="cs-feat --mode quick", + reason="quick feature review-fix is required before review can pass", + evidence=evidence, + ) + if code_review_status != "passed": + return decision( + workflow="feature", + status="continue", + next_action="cs-code-review", + reason=f"quick feature code-review status is {code_review_status}", + evidence=evidence, + ) + if not code_review_gate_passed: + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-code-review-evidence", + reason="passed Quick review lacks independent Task agent evidence", + blocking=["passed code review must have reviewer: subagent or subagent+ocr"], + evidence=evidence, + ) + return decision( + workflow="feature", + status="complete", + next_action="CS_FEATURE_QUICK_COMPLETE", + reason="quick feature ff-note and independent code review are complete", + evidence=evidence, + ) + + +def roadmap_owner_evidence( + root: Path, + roadmap: Path, + goal_state: Path, + state: dict[str, Any], + row: dict[str, Any], + roadmap_item: str, +) -> dict[str, Any]: + return { + "owner_workflow": "epic", + "roadmap_owner_source": "roadmap-goal-state", + "roadmap": rel(root, roadmap), + "roadmap_item": roadmap_item, + "roadmap_goal_state": rel(root, goal_state), + "roadmap_goal_status": state.get("status"), + "roadmap_feature_status": row.get("status"), + "epic_command": tool_command( + "codestable-workflow-next.py", + "epic", + "--roadmap", + rel(root, roadmap), + "--json", + ), + } + + +def roadmap_items_owner_evidence( + root: Path, + roadmap: Path, + items_path: Path, + goal_state: Path, + row: dict[str, Any], + roadmap_item: str, +) -> dict[str, Any]: + return { + "owner_workflow": "epic", + "roadmap_owner_source": "roadmap-items", + "roadmap": rel(root, roadmap), + "roadmap_item": roadmap_item, + "roadmap_items": rel(root, items_path), + "roadmap_goal_state": rel(root, goal_state), + "roadmap_goal_status": "missing", + "roadmap_feature_status": row.get("status"), + "epic_command": tool_command( + "codestable-workflow-next.py", + "epic", + "--roadmap", + rel(root, roadmap), + "--json", + ), + } + + +def roadmap_items_owner( + root: Path, + roadmap: Path, + roadmap_item: str, + feature: Path, + goal_state: Path, +) -> tuple[dict[str, Any] | None, str | None]: + items_path = find_items_file(roadmap) + if items_path is None: + expected = roadmap / f"{roadmap.name}-items.yaml" + return None, f"feature design references roadmap without items: {rel(root, expected)}" + data = load_yaml_artifact(items_path) + rows = roadmap_item_rows(data) + if rows is None: + return None, f"roadmap items must be a list of mappings: {rel(root, items_path)}" + items_roadmap = str(data.get("roadmap") or "").strip() + if items_roadmap and items_roadmap != roadmap.name: + return None, f"roadmap items identity does not match feature design: {rel(root, items_path)}" + matching_rows = [ + row + for row in rows + if str(row.get("roadmap_item") or row.get("slug") or "").strip() == roadmap_item + ] + if len(matching_rows) != 1: + return None, f"roadmap items do not uniquely own this feature: {rel(root, items_path)}" + row = matching_rows[0] + try: + owner_path = feature_pointer_path(root, row.get("feature")) + if owner_path is not None and owner_path.resolve() != feature.resolve(): + return None, f"roadmap items feature pointer does not match this feature: {rel(root, items_path)}" + except (ValueError, OSError) as exc: + return None, f"roadmap items feature pointer is invalid in {rel(root, items_path)} ({exc})" + owner = roadmap_items_owner_evidence(root, roadmap, items_path, goal_state, row, roadmap_item) + reverse_owner, reverse_error = reverse_roadmap_items_owner( + root, + feature, + exclude_items_claim=(items_path, roadmap_item), + ) + expected_path = str(rel(root, items_path)) + if reverse_error is not None: + return None, f"roadmap items {expected_path} owns this feature; {reverse_error}" + if reverse_owner is not None: + actual_path = reverse_owner.get("roadmap_items") + return None, f"multiple roadmap items claim this feature: {expected_path}, {actual_path}" + return owner, None + + +def reverse_roadmap_items_owner( + root: Path, + feature: Path, + exclude_items_claim: tuple[Path, str] | None = None, +) -> tuple[dict[str, Any] | None, str | None]: + roadmap_root = root / ".codestable" / "roadmap" + if not roadmap_root.exists(): + return None, None + + owners: list[tuple[Path, Path, dict[str, Any], str]] = [] + invalid_items: list[str] = [] + for roadmap in sorted(path for path in roadmap_root.iterdir() if path.is_dir()): + items_path = find_items_file(roadmap) + if items_path is None: + continue + items_path_text = str(rel(root, items_path)) + try: + data = load_yaml(items_path) + except Exception as exc: + invalid_items.append(f"{items_path_text} ({type(exc).__name__})") + continue + rows = roadmap_item_rows(data) + if rows is None: + invalid_items.append(items_path_text) + continue + identity_matches = str(data.get("roadmap") or "").strip() in {"", roadmap.name} + for row in rows: + try: + owner_path = find_feature_dir(root, roadmap.name, row) + owner_matches = owner_path is not None and owner_path.resolve() == feature.resolve() + except (FeatureLookupError, ValueError, OSError) as exc: + invalid_items.append(f"{items_path_text} ({exc})") + continue + if not owner_matches: + continue + roadmap_item = str(row.get("roadmap_item") or row.get("slug") or "").strip() + if ( + exclude_items_claim is not None + and items_path.resolve() == exclude_items_claim[0].resolve() + and roadmap_item == exclude_items_claim[1] + ): + continue + if not identity_matches or not roadmap_item: + invalid_items.append(items_path_text) + continue + owners.append((roadmap, items_path, row, roadmap_item)) + + if invalid_items: + invalid_paths = ", ".join(sorted(set(invalid_items))) + return None, f"cannot prove unique legacy roadmap ownership; invalid items: {invalid_paths}" + if len(owners) > 1: + owner_paths = ", ".join(sorted({str(rel(root, owner[1])) for owner in owners})) + return None, f"multiple roadmap items claim this legacy feature_dir: {owner_paths}" + if not owners: + return None, None + roadmap, items_path, row, roadmap_item = owners[0] + goal_state = roadmap / "goal-state.yaml" + return roadmap_items_owner_evidence(root, roadmap, items_path, goal_state, row, roadmap_item), None + + +def reverse_roadmap_goal_owner( + root: Path, + feature: Path, + exclude_goal_state_claim: tuple[Path, str] | None = None, +) -> tuple[dict[str, Any] | None, str | None]: + roadmap_root = root / ".codestable" / "roadmap" + if not roadmap_root.exists(): + return None, None + + owners: list[tuple[Path, Path, dict[str, Any], dict[str, Any], str]] = [] + invalid_states: list[str] = [] + for goal_state in sorted(roadmap_root.glob("*/goal-state.yaml")): + roadmap = goal_state.parent + goal_state_path = str(rel(root, goal_state)) + try: + state = load_yaml(goal_state) + except Exception as exc: # Legacy recovery cannot silently skip an unreadable possible owner. + invalid_states.append(f"{goal_state_path} ({type(exc).__name__})") + continue + if not isinstance(state, dict): + invalid_states.append(goal_state_path) + continue + identity_matches = str(state.get("roadmap") or "").strip() == roadmap.name + feature_rows = state.get("features") + if not isinstance(feature_rows, list) or any(not isinstance(row, dict) for row in feature_rows): + invalid_states.append(goal_state_path) + continue + for row in feature_rows: + feature_dir = row.get("feature_dir") + if feature_dir is None or feature_dir == "" or feature_dir == "null": + continue + if not isinstance(feature_dir, str): + invalid_states.append(goal_state_path) + continue + try: + owner_path = checked_yaml_path(feature_dir, "feature_dir") + if not owner_path.is_absolute(): + owner_path = root / owner_path + owner_matches = owner_path.resolve() == feature.resolve() + except (ValueError, OSError) as exc: + invalid_states.append(f"{goal_state_path} ({type(exc).__name__})") + continue + if not owner_matches: + continue + if not identity_matches: + invalid_states.append(goal_state_path) + continue + roadmap_item = str(row.get("roadmap_item") or row.get("slug") or "").strip() + if not roadmap_item: + invalid_states.append(goal_state_path) + continue + if ( + exclude_goal_state_claim is not None + and goal_state.resolve() == exclude_goal_state_claim[0].resolve() + and roadmap_item == exclude_goal_state_claim[1] + ): + continue + owners.append((roadmap, goal_state, state, row, roadmap_item)) + + if invalid_states: + invalid_paths = ", ".join(sorted(set(invalid_states))) + return None, f"cannot prove unique legacy roadmap ownership; invalid goal-state: {invalid_paths}" + if len(owners) > 1: + owner_paths = ", ".join(sorted({str(rel(root, owner[1])) for owner in owners})) + return None, f"multiple roadmap goal-states claim this legacy feature_dir: {owner_paths}" + if not owners: + return None, None + roadmap, goal_state, state, row, roadmap_item = owners[0] + return roadmap_owner_evidence(root, roadmap, goal_state, state, row, roadmap_item), None + + +def roadmap_goal_owner( + root: Path, + feature: Path, + design_meta: dict[str, Any], +) -> tuple[dict[str, Any] | None, str | None]: + roadmap_slug = str(design_meta.get("roadmap") or "").strip() + roadmap_item = str(design_meta.get("roadmap_item") or "").strip() + if bool(roadmap_slug) != bool(roadmap_item): + return None, "feature design must define roadmap and roadmap_item together" + if not roadmap_slug: + reverse_owner, reverse_error = reverse_roadmap_goal_owner(root, feature) + if reverse_owner is not None or reverse_error is not None: + return reverse_owner, reverse_error + return reverse_roadmap_items_owner(root, feature) + try: + roadmap_path = checked_yaml_path(roadmap_slug, "roadmap") + except (ValueError, OSError): + return None, "feature design has an invalid roadmap owner path" + if roadmap_path.name != roadmap_slug: + return None, "feature design has an invalid roadmap owner path" + + roadmap = root / ".codestable" / "roadmap" / roadmap_slug + goal_state = roadmap / "goal-state.yaml" + if not goal_state.exists(): + reverse_owner, reverse_error = reverse_roadmap_goal_owner(root, feature) + expected_path = str(rel(root, goal_state)) + if reverse_error is not None: + return None, f"feature design expects missing roadmap goal-state {expected_path}; {reverse_error}" + if reverse_owner is not None: + actual_path = reverse_owner.get("roadmap_goal_state") + return None, ( + f"feature design expects missing roadmap goal-state {expected_path}; " + f"conflicting roadmap goal-state claims this feature: {actual_path}" + ) + return roadmap_items_owner(root, roadmap, roadmap_item, feature, goal_state) + try: + state = load_yaml(goal_state) + except Exception as exc: # Parser errors must become a recoverable workflow decision. + return None, f"cannot parse roadmap goal-state {rel(root, goal_state)} ({type(exc).__name__})" + if not isinstance(state, dict): + return None, f"roadmap goal-state must be a mapping: {rel(root, goal_state)}" + if str(state.get("roadmap") or "").strip() != roadmap_slug: + return None, f"roadmap goal-state identity does not match feature design: {rel(root, goal_state)}" + + matching_rows = [] + for row in as_list(state.get("features")): + if not isinstance(row, dict): + continue + row_item = str(row.get("roadmap_item") or row.get("slug") or "").strip() + if row_item == roadmap_item: + matching_rows.append(row) + if len(matching_rows) != 1: + return None, f"roadmap goal-state does not uniquely own this feature: {rel(root, goal_state)}" + + row = matching_rows[0] + feature_dir = row.get("feature_dir") + if feature_dir is not None and feature_dir != "" and feature_dir != "null": + if not isinstance(feature_dir, str): + return None, f"roadmap goal-state feature_dir must be a string: {rel(root, goal_state)}" + try: + owner_path = checked_yaml_path(feature_dir, "feature_dir") + if not owner_path.is_absolute(): + owner_path = root / owner_path + owner_matches = owner_path.resolve() == feature.resolve() + except (ValueError, OSError) as exc: + return None, ( + f"roadmap goal-state feature_dir is invalid: {rel(root, goal_state)} " + f"({type(exc).__name__})" + ) + if not owner_matches: + return None, f"roadmap goal-state feature_dir does not match this feature: {rel(root, goal_state)}" + + owner = roadmap_owner_evidence(root, roadmap, goal_state, state, row, roadmap_item) + reverse_owner, reverse_error = reverse_roadmap_goal_owner( + root, + feature, + exclude_goal_state_claim=(goal_state, roadmap_item), + ) + expected_path = str(rel(root, goal_state)) + if reverse_error is not None: + return None, f"roadmap goal-state {expected_path} owns this feature; {reverse_error}" + if reverse_owner is not None: + actual_path = reverse_owner.get("roadmap_goal_state") + return None, ( + f"multiple roadmap goal-states claim this feature: {expected_path}, {actual_path}" + ) + items_path = find_items_file(roadmap) + reverse_items_owner, reverse_items_error = reverse_roadmap_items_owner( + root, + feature, + exclude_items_claim=(items_path, roadmap_item) if items_path is not None else None, + ) + expected_items_path = str(rel(root, items_path)) if items_path is not None else expected_path + if reverse_items_error is not None: + return None, ( + f"roadmap goal-state {expected_path} owns this feature via {expected_items_path}; " + f"{reverse_items_error}" + ) + if reverse_items_owner is not None: + actual_items_path = reverse_items_owner.get("roadmap_items") + return None, ( + f"multiple roadmap items claim this feature: {expected_items_path}, {actual_items_path}" + ) + return owner, None + + def item_evidence(root: Path, item: dict[str, Any], feature: Path | None, artifacts: dict[str, Path | None]) -> dict[str, Any]: return { "item": item.get("slug"), @@ -188,7 +846,7 @@ def item_evidence(root: Path, item: dict[str, Any], feature: Path | None, artifa } -def epic_next(roadmap: Path) -> dict[str, Any]: +def _epic_next(roadmap: Path) -> dict[str, Any]: roadmap = roadmap.resolve() root = project_root(roadmap) if not roadmap.is_dir(): @@ -278,7 +936,17 @@ def epic_next(roadmap: Path) -> dict[str, Any]: if item.get("status") == "dropped": completed.append({"item": item_slug, "status": "dropped"}) continue - feature = find_feature_dir(root, roadmap.name, item) + try: + feature = find_feature_dir(root, roadmap.name, item) + except FeatureLookupError as exc: + return decision( + workflow="epic", + status="blocked", + next_action="fix-roadmap-items", + reason=str(exc), + blocking=[f"resolve ambiguous feature ownership in {rel(root, items_path)}"], + evidence={"roadmap": rel(root, roadmap), "items": rel(root, items_path)}, + ) artifacts = feature_artifacts(feature, item_slug) if feature else {"design": None, "checklist": None, "design_review": None} missing = [ label @@ -325,7 +993,7 @@ def epic_next(roadmap: Path) -> dict[str, Any]: evidence={"roadmap": rel(root, roadmap), "items": rel(root, items_path), "completed_items": completed}, ) - state = load_yaml(goal_state) + state = load_yaml_artifact(goal_state) state = state if isinstance(state, dict) else {} state_status = state.get("status") if state_status in {"complete", "completed"}: @@ -379,7 +1047,14 @@ def epic_next(roadmap: Path) -> dict[str, Any]: ) -def feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: +def epic_next(roadmap: Path) -> dict[str, Any]: + try: + return _epic_next(roadmap) + except ArtifactParseError as error: + return artifact_parse_decision("epic", roadmap, error) + + +def _feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: feature = feature.resolve() root = project_root(feature) if not feature.is_dir(): @@ -393,19 +1068,150 @@ def feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: artifacts = feature_artifacts(feature) design = artifacts["design"] - review = artifacts["design_review"] + checklist = artifacts["checklist"] + design_review = artifacts["design_review"] + code_review = artifacts["code_review"] + qa = artifacts["qa"] + acceptance = artifacts["acceptance"] + ff_note = artifacts["ff_note"] goal_state = artifacts["goal_state"] - design_status = status_of(design) - review_status = status_of(review) + design_meta = frontmatter(design) if design else {} + design_review_meta = frontmatter(design_review) if design_review else {} + code_review_meta = frontmatter(code_review) if code_review else {} + qa_meta = frontmatter(qa) if qa else {} + acceptance_meta = frontmatter(acceptance) if acceptance else {} + ff_note_meta = frontmatter(ff_note) if ff_note else {} + design_status = str(design_meta.get("status") or "missing") + design_review_status = str(design_review_meta.get("status") or "missing") + code_review_status = str(code_review_meta.get("status") or "missing") + qa_status = str(qa_meta.get("status") or "missing") + acceptance_status = str(acceptance_meta.get("status") or "missing") + checklist_steps_done = all_checklist_steps_done(checklist) + code_review_gate_ok = review_gate_passed(code_review, code_review_meta) + design_roadmap = str(design_meta.get("roadmap") or "").strip() + design_roadmap_item = str(design_meta.get("roadmap_item") or "").strip() + roadmap_metadata_incomplete = bool(design_roadmap) != bool(design_roadmap_item) + roadmap_owner, roadmap_owner_error = roadmap_goal_owner(root, feature, design_meta) + configured_lane_value = design_meta.get("execution_lane") + configured_lane = str(configured_lane_value).strip().lower() if configured_lane_value is not None else None + lane_conflict = False + if epic_child_batch: + execution_lane = "goal" + execution_lane_source = "epic-child-batch" + elif goal_state is not None: + execution_lane = "goal" + execution_lane_source = "feature-goal-state" + elif roadmap_owner is not None: + execution_lane = "goal" + execution_lane_source = str(roadmap_owner.get("roadmap_owner_source") or "roadmap-goal-state") + elif ff_note is not None and configured_lane not in {None, "quick"}: + execution_lane = str(configured_lane) + execution_lane_source = "conflict" + lane_conflict = True + elif ff_note is not None: + execution_lane = "quick" + execution_lane_source = "ff-note" + elif configured_lane is not None: + execution_lane = configured_lane + execution_lane_source = "design" + else: + execution_lane = "standard" + execution_lane_source = "legacy-default" evidence = { "feature_dir": rel(root, feature), "design": rel(root, design), "design_status": design_status, - "design_review": rel(root, review), - "design_review_status": review_status, + "design_roadmap": design_roadmap or None, + "design_roadmap_item": design_roadmap_item or None, + "execution_lane": execution_lane, + "execution_lane_source": execution_lane_source, + "configured_execution_lane": configured_lane, + "checklist": rel(root, checklist), + "checklist_steps_done": checklist_steps_done, + "design_review": rel(root, design_review), + "design_review_status": design_review_status, + "code_review": rel(root, code_review), + "code_review_status": code_review_status, + "code_review_doc_type": code_review_meta.get("doc_type"), + "code_review_reviewer": code_review_meta.get("reviewer"), + "code_review_gate_passed": code_review_gate_ok, + "qa": rel(root, qa), + "qa_status": qa_status, + "qa_doc_type": qa_meta.get("doc_type"), + "acceptance": rel(root, acceptance), + "acceptance_status": acceptance_status, + "acceptance_doc_type": acceptance_meta.get("doc_type"), + "ff_note": rel(root, ff_note), "goal_state": rel(root, goal_state), + "roadmap_owner_error": roadmap_owner_error, } + if roadmap_owner_error is not None: + return decision( + workflow="feature", + status="blocked", + next_action=( + "fix-feature-roadmap-metadata" + if roadmap_metadata_incomplete + else "inspect-epic-goal-state" + ), + reason=roadmap_owner_error, + blocking=[roadmap_owner_error], + evidence=evidence, + ) + if goal_state is not None and roadmap_owner is not None: + return decision( + workflow="feature", + status="blocked", + next_action="resolve-feature-goal-ownership", + reason="feature and roadmap goal-state both claim this feature", + blocking=["feature cannot be owned by both standalone Goal and Epic Goal"], + evidence={**evidence, **roadmap_owner}, + ) + if lane_conflict: + return decision( + workflow="feature", + status="blocked", + next_action="resolve-feature-execution-lane-conflict", + reason="ff-note conflicts with the execution lane recorded by the feature design", + blocking=["record execution_lane: quick before resuming a reclassified feature"], + evidence=evidence, + ) + if execution_lane not in VALID_EXECUTION_LANES: + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-execution-lane", + reason=f"unsupported feature execution_lane: {execution_lane}", + blocking=["feature execution_lane must be quick, standard, or goal"], + evidence=evidence, + ) + if ( + execution_lane == "quick" + and design is not None + and not str(design_meta.get("execution_lane_reason") or "").strip() + ): + return decision( + workflow="feature", + status="blocked", + next_action="fix-feature-execution-lane", + reason="reclassified Quick design lacks an execution_lane_reason", + blocking=["record the owner signal and risk recheck before resuming Quick"], + evidence=evidence, + ) + if execution_lane == "quick": + return quick_feature_next( + ff_note=ff_note, + ff_note_meta=ff_note_meta, + code_review=code_review, + code_review_meta=code_review_meta, + code_review_gate_passed=code_review_gate_ok, + qa=qa, + qa_meta=qa_meta, + acceptance=acceptance, + acceptance_meta=acceptance_meta, + evidence=evidence, + ) if design is None: return decision( workflow="feature", @@ -415,58 +1221,72 @@ def feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: missing_artifacts=[f"{feature_slug_from_dir(feature)}-design.md"], evidence=evidence, ) - if design_status == "approved" and review_status != "passed": + if design_status == "approved" and design_review_status != "passed": return decision( workflow="feature", status="blocked", next_action="fix-feature-design-review-state", reason="approved design lacks a passed design-review", - blocking=[f"approved design has design-review status: {review_status}"], + blocking=[f"approved design has design-review status: {design_review_status}"], evidence=evidence, ) - if review_status in {"changes-requested", "blocked"}: + if design_review_status in {"changes-requested", "blocked"}: return decision( workflow="feature", status="continue", next_action="cs-feat design", - reason=f"design-review is {review_status}", + reason=f"design-review is {design_review_status}", evidence=evidence, ) - if design_status == "draft" and review_status != "passed": + if design_status == "draft" and design_review_status != "passed": return decision( workflow="feature", status="continue", next_action="cs-feat design-review", reason="draft design still needs a passed design-review", - missing_artifacts=[] if review else [f"{feature_slug_from_dir(feature)}-design-review.md"], + missing_artifacts=[] if design_review else [f"{feature_slug_from_dir(feature)}-design-review.md"], evidence=evidence, ) - if review_status == "passed" and design_status != "approved": - if epic_child_batch: - meta = frontmatter(design) - roadmap_slug = meta.get("roadmap") - if not roadmap_slug: - return decision( - workflow="feature", - status="blocked", - next_action="fix-feature-roadmap-metadata", - reason="epic child batch feature lacks roadmap frontmatter", - blocking=["feature design must include roadmap and roadmap_item in epic_child_batch"], - evidence=evidence, - ) - roadmap = root / ".codestable" / "roadmap" / str(roadmap_slug) + if design_review_status == "passed" and epic_child_batch: + roadmap_slug = design_meta.get("roadmap") + roadmap_item = design_meta.get("roadmap_item") + if not roadmap_slug or not roadmap_item: return decision( workflow="feature", - status="continue", - next_action="return-to-cs-epic-batch-loop", - reason="epic child design is reviewed; cs-epic must decide the next batch action", - evidence={ - **evidence, - "roadmap": rel(root, roadmap), - "roadmap_item": meta.get("roadmap_item"), - "epic_command": tool_command("codestable-workflow-next.py", "epic", "--roadmap", rel(root, roadmap), "--json"), - }, + status="blocked", + next_action="fix-feature-roadmap-metadata", + reason="epic child batch feature lacks roadmap frontmatter", + blocking=["feature design must include roadmap and roadmap_item in epic_child_batch"], + evidence=evidence, ) + roadmap = root / ".codestable" / "roadmap" / str(roadmap_slug) + return decision( + workflow="feature", + status="continue", + next_action="return-to-cs-epic-batch-loop", + reason="epic child design is reviewed; cs-epic must decide the next batch action", + evidence={ + **evidence, + "roadmap": rel(root, roadmap), + "roadmap_item": roadmap_item, + "epic_command": tool_command( + "codestable-workflow-next.py", + "epic", + "--roadmap", + rel(root, roadmap), + "--json", + ), + }, + ) + if roadmap_owner is not None: + return decision( + workflow="feature", + status="continue", + next_action="return-to-cs-epic", + reason="feature is owned by an existing roadmap goal-state", + evidence={**evidence, **roadmap_owner}, + ) + if design_review_status == "passed" and design_status != "approved": return decision( workflow="feature", status="user_gate", @@ -474,7 +1294,7 @@ def feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: reason="design-review passed and the single feature design awaits user approval", evidence=evidence, ) - if design_status == "approved" and goal_state is None: + if design_status == "approved" and execution_lane == "goal" and goal_state is None: return decision( workflow="feature", status="goal_package", @@ -484,7 +1304,20 @@ def feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: evidence=evidence, ) - state = load_yaml(goal_state) if goal_state else {} + if design_status == "approved" and execution_lane == "standard" and goal_state is None: + return standard_feature_next( + checklist_steps_done=checklist_steps_done, + code_review=code_review, + code_review_meta=code_review_meta, + code_review_gate_passed=code_review_gate_ok, + qa=qa, + qa_meta=qa_meta, + acceptance=acceptance, + acceptance_meta=acceptance_meta, + evidence=evidence, + ) + + state = load_yaml_artifact(goal_state) if goal_state else {} state = state if isinstance(state, dict) else {} stage = state.get("stage") status = state.get("status") @@ -542,6 +1375,13 @@ def feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: ) +def feature_next(feature: Path, epic_child_batch: bool) -> dict[str, Any]: + try: + return _feature_next(feature, epic_child_batch) + except ArtifactParseError as error: + return artifact_parse_decision("feature", feature, error) + + def print_human(result: dict[str, Any]) -> None: print(f"{result['workflow']} next: {result['status']} -> {result['next_action']}") print(result["reason"]) diff --git a/plugins/codestable/skills/cs/SKILL.md b/plugins/codestable/skills/cs/SKILL.md index a36794f..2415856 100644 --- a/plugins/codestable/skills/cs/SKILL.md +++ b/plugins/codestable/skills/cs/SKILL.md @@ -92,6 +92,8 @@ Ambiguous -> HumanCheckpoint ClarifyRoute | 开发者 / 用户指南、API 参考 | `cs-docs` | | 阶段收尾、全局文档与记忆卫生 | `cs-docs-neat` | +`cs` 只决定工作类型;转交 `cs-feat` 后由它做风险分级,自动选择 Quick、Standard 或 Goal。不要因为请求属于“新功能”就预设完整 design/goal 流程。 + 一个请求同一时刻只转交一个主入口。若用户同时给出两个独立诉求,返回 `HumanCheckpoint ClarifyRoute` 询问先后顺序,不并行加载两个目标。 ## 转交协议 @@ -118,7 +120,7 @@ Dispatch: continuing-current-run | recommendation-only 体系速读保持简短,并只介绍推荐主入口: -- 生命周期:`cs-feat` 覆盖 design、design-review、goal 包、impl、code review、QA、accept,并可由可见 driver 长程执行;另有 `cs-issue`、`cs-refactor`、`cs-epic`。 +- 生命周期:`cs-feat` 先做风险分级;Quick 走实现/验证/一次 review,Standard 在当前 run 走 design/impl/review/accept-inline,Goal 才走 goal 包、impl、code review、QA、accept 并可由可见 driver 长程执行;另有 `cs-issue`、`cs-refactor`、`cs-epic`。 - 横切能力:`cs-code-review`、`cs-audit`、`cs-docs`、`cs-docs-neat`、`cs-feedback`。 - 需求与知识:`cs-req`、`cs-domain`、`cs-keep`、`cs-note`。 - 启动与探索:`cs-onboard`、`cs-brainstorm`、`cs-goal`。 diff --git a/tests/test_codestable_workflow_next.py b/tests/test_codestable_workflow_next.py index 3976221..0d8c616 100644 --- a/tests/test_codestable_workflow_next.py +++ b/tests/test_codestable_workflow_next.py @@ -6,6 +6,8 @@ import subprocess import sys from pathlib import Path +import pytest + TOOLS_DIR = Path(__file__).resolve().parents[1] / "plugins/codestable/skills/cs-onboard/tools" sys.path.insert(0, str(TOOLS_DIR)) @@ -56,12 +58,25 @@ def write_roadmap(repo: Path, status: str = "active") -> Path: return roadmap -def write_feature(repo: Path, slug: str, *, design_status: str = "draft", review_status: str = "passed") -> Path: +def write_feature( + repo: Path, + slug: str, + *, + design_status: str = "draft", + review_status: str = "passed", + execution_lane: str | None = None, + execution_lane_reason: str | None = None, + include_roadmap: bool = False, +) -> Path: feature = repo / ".codestable/features" / f"2026-07-02-{slug}" + roadmap_fields = f"roadmap: billing-system\nroadmap_item: {slug}\n" if include_roadmap else "" + lane_field = f"execution_lane: {execution_lane}\n" if execution_lane else "" + lane_reason_field = f"execution_lane_reason: {execution_lane_reason}\n" if execution_lane_reason else "" write( feature / f"{slug}-design.md", f"---\ndoc_type: feature-design\nfeature: 2026-07-02-{slug}\n" - f"roadmap: billing-system\nroadmap_item: {slug}\nstatus: {design_status}\n---\n# Design\n", + f"{roadmap_fields}{lane_field}{lane_reason_field}" + f"status: {design_status}\n---\n# Design\n", ) write(feature / f"{slug}-checklist.yaml", "steps:\n - id: step-1\n status: pending\n") write( @@ -95,10 +110,93 @@ def write_goal_state( write(directory / "goal-state.yaml", "\n".join(lines) + "\n") +def write_code_review( + feature: Path, + slug: str, + *, + status: str = "passed", + reviewer: str | None = "subagent", + doc_type: str = "feature-review", +) -> None: + reviewer_field = f"reviewer: {reviewer}\n" if reviewer is not None else "" + write( + feature / f"{slug}-review.md", + f"---\ndoc_type: {doc_type}\nstatus: {status}\n{reviewer_field}---\n# Review\n", + ) + + +def write_ff_note(feature: Path, slug: str) -> None: + write( + feature / f"{slug}-ff-note.md", + f"---\ndoc_type: feature-ff-note\nfeature: {slug}\ndate: 2026-07-13\n---\n# Fastforward Note\n", + ) + + +def write_roadmap_goal_state(roadmap: Path, *, feature_slug: str = "api-seed") -> None: + write( + roadmap / "goal-state.yaml", + "roadmap: billing-system\n" + "status: ready-to-dispatch\n" + "driver_kind: paseo\n" + 'driver_id: "epic-run-123"\n' + "current_feature_index: 0\n" + "features:\n" + f" - slug: {feature_slug}\n" + f" roadmap_item: {feature_slug}\n" + f" feature_dir: .codestable/features/2026-07-02-{feature_slug}\n" + " status: implementing\n", + ) + + +def write_reverse_owner_state( + repo: Path, + roadmap_slug: str, + *, + state_roadmap: str | None = None, + rows: list[tuple[str | None, str]], +) -> Path: + lines = [ + f"roadmap: {state_roadmap or roadmap_slug}", + "status: ready-to-dispatch", + "features:", + ] + for item, feature_dir in rows: + lines.append(f" - slug: {item}" if item else " - status: implementing") + if item: + lines.append(f" roadmap_item: {item}") + lines.append(f" feature_dir: {feature_dir}") + if item: + lines.append(" status: implementing") + goal_state = repo / ".codestable/roadmap" / roadmap_slug / "goal-state.yaml" + write(goal_state, "\n".join(lines) + "\n") + return goal_state + + +def run_cli_json(repo: Path, workflow: str, path: Path) -> tuple[subprocess.CompletedProcess[str], dict[str, object]]: + completed = subprocess.run( + [ + sys.executable, + (TOOLS_DIR / "codestable-workflow-next.py").as_posix(), + workflow, + f"--{workflow if workflow == 'feature' else 'roadmap'}", + path.as_posix(), + "--json", + ], + cwd=repo, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={"PYTHONDONTWRITEBYTECODE": "1"}, + timeout=10, + ) + return completed, json.loads(completed.stdout) + + def test_epic_continues_when_only_first_child_design_review_passed(tmp_path: Path) -> None: repo = init_repo(tmp_path) roadmap = write_roadmap(repo) - write_feature(repo, "api-seed") + write_feature(repo, "api-seed", include_roadmap=True) result = workflow_next.epic_next(roadmap) @@ -113,8 +211,8 @@ def test_epic_continues_when_only_first_child_design_review_passed(tmp_path: Pat def test_epic_user_gate_only_after_all_child_design_reviews_passed(tmp_path: Path) -> None: repo = init_repo(tmp_path) roadmap = write_roadmap(repo) - write_feature(repo, "api-seed") - write_feature(repo, "ui-seed") + write_feature(repo, "api-seed", include_roadmap=True) + write_feature(repo, "ui-seed", include_roadmap=True) result = workflow_next.epic_next(roadmap) @@ -128,8 +226,8 @@ def test_epic_user_gate_only_after_all_child_design_reviews_passed(tmp_path: Pat def test_epic_goal_package_after_batch_approval(tmp_path: Path) -> None: repo = init_repo(tmp_path) roadmap = write_roadmap(repo) - write_feature(repo, "api-seed", design_status="approved") - write_feature(repo, "ui-seed", design_status="approved") + write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True) result = workflow_next.epic_next(roadmap) @@ -141,7 +239,8 @@ def test_epic_goal_package_after_batch_approval(tmp_path: Path) -> None: def test_feature_epic_child_batch_returns_to_epic_loop(tmp_path: Path) -> None: repo = init_repo(tmp_path) - feature = write_feature(repo, "api-seed") + write_roadmap(repo) + feature = write_feature(repo, "api-seed", include_roadmap=True) result = workflow_next.feature_next(feature, epic_child_batch=True) @@ -168,6 +267,1216 @@ def test_feature_single_mode_stops_at_design_confirmation(tmp_path: Path) -> Non assert result["final_answer_allowed"] is True +def test_feature_legacy_epic_child_with_roadmap_goal_state_returns_to_epic(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write_roadmap_goal_state(roadmap) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "return-to-cs-epic" + assert result["evidence"]["roadmap_item"] == "api-seed" + assert result["evidence"]["roadmap_goal_state"].endswith("billing-system/goal-state.yaml") + + +@pytest.mark.parametrize("artifacts_complete", [False, True]) +def test_feature_legacy_epic_child_without_roadmap_frontmatter_is_reverse_owned( + tmp_path: Path, + artifacts_complete: bool, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write_roadmap_goal_state(roadmap) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + if artifacts_complete: + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "return-to-cs-epic" + assert result["evidence"]["execution_lane"] == "goal" + assert result["evidence"]["execution_lane_source"] == "roadmap-goal-state" + assert result["next_action"] != "CS_FEATURE_STANDARD_COMPLETE" + + +@pytest.mark.parametrize( + ("design_status", "artifacts_complete"), + [("draft", False), ("approved", True)], +) +def test_feature_pre_goal_package_metadata_less_child_with_items_pointer_returns_to_epic( + tmp_path: Path, + design_status: str, + artifacts_complete: bool, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write( + roadmap / "billing-system-items.yaml", + "roadmap: billing-system\n" + "items:\n" + " - slug: api-seed\n" + " status: planned\n" + " feature: .codestable/features/2026-07-02-api-seed\n", + ) + feature = write_feature(repo, "api-seed", design_status=design_status, include_roadmap=False) + if artifacts_complete: + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + epic_result = workflow_next.epic_next(roadmap) + + assert result["status"] == "continue" + assert result["next_action"] == "return-to-cs-epic" + assert result["evidence"]["execution_lane"] == "goal" + assert result["evidence"]["execution_lane_source"] == "roadmap-items" + assert result["evidence"]["roadmap_items"].endswith("billing-system-items.yaml") + assert result["next_action"] not in {"feature-design-confirmation", "CS_FEATURE_STANDARD_COMPLETE"} + assert epic_result["next_action"] == ( + "all-feature-designs-confirmation" if design_status == "draft" else "cs-epic goal-package" + ) + + +@pytest.mark.parametrize( + ("design_status", "artifacts_complete"), + [("draft", False), ("approved", True)], +) +def test_feature_pre_goal_package_metadata_less_child_with_items_glob_returns_to_epic( + tmp_path: Path, + design_status: str, + artifacts_complete: bool, +) -> None: + repo = init_repo(tmp_path) + write_roadmap(repo) + feature = write_feature(repo, "api-seed", design_status=design_status, include_roadmap=False) + if artifacts_complete: + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "return-to-cs-epic" + assert result["evidence"]["execution_lane_source"] == "roadmap-items" + assert result["next_action"] not in {"feature-design-confirmation", "CS_FEATURE_STANDARD_COMPLETE"} + + +@pytest.mark.parametrize("same_items_file", [False, True]) +def test_reverse_items_owner_multiple_claims_fail_closed( + tmp_path: Path, + same_items_file: bool, +) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + feature_dir = ".codestable/features/2026-07-02-api-seed" + first = repo / ".codestable/roadmap/first-roadmap/first-roadmap-items.yaml" + if same_items_file: + write( + first, + "roadmap: first-roadmap\nitems:\n" + f" - slug: api-seed\n feature: {feature_dir}\n" + f" - slug: api-seed-copy\n feature: {feature_dir}\n", + ) + expected_paths = [first.relative_to(repo).as_posix()] + else: + second = repo / ".codestable/roadmap/second-roadmap/second-roadmap-items.yaml" + write(first, f"roadmap: first-roadmap\nitems:\n - slug: api-seed\n feature: {feature_dir}\n") + write(second, f"roadmap: second-roadmap\nitems:\n - slug: api-seed\n feature: {feature_dir}\n") + expected_paths = [first.relative_to(repo).as_posix(), second.relative_to(repo).as_posix()] + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert "multiple roadmap items claim" in result["reason"] + assert all(path in result["blocking"][0] for path in expected_paths) + + +@pytest.mark.parametrize("contents", ["items: [unterminated\n", "- api-seed\n"]) +def test_reverse_items_owner_invalid_artifact_fails_closed( + tmp_path: Path, + contents: str, +) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + items_path = repo / ".codestable/roadmap/broken-roadmap/broken-roadmap-items.yaml" + write(items_path, contents) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + expected_path = items_path.relative_to(repo).as_posix() + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert expected_path in result["blocking"][0] + assert expected_path in result["evidence"]["roadmap_owner_error"] + + +@pytest.mark.parametrize( + "contents", + [ + "roadmap: broken-roadmap\nitems:\n slug: api-seed\n feature: null\n", + "roadmap: broken-roadmap\nitems:\n - slug: api-seed\n feature: []\n", + ], +) +def test_reverse_items_owner_invalid_yaml_shape_returns_structured_block( + tmp_path: Path, + contents: str, +) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + items_path = repo / ".codestable/roadmap/broken-roadmap/broken-roadmap-items.yaml" + write(items_path, contents) + + completed, payload = run_cli_json(repo, "feature", feature) + + expected_path = items_path.relative_to(repo).as_posix() + assert completed.returncode == 1 + assert completed.stderr == "" + assert payload["status"] == "blocked" + assert expected_path in payload["blocking"][0] + + +@pytest.mark.parametrize( + "contents", + [ + "roadmap: billing-system\nitems:\n slug: api-seed\n feature: null\n", + "roadmap: billing-system\nitems:\n - slug: api-seed\n feature: []\n", + ], +) +def test_epic_invalid_items_yaml_shape_returns_structured_block( + tmp_path: Path, + contents: str, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + items_path = roadmap / "billing-system-items.yaml" + write(items_path, contents) + + completed, payload = run_cli_json(repo, "epic", roadmap) + + expected_path = items_path.relative_to(repo).as_posix() + assert completed.returncode == 1 + assert completed.stderr == "" + assert payload["status"] == "blocked" + assert payload["next_action"] == "fix-epic-artifact-yaml" + assert expected_path in payload["blocking"][0] + + +@pytest.mark.parametrize("quick", [False, True]) +def test_reverse_items_owner_ignores_unrelated_valid_items(tmp_path: Path, quick: bool) -> None: + repo = init_repo(tmp_path) + if quick: + feature = repo / ".codestable/features/2026-07-13-small-export" + write_ff_note(feature, "small-export") + else: + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write( + repo / ".codestable/roadmap/other-roadmap/other-roadmap-items.yaml", + "roadmap: other-roadmap\n" + "items:\n" + " - slug: other-feature\n" + " feature: .codestable/features/2026-07-02-other-feature\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == ("cs-code-review" if quick else "cs-feat implementation") + assert result["evidence"]["execution_lane"] == ("quick" if quick else "standard") + + +@pytest.mark.parametrize("quick", [False, True]) +def test_reverse_items_owner_ignores_slug_suffix_collision(tmp_path: Path, quick: bool) -> None: + repo = init_repo(tmp_path) + if quick: + item_slug = "export" + feature = repo / ".codestable/features/2026-07-13-small-export" + write_ff_note(feature, "small-export") + write_code_review(feature, "small-export") + else: + item_slug = "auth" + feature = write_feature(repo, "user-auth", design_status="approved", include_roadmap=False) + write( + repo / ".codestable/roadmap/platform/platform-items.yaml", + "roadmap: platform\n" + "items:\n" + f" - slug: {item_slug}\n" + " feature: null\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == ("complete" if quick else "continue") + assert result["next_action"] == ("CS_FEATURE_QUICK_COMPLETE" if quick else "cs-feat implementation") + assert result["evidence"]["execution_lane"] == ("quick" if quick else "standard") + + +def test_reverse_items_owner_multiple_exact_slug_directories_fail_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write(repo / ".codestable/features/2026-07-01-api-seed/placeholder.txt", "") + write( + roadmap / "billing-system-items.yaml", + "roadmap: billing-system\nitems:\n - slug: api-seed\n feature: null\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + epic_result = workflow_next.epic_next(roadmap) + + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert "multiple feature directories match roadmap item api-seed" in result["reason"] + assert epic_result["status"] == "blocked" + assert epic_result["next_action"] == "fix-roadmap-items" + assert "multiple feature directories match roadmap item api-seed" in epic_result["reason"] + + +def test_reverse_items_owner_does_not_fallback_from_explicit_missing_pointer(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write( + roadmap / "billing-system-items.yaml", + "roadmap: billing-system\n" + "items:\n" + " - slug: api-seed\n" + " feature: .codestable/features/2026-07-02-missing-feature\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + epic_result = workflow_next.epic_next(roadmap) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat implementation" + assert result["evidence"]["execution_lane"] == "standard" + assert epic_result["status"] == "continue" + assert epic_result["next_action"] == "cs-feat design/design-review" + assert epic_result["evidence"]["next_item"]["feature_dir"] is None + + +def test_reverse_items_owner_invalid_pointer_returns_structured_block(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + items_path = repo / ".codestable/roadmap/platform/platform-items.yaml" + write( + items_path, + "roadmap: platform\n" + "items:\n" + " - slug: api-seed\n" + ' feature: ".codestable/features/2026\\0-07-02-api-seed"\n', + ) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + completed, payload = run_cli_json(repo, "feature", feature) + + expected_path = items_path.relative_to(repo).as_posix() + assert completed.returncode == 1 + assert completed.stderr == "" + assert payload["status"] == "blocked" + assert expected_path in payload["blocking"][0] + assert payload["next_action"] != "CS_FEATURE_STANDARD_COMPLETE" + + +def test_reverse_owner_ignores_unrelated_valid_goal_state(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write_reverse_owner_state( + repo, + "other-roadmap", + rows=[("other-feature", ".codestable/features/2026-07-02-other-feature")], + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat implementation" + assert result["evidence"]["execution_lane"] == "standard" + + +@pytest.mark.parametrize("quick", [False, True]) +def test_reverse_owner_ignores_unrelated_identity_mismatch(tmp_path: Path, quick: bool) -> None: + repo = init_repo(tmp_path) + if quick: + feature = repo / ".codestable/features/2026-07-13-small-export" + write_ff_note(feature, "small-export") + else: + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write_reverse_owner_state( + repo, + "renamed-roadmap", + state_roadmap="old-roadmap", + rows=[("other-feature", ".codestable/features/2026-07-02-other-feature")], + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == ("cs-code-review" if quick else "cs-feat implementation") + + +def test_reverse_owner_identity_mismatch_claiming_feature_fails_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + goal_state = write_reverse_owner_state( + repo, + "renamed-roadmap", + state_roadmap="old-roadmap", + rows=[("api-seed", ".codestable/features/2026-07-02-api-seed")], + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + expected_path = goal_state.relative_to(repo).as_posix() + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert expected_path in result["blocking"][0] + assert expected_path in result["evidence"]["roadmap_owner_error"] + + +def test_reverse_owner_unparseable_goal_state_fails_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + goal_state = repo / ".codestable/roadmap/broken-roadmap/goal-state.yaml" + write(goal_state, "roadmap: broken-roadmap\nfeatures: [unterminated\n") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + expected_path = goal_state.relative_to(repo).as_posix() + assert result["status"] == "blocked" + assert expected_path in result["blocking"][0] + assert expected_path in result["evidence"]["roadmap_owner_error"] + + +@pytest.mark.parametrize("same_state", [False, True]) +def test_reverse_owner_multiple_claims_fail_closed(tmp_path: Path, same_state: bool) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + feature_dir = ".codestable/features/2026-07-02-api-seed" + first = write_reverse_owner_state(repo, "first-roadmap", rows=[("api-seed", feature_dir)]) + if same_state: + write_reverse_owner_state( + repo, + "first-roadmap", + rows=[("api-seed", feature_dir), ("api-seed-copy", feature_dir)], + ) + expected_paths = [first.relative_to(repo).as_posix()] + else: + second = write_reverse_owner_state(repo, "second-roadmap", rows=[("api-seed", feature_dir)]) + expected_paths = [first.relative_to(repo).as_posix(), second.relative_to(repo).as_posix()] + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert "multiple roadmap goal-states claim" in result["reason"] + assert all(path in result["blocking"][0] for path in expected_paths) + + +def test_reverse_owner_matching_row_without_item_fails_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + goal_state = write_reverse_owner_state( + repo, + "broken-roadmap", + rows=[(None, ".codestable/features/2026-07-02-api-seed")], + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + expected_path = goal_state.relative_to(repo).as_posix() + assert result["status"] == "blocked" + assert expected_path in result["blocking"][0] + assert expected_path in result["evidence"]["roadmap_owner_error"] + + +@pytest.mark.parametrize("forward", [False, True]) +def test_goal_state_invalid_feature_dir_returns_structured_block( + tmp_path: Path, + forward: bool, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + goal_state = roadmap / "goal-state.yaml" + write( + goal_state, + "roadmap: billing-system\n" + "status: ready-to-dispatch\n" + "features:\n" + " - slug: api-seed\n" + " roadmap_item: api-seed\n" + ' feature_dir: ".codestable/features/2026\\0-07-02-api-seed"\n' + " status: implementing\n", + ) + feature = write_feature( + repo, + "api-seed", + design_status="approved", + include_roadmap=forward, + ) + + completed, payload = run_cli_json(repo, "feature", feature) + + expected_path = goal_state.relative_to(repo).as_posix() + assert completed.returncode == 1 + assert completed.stderr == "" + assert payload["status"] == "blocked" + assert expected_path in payload["blocking"][0] + + +@pytest.mark.parametrize( + "features_yaml", + [ + "features:\n slug: api-seed\n feature_dir: .codestable/features/2026-07-02-api-seed\n", + "features:\n - api-seed\n", + ], +) +def test_reverse_goal_owner_invalid_features_shape_fails_closed( + tmp_path: Path, + features_yaml: str, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + goal_state = roadmap / "goal-state.yaml" + write(goal_state, "roadmap: billing-system\nstatus: ready-to-dispatch\n" + features_yaml) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + completed, payload = run_cli_json(repo, "feature", feature) + + expected_path = goal_state.relative_to(repo).as_posix() + assert completed.returncode == 1 + assert completed.stderr == "" + assert payload["status"] == "blocked" + assert expected_path in payload["blocking"][0] + assert payload["next_action"] != "CS_FEATURE_STANDARD_COMPLETE" + + +def test_forward_metadata_missing_state_detects_external_reverse_claim(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + external = write_reverse_owner_state( + repo, + "external-roadmap", + rows=[("api-seed", ".codestable/features/2026-07-02-api-seed")], + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + expected = [ + ".codestable/roadmap/billing-system/goal-state.yaml", + external.relative_to(repo).as_posix(), + ] + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert result["next_action"] != "CS_FEATURE_STANDARD_COMPLETE" + assert all(path in result["blocking"][0] for path in expected) + + +def test_forward_owner_detects_second_external_claim(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write_roadmap_goal_state(roadmap) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + external = write_reverse_owner_state( + repo, + "external-roadmap", + rows=[("api-seed", ".codestable/features/2026-07-02-api-seed")], + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert external.relative_to(repo).as_posix() in result["blocking"][0] + + +@pytest.mark.parametrize( + ("design_status", "artifacts_complete"), + [("draft", False), ("approved", True)], +) +def test_feature_pre_goal_package_roadmap_child_returns_to_epic( + tmp_path: Path, + design_status: str, + artifacts_complete: bool, +) -> None: + repo = init_repo(tmp_path) + write_roadmap(repo) + feature = write_feature(repo, "api-seed", design_status=design_status, include_roadmap=True) + if artifacts_complete: + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "return-to-cs-epic" + assert result["evidence"]["roadmap_owner_source"] == "roadmap-items" + assert result["evidence"]["roadmap_items"].endswith("billing-system-items.yaml") + assert result["next_action"] not in {"feature-design-confirmation", "CS_FEATURE_STANDARD_COMPLETE"} + + +@pytest.mark.parametrize( + ("metadata_field", "metadata_value"), + [("roadmap", "billing-system"), ("roadmap_item", "api-seed")], +) +def test_feature_incomplete_roadmap_metadata_fails_closed( + tmp_path: Path, + metadata_field: str, + metadata_value: str, +) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write( + feature / "api-seed-design.md", + "---\ndoc_type: feature-design\nfeature: 2026-07-02-api-seed\n" + f"{metadata_field}: {metadata_value}\nstatus: approved\n---\n# Design\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "fix-feature-roadmap-metadata" + assert "roadmap and roadmap_item together" in result["reason"] + + +def test_feature_invalid_roadmap_slug_returns_structured_block(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False) + write( + feature / "api-seed-design.md", + "---\n" + "doc_type: feature-design\n" + "feature: 2026-07-02-api-seed\n" + 'roadmap: "billing\\0x"\n' + "roadmap_item: api-seed\n" + "status: approved\n" + "---\n# Design\n", + ) + + completed, payload = run_cli_json(repo, "feature", feature) + + assert completed.returncode == 1 + assert completed.stderr == "" + assert payload["status"] == "blocked" + assert payload["next_action"] == "inspect-epic-goal-state" + assert "invalid roadmap owner path" in payload["reason"] + + +def test_feature_pre_goal_package_roadmap_without_matching_item_fails_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write( + roadmap / "billing-system-items.yaml", + "roadmap: billing-system\nitems:\n - slug: ui-seed\n status: planned\n", + ) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + items_path = ".codestable/roadmap/billing-system/billing-system-items.yaml" + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert items_path in result["blocking"][0] + assert "do not uniquely own" in result["reason"] + + +def test_feature_pre_goal_package_roadmap_item_pointer_must_match_feature(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write( + roadmap / "billing-system-items.yaml", + "roadmap: billing-system\n" + "items:\n" + " - slug: api-seed\n" + " status: planned\n" + " feature: .codestable/features/2026-07-02-other-feature\n", + ) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + items_path = ".codestable/roadmap/billing-system/billing-system-items.yaml" + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert "feature pointer does not match" in result["reason"] + assert items_path in result["blocking"][0] + + +@pytest.mark.parametrize( + "contents", + [ + "roadmap: billing-system\nitems:\n slug: api-seed\n feature: null\n", + "roadmap: billing-system\nitems:\n - slug: api-seed\n feature: []\n", + ], +) +def test_forward_items_owner_invalid_yaml_shape_returns_structured_block( + tmp_path: Path, + contents: str, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + items_path = roadmap / "billing-system-items.yaml" + write(items_path, contents) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + + completed, payload = run_cli_json(repo, "feature", feature) + + expected_path = items_path.relative_to(repo).as_posix() + assert completed.returncode == 1 + assert completed.stderr == "" + assert payload["status"] == "blocked" + assert payload["next_action"] == "inspect-epic-goal-state" + assert expected_path in payload["blocking"][0] + + +@pytest.mark.parametrize("same_items_file", [False, True]) +def test_forward_items_owner_detects_second_items_claim( + tmp_path: Path, + same_items_file: bool, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + feature_dir = ".codestable/features/2026-07-02-api-seed" + primary_items = roadmap / "billing-system-items.yaml" + if same_items_file: + write( + primary_items, + "roadmap: billing-system\nitems:\n" + f" - slug: api-seed\n feature: {feature_dir}\n" + f" - slug: other-item\n feature: {feature_dir}\n", + ) + expected_paths = [primary_items.relative_to(repo).as_posix()] + else: + secondary_items = repo / ".codestable/roadmap/secondary/secondary-items.yaml" + write( + primary_items, + f"roadmap: billing-system\nitems:\n - slug: api-seed\n feature: {feature_dir}\n", + ) + write( + secondary_items, + f"roadmap: secondary\nitems:\n - slug: other-item\n feature: {feature_dir}\n", + ) + expected_paths = [ + primary_items.relative_to(repo).as_posix(), + secondary_items.relative_to(repo).as_posix(), + ] + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert "multiple roadmap items claim" in result["reason"] + assert all(path in result["blocking"][0] for path in expected_paths) + + +@pytest.mark.parametrize("same_items_file", [False, True]) +def test_forward_goal_owner_detects_second_items_claim( + tmp_path: Path, + same_items_file: bool, +) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write_roadmap_goal_state(roadmap) + feature_dir = ".codestable/features/2026-07-02-api-seed" + primary_items = roadmap / "billing-system-items.yaml" + primary = ( + "roadmap: billing-system\nitems:\n" + " - slug: api-seed\n feature: null\n" + ) + if same_items_file: + write(primary_items, primary + f" - slug: other-item\n feature: {feature_dir}\n") + expected_paths = [primary_items.relative_to(repo).as_posix()] + else: + secondary_items = repo / ".codestable/roadmap/secondary/secondary-items.yaml" + write(primary_items, primary) + write( + secondary_items, + f"roadmap: secondary\nitems:\n - slug: other-item\n feature: {feature_dir}\n", + ) + expected_paths = [ + primary_items.relative_to(repo).as_posix(), + secondary_items.relative_to(repo).as_posix(), + ] + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert "multiple roadmap items claim" in result["reason"] + assert all(path in result["blocking"][0] for path in expected_paths) + + +def test_forward_goal_owner_detects_second_claim_in_same_goal_state(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + feature_dir = ".codestable/features/2026-07-02-api-seed" + goal_state = roadmap / "goal-state.yaml" + write( + goal_state, + "roadmap: billing-system\n" + "status: ready-to-dispatch\n" + "features:\n" + " - slug: api-seed\n" + " roadmap_item: api-seed\n" + f" feature_dir: {feature_dir}\n" + " status: implementing\n" + " - slug: duplicate-claim\n" + " roadmap_item: duplicate-claim\n" + f" feature_dir: {feature_dir}\n" + " status: pending\n", + ) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + expected_path = goal_state.relative_to(repo).as_posix() + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert "multiple roadmap goal-states claim" in result["reason"] + assert expected_path in result["blocking"][0] + + +def test_feature_epic_child_batch_never_completes_as_standard(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + write_roadmap(repo) + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=True) + + assert result["status"] == "continue" + assert result["next_action"] == "return-to-cs-epic-batch-loop" + assert result["evidence"]["execution_lane"] == "goal" + assert result["evidence"]["execution_lane_source"] == "epic-child-batch" + assert result["next_action"] != "CS_FEATURE_STANDARD_COMPLETE" + + +def test_feature_draft_owned_by_roadmap_goal_returns_to_epic(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write_roadmap_goal_state(roadmap) + feature = write_feature( + repo, + "api-seed", + design_status="draft", + review_status="passed", + include_roadmap=True, + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "return-to-cs-epic" + assert result["next_action"] != "feature-design-confirmation" + assert result["evidence"]["execution_lane_source"] == "roadmap-goal-state" + + +def test_feature_parent_goal_state_with_mismatched_owner_fails_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write_roadmap_goal_state(roadmap, feature_slug="ui-seed") + feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "inspect-epic-goal-state" + assert "standard" not in result["next_action"] + + +def test_feature_quick_ff_note_recovers_review_then_completion(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = repo / ".codestable/features/2026-07-13-small-export" + write_ff_note(feature, "small-export") + + review = workflow_next.feature_next(feature, epic_child_batch=False) + assert review["status"] == "continue" + assert review["next_action"] == "cs-code-review" + assert review["evidence"]["execution_lane"] == "quick" + + write_code_review(feature, "small-export") + complete = workflow_next.feature_next(feature, epic_child_batch=False) + assert complete["status"] == "complete" + assert complete["next_action"] == "CS_FEATURE_QUICK_COMPLETE" + + +def test_feature_quick_ff_note_routes_review_fix(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = repo / ".codestable/features/2026-07-13-small-export" + write_ff_note(feature, "small-export") + write_code_review(feature, "small-export", status="changes-requested") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat --mode quick" + assert "review-fix" in result["reason"] + + +@pytest.mark.parametrize( + ("artifact", "status"), + [("qa", "failed"), ("qa", "blocked"), ("acceptance", "failed"), ("acceptance", "blocked")], +) +def test_feature_quick_rejects_existing_failed_quality_evidence( + tmp_path: Path, + artifact: str, + status: str, +) -> None: + repo = init_repo(tmp_path) + feature = write_feature( + repo, + "api-seed", + design_status="approved", + execution_lane="quick", + execution_lane_reason="owner-requested-after-risk-recheck", + ) + write_ff_note(feature, "api-seed") + write_code_review(feature, "api-seed") + doc_type = "feature-qa" if artifact == "qa" else "feature-acceptance" + write( + feature / f"api-seed-{artifact}.md", + f"---\ndoc_type: {doc_type}\nstatus: {status}\n---\n# Evidence\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + artifact_path = f".codestable/features/2026-07-02-api-seed/api-seed-{artifact}.md" + assert result["status"] == "blocked" + assert result["next_action"] == "resolve-quick-quality-conflict" + assert artifact_path in result["blocking"][0] + assert result["next_action"] != "CS_FEATURE_QUICK_COMPLETE" + + +def test_feature_quick_allows_existing_passed_quality_evidence(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature( + repo, + "api-seed", + design_status="approved", + execution_lane="quick", + execution_lane_reason="owner-requested-after-risk-recheck", + ) + write_ff_note(feature, "api-seed") + write_code_review(feature, "api-seed") + write(feature / "api-seed-qa.md", "---\ndoc_type: feature-qa\nstatus: passed\n---\n# QA\n") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "complete" + assert result["next_action"] == "CS_FEATURE_QUICK_COMPLETE" + + +def test_feature_reclassified_quick_design_resumes_fastforward(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature( + repo, + "api-seed", + design_status="approved", + execution_lane="quick", + execution_lane_reason="owner-requested-after-risk-recheck", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat --mode quick" + assert result["evidence"]["execution_lane_source"] == "design" + + +def test_feature_reclassified_quick_design_requires_persisted_reason(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="quick") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "fix-feature-execution-lane" + assert "execution_lane_reason" in result["reason"] + + +def test_feature_ff_note_conflicting_with_recorded_standard_lane_fails_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write_ff_note(feature, "api-seed") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "resolve-feature-execution-lane-conflict" + + +def test_feature_goal_state_overrides_reclassified_quick_lane(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="quick") + write_ff_note(feature, "api-seed") + write_goal_state(feature, stage="implementation", status="ready-to-dispatch") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "dispatch_goal" + assert result["evidence"]["execution_lane"] == "goal" + assert result["evidence"]["execution_lane_source"] == "feature-goal-state" + + +def test_feature_standard_lane_recovers_without_goal_package_or_standalone_qa(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + + implementation = workflow_next.feature_next(feature, epic_child_batch=False) + assert implementation["status"] == "continue" + assert implementation["next_action"] == "cs-feat implementation" + assert implementation["evidence"]["execution_lane"] == "standard" + + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + review = workflow_next.feature_next(feature, epic_child_batch=False) + assert review["status"] == "continue" + assert review["next_action"] == "cs-code-review" + + write_code_review(feature, "api-seed") + acceptance = workflow_next.feature_next(feature, epic_child_batch=False) + assert acceptance["status"] == "continue" + assert acceptance["next_action"] == "cs-feat --stage accept" + + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + complete = workflow_next.feature_next(feature, epic_child_batch=False) + assert complete["status"] == "complete" + assert complete["next_action"] == "CS_FEATURE_STANDARD_COMPLETE" + + +def test_feature_standard_lane_routes_review_fix_without_goal_state(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved") + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write( + feature / "api-seed-review.md", + "---\ndoc_type: feature-review\nstatus: changes-requested\n---\n# Review\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat implementation review-fix" + assert result["evidence"]["execution_lane"] == "standard" + + +@pytest.mark.parametrize("qa_status", ["failed", "blocked"]) +def test_feature_standard_lane_routes_failed_qa_to_qa_fix(tmp_path: Path, qa_status: str) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-qa.md", + f"---\ndoc_type: feature-qa\nstatus: {qa_status}\n---\n# QA\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat --stage impl qa-fix" + + +def test_feature_standard_lane_resumes_nonterminal_qa(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write(feature / "api-seed-qa.md", "---\ndoc_type: feature-qa\nstatus: pending\n---\n# QA\n") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat --stage qa" + + +@pytest.mark.parametrize( + ("doc_type", "reviewer"), + [ + ("feature-review", None), + ("feature-review", "self"), + ("feature-review", "ocr"), + ("wrong-review", "subagent"), + ], +) +def test_feature_standard_lane_rejects_untrusted_passed_review( + tmp_path: Path, + doc_type: str, + reviewer: str | None, +) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed", doc_type=doc_type, reviewer=reviewer) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "fix-feature-code-review-evidence" + + +def test_feature_standard_lane_rejects_reviewer_marker_outside_frontmatter(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write( + feature / "api-seed-review.md", + "---\ndoc_type: feature-review\nstatus: passed\nreviewer: self\n---\n" + "# Review\n\nreviewer: subagent\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "fix-feature-code-review-evidence" + + +def test_feature_standard_lane_honors_explicit_self_review_fallback( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed", reviewer="self") + monkeypatch.setenv("CODESTABLE_ALLOW_SELF_REVIEW_FALLBACK", "1") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["next_action"] == "cs-feat --stage accept" + + +def test_feature_execution_lane_is_normalized(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane='" Standard "') + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "continue" + assert result["evidence"]["execution_lane"] == "standard" + + +def test_feature_standard_lane_rejects_wrong_acceptance_doc_type(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n") + write_code_review(feature, "api-seed") + write( + feature / "api-seed-acceptance.md", + "---\ndoc_type: wrong-acceptance\nstatus: passed\n---\n# Acceptance\n", + ) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "fix-feature-acceptance-evidence" + + +def test_feature_standard_lane_parses_checklist_once(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + calls = 0 + original = workflow_next.all_checklist_steps_done + + def counted(checklist: Path | None) -> bool: + nonlocal calls + calls += 1 + return original(checklist) + + monkeypatch.setattr(workflow_next, "all_checklist_steps_done", counted) + workflow_next.feature_next(feature, epic_child_batch=False) + + assert calls == 1 + + +def test_feature_goal_lane_still_requires_goal_package(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="goal") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "goal_package" + assert result["next_action"] == "cs-feat goal-package" + assert result["evidence"]["execution_lane"] == "goal" + + +def test_feature_existing_goal_state_overrides_recorded_standard_lane(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + write_goal_state(feature, stage="implementation", status="ready-to-dispatch") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "dispatch_goal" + assert result["evidence"]["execution_lane"] == "goal" + + +def test_feature_unknown_execution_lane_fails_closed(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", execution_lane="turbo") + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + assert result["status"] == "blocked" + assert result["next_action"] == "fix-feature-execution-lane" + + def test_feature_review_failure_returns_to_design(tmp_path: Path) -> None: repo = init_repo(tmp_path) @@ -260,8 +1569,8 @@ def test_feature_terminal_goal_states_override_stale_driver(tmp_path: Path) -> N def test_epic_goal_runtime_distinguishes_dispatch_and_active_driver(tmp_path: Path) -> None: repo = init_repo(tmp_path) roadmap = write_roadmap(repo) - write_feature(repo, "api-seed", design_status="approved") - write_feature(repo, "ui-seed", design_status="approved") + write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True) write_goal_state(roadmap, status="ready-to-dispatch") dispatch = workflow_next.epic_next(roadmap) @@ -297,8 +1606,8 @@ def test_epic_active_roadmap_requires_review_artifact_before_goal_state(tmp_path def test_epic_terminal_goal_states_override_stale_driver(tmp_path: Path) -> None: repo = init_repo(tmp_path) roadmap = write_roadmap(repo) - write_feature(repo, "api-seed", design_status="approved") - write_feature(repo, "ui-seed", design_status="approved") + write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True) write_goal_state( roadmap, status="complete", @@ -325,10 +1634,68 @@ def test_epic_terminal_goal_states_override_stale_driver(tmp_path: Path) -> None assert handoff["evidence"]["handoff_next"] == "provision migration environment" +@pytest.mark.parametrize( + "artifact", + ["design", "checklist", "review", "qa", "acceptance", "ff-note", "goal-state"], +) +def test_feature_corrupt_artifact_returns_structured_json_block(tmp_path: Path, artifact: str) -> None: + repo = init_repo(tmp_path) + feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard") + corrupt = "---\ndoc_type: [unterminated\n---\n# Corrupt\n" + if artifact == "design": + target = feature / "api-seed-design.md" + write(target, corrupt) + elif artifact == "checklist": + target = feature / "api-seed-checklist.yaml" + write(target, "steps: [unterminated\n") + elif artifact == "goal-state": + target = feature / "goal-state.yaml" + write(target, "stage: [unterminated\n") + else: + target = feature / f"api-seed-{artifact}.md" + write(target, corrupt) + + result = workflow_next.feature_next(feature, epic_child_batch=False) + + expected_path = target.relative_to(repo).as_posix() + assert result["status"] == "blocked" + assert result["next_action"] == "fix-feature-artifact-yaml" + assert expected_path in result["blocking"][0] + assert expected_path in result["evidence"]["invalid_artifact"] + + completed, payload = run_cli_json(repo, "feature", feature) + assert completed.returncode == 1 + assert "Traceback" not in completed.stderr + assert payload["status"] == "blocked" + assert expected_path in str(payload["reason"]) + + +def test_epic_corrupt_goal_state_returns_structured_json_block(tmp_path: Path) -> None: + repo = init_repo(tmp_path) + roadmap = write_roadmap(repo) + write_feature(repo, "api-seed", design_status="approved", include_roadmap=True) + write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True) + target = roadmap / "goal-state.yaml" + write(target, "status: [unterminated\n") + + result = workflow_next.epic_next(roadmap) + + expected_path = target.relative_to(repo).as_posix() + assert result["status"] == "blocked" + assert result["next_action"] == "fix-epic-artifact-yaml" + assert expected_path in result["blocking"][0] + + completed, payload = run_cli_json(repo, "epic", roadmap) + assert completed.returncode == 1 + assert "Traceback" not in completed.stderr + assert payload["status"] == "blocked" + assert expected_path in str(payload["reason"]) + + def test_cli_accepts_json_before_or_after_subcommand(tmp_path: Path) -> None: repo = init_repo(tmp_path) roadmap = write_roadmap(repo) - write_feature(repo, "api-seed") + write_feature(repo, "api-seed", include_roadmap=True) for command in ( [ diff --git a/tests/test_skill_contracts.py b/tests/test_skill_contracts.py index a467985..31b3de6 100644 --- a/tests/test_skill_contracts.py +++ b/tests/test_skill_contracts.py @@ -162,6 +162,11 @@ def test_goal_routing_fixtures_use_current_state_schema() -> None: assert feat["rt-f12"]["expect"]["result_type"] == "GoalHandoff" assert feat["rt-f13"]["expect"]["result_type"] == "NeedsHuman" assert feat["rt-f14"]["expect"]["result_type"] == "HumanCheckpoint" + assert feat["rt-f15"]["expect"]["target"] == "FastForward" + assert feat["rt-f16"]["expect"]["target"] == "Implementation" + assert feat["rt-f16"]["expect"]["must_not_target"] == "GoalPackage" + assert feat["rt-f17"]["expect"]["target"] == "GoalPackage" + assert feat["rt-f18"]["expect"]["target"] == "FastForward" assert epic["rt-p09"]["expect"]["result_type"] == "DispatchGoalDriver" assert epic["rt-p11"]["expect"]["result_type"] == "ReportDriver" diff --git a/tests/test_skill_entry_simplification.py b/tests/test_skill_entry_simplification.py index e0cbd5c..b06a1de 100644 --- a/tests/test_skill_entry_simplification.py +++ b/tests/test_skill_entry_simplification.py @@ -201,7 +201,7 @@ MAIN_ENTRY_SKILLS = [ MAIN_ENTRY_ARGUMENT_HINTS = { "cs": "[request]", "cs-onboard": "[--mode refresh-runtime]", - "cs-feat": "[--stage design|design-review|impl|qa|accept|goal-package] [--mode fastforward] ", + "cs-feat": "[--stage design|design-review|impl|qa|accept|goal-package] [--mode quick|standard|goal|fastforward] ", "cs-issue": "[--stage report|analyze|fix] ", "cs-refactor": "[--stage scan|design|apply] [--mode standard|fastforward] ", "cs-docs": "[--mode tutorial|api] ", @@ -361,6 +361,27 @@ def test_feat_and_epic_document_goal_driver_dispatch() -> None: assert "fenced `/goal`" in epic_goal +def test_feature_contract_classifies_quick_standard_and_goal_lanes() -> None: + feat = (SKILLS / "cs-feat/SKILL.md").read_text(encoding="utf-8") + router = (SKILLS / "cs/SKILL.md").read_text(encoding="utf-8") + fastforward = (SKILLS / "cs-feat/references/fastforward/protocol.md").read_text(encoding="utf-8") + design_reference = (SKILLS / "cs-feat/references/design/reference.md").read_text(encoding="utf-8") + code_review = (SKILLS / "cs-code-review/SKILL.md").read_text(encoding="utf-8") + + assert "data ExecutionLane = Quick | Standard | Goal" in feat + assert "classifyExecutionLane" in feat + assert "quickEligible" in feat + assert "用户明确要求长程自主执行" in feat + assert "已有 `goal-state.yaml`" in feat + assert "这是小改动" in feat and "文档比代码多" in feat + assert "风险分级" in router + assert "默认按任务事实自动选择" in fastforward + assert "execution_lane: standard" in design_reference + assert "focused closure" in code_review + assert "test/docs/type/metadata/nit-only" in code_review + assert "无法确定" in code_review and "完整独立复审" in code_review + + def test_goal_mode_overrides_stage_user_waits() -> None: impl = (SKILLS / "cs-feat/references/implementation/protocol.md").read_text(encoding="utf-8") impl_reference = (SKILLS / "cs-feat/references/implementation/support/reference.md").read_text(encoding="utf-8") diff --git a/tests/test_skill_workflow_scenarios.py b/tests/test_skill_workflow_scenarios.py index 33ebbfd..2464d14 100644 --- a/tests/test_skill_workflow_scenarios.py +++ b/tests/test_skill_workflow_scenarios.py @@ -174,14 +174,21 @@ def feature_dir(repo: Path, slug: str) -> Path: return repo / ".codestable/features" / f"2026-07-02-{slug}" -def write_feature_design(repo: Path, slug: str, status: str = "draft", roadmap: str | None = None) -> Path: +def write_feature_design( + repo: Path, + slug: str, + status: str = "draft", + roadmap: str | None = None, + execution_lane: str = "standard", +) -> Path: directory = feature_dir(repo, slug) roadmap_fields = "" if roadmap: roadmap_fields = f"roadmap: {roadmap}\nroadmap_item: {slug}\n" write( directory / f"{slug}-design.md", - f"---\ndoc_type: feature-design\nfeature: 2026-07-02-{slug}\n{roadmap_fields}status: {status}\n---\n# Design\n", + f"---\ndoc_type: feature-design\nfeature: 2026-07-02-{slug}\n{roadmap_fields}" + f"execution_lane: {execution_lane}\nstatus: {status}\n---\n# Design\n", ) write( directory / f"{slug}-checklist.yaml", @@ -219,7 +226,9 @@ def feature_next(repo: Path, slug: str) -> Action: if not design.exists(): return Action("load-reference", "cs-feat/references/design/protocol.md") - design_status = frontmatter(design).get("status") + design_meta = frontmatter(design) + design_status = design_meta.get("status") + execution_lane = design_meta.get("execution_lane", "standard") review_status = frontmatter(review).get("status") if design_status == "approved" and review_status != "passed": return Action("blocked", "invalid-approved-design-review") @@ -230,7 +239,9 @@ def feature_next(repo: Path, slug: str) -> Action: if review_status == "passed" and design_status != "approved": return Action("user-checkpoint", "feature-design-confirmation") if design_status == "approved" and not goal_state.exists(): - return Action("load-reference", "cs-feat/references/goal/protocol.md") + if execution_lane == "goal": + return Action("load-reference", "cs-feat/references/goal/protocol.md") + return Action("load-reference", "cs-feat/references/implementation/protocol.md") state = top_level_yaml(goal_state) stage = state.get("stage") @@ -430,7 +441,10 @@ def code_review_next(repo: Path, feature_slug: str | None = None, range_arg: str if frontmatter(review).get("status") == "passed": diff = git(repo, "diff", "--stat").stdout if not diff: - return Action("downstream", "cs-feat QA") + design = directory / f"{feature_slug}-design.md" + lane = frontmatter(design).get("execution_lane", "standard") + downstream = "cs-feat QA" if lane == "goal" else "cs-feat acceptance-inline" + return Action("downstream", downstream) diff = git(repo, "diff", "--stat").stdout if diff: return Action("run-review", f".codestable/features/2026-07-02-{feature_slug}") @@ -548,7 +562,7 @@ def test_feature_long_range_scenario_simulates_human_design_confirmation(tmp_pat "cs-feat", "SKILL.md", "design gate 停下来等用户确认", - "默认生成单 feature goal 包", + "Goal lane", "可见 Task agent goal driver 长程执行", ) assert_doc_contains( @@ -562,7 +576,7 @@ def test_feature_long_range_scenario_simulates_human_design_confirmation(tmp_pat slug = "export-csv" assert feature_next(repo, slug) == Action("load-reference", "cs-feat/references/design/protocol.md") - write_feature_design(repo, slug, status="draft") + write_feature_design(repo, slug, status="draft", execution_lane="goal") assert feature_next(repo, slug) == Action("load-reference", "cs-feat/references/design-review/protocol.md") write_feature_design_review(repo, slug, status="passed") assert feature_next(repo, slug) == Action("user-checkpoint", "feature-design-confirmation") @@ -577,6 +591,64 @@ def test_feature_long_range_scenario_simulates_human_design_confirmation(tmp_pat assert feature_next(repo, slug) == Action("report-visible-driver", "paseo-123") +def test_feature_quick_lane_is_default_for_clear_local_work_and_can_reclassify() -> None: + assert_doc_contains( + "cs-feat", + "SKILL.md", + "classifyExecutionLane", + "Quick", + "Standard", + "Goal", + "复用既有公开契约", + "目标验证入口", + "流程太重", + "重新分类", + "data QuickRunState", + "CS_FEATURE_QUICK_COMPLETE", + "CS_FEATURE_STANDARD_COMPLETE", + "先把 `execution_lane: quick`", + "已有 `goal-state.yaml` 时不得原地降级", + ) + assert_doc_contains( + "cs-feat", + "references/fastforward/protocol.md", + "默认按任务事实自动选择", + "首次独立代码审查", + "不生成独立 QA / acceptance 报告", + ) + + +def test_feature_spec_routes_roadmap_owned_children_to_epic() -> None: + assert_doc_contains( + "cs-feat", + "SKILL.md", + "roadmapOwner", + "hasRoadmapOwner(s)", + "parent `items.yaml`", + "精确目录 slug", + "features[].feature_dir", + "RoutedTo ", + ) + + +def test_feature_review_restarts_only_for_material_changes() -> None: + assert_doc_contains( + "cs-feat", + "references/design-review/protocol.md", + "实质变化", + "focused closure", + "文字、编号、链接、格式", + ) + assert_doc_contains( + "cs-code-review", + "SKILL.md", + "test/docs/type/metadata/nit-only", + "focused closure", + "行为、公开契约、安全、数据、并发或架构", + "完整独立复审", + ) + + @pytest.mark.parametrize("review_status", ["changes-requested", "blocked"]) def test_feature_design_review_failure_returns_to_design(tmp_path: Path, review_status: str) -> None: repo = init_isolated_repo(tmp_path) @@ -858,7 +930,7 @@ def test_code_review_scenario_handles_feature_diff_and_ad_hoc_range(tmp_path: Pa ) git(repo, "add", ".") git(repo, "commit", "-m", "add review") - assert code_review_next(repo, feature_slug=slug) == Action("downstream", "cs-feat QA") + assert code_review_next(repo, feature_slug=slug) == Action("downstream", "cs-feat acceptance-inline") @pytest.mark.parametrize("entry,expected", sorted(COMPATIBILITY_ENTRIES.items())) From 69d63ebe099a45da7fe5e6133e1eb1fe8c0aadec Mon Sep 17 00:00:00 2001 From: dafang Date: Tue, 14 Jul 2026 11:41:22 +0800 Subject: [PATCH 2/2] fix: align plugin docs with canonical repository --- README.en.md | 10 +++++----- tests/test_plugin_package.py | 4 ++-- tools/check-plugin-package.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.en.md b/README.en.md index f8d927e..ece2094 100644 --- a/README.en.md +++ b/README.en.md @@ -25,27 +25,27 @@ Tired of OpenSpec's flimsiness, Oh-My-OpenAgent's over-engineering, and Superpow Codex plugin marketplace: ```bash -codex plugin marketplace add liuzhengdongfortest/CodeStable +codex plugin marketplace add codestable/CodeStable codex plugin add codestable@codestable ``` Claude plugin marketplace: ```text -/plugin marketplace add liuzhengdongfortest/CodeStable +/plugin marketplace add codestable/CodeStable /plugin install codestable@codestable ``` `skills` CLI: ```bash -npx skills@latest add liuzhengdongfortest/CodeStable +npx skills@latest add codestable/CodeStable ``` If your `skills` CLI does not discover the plugin entity through the marketplace catalog, use the deep-scan fallback: ```bash -npx skills@latest add liuzhengdongfortest/CodeStable --full-depth +npx skills@latest add codestable/CodeStable --full-depth ``` The CodeStable plugin only packages `cs` / `cs-*` skills under `plugins/codestable/skills/`; the repository root no longer keeps standalone skill directories. @@ -78,7 +78,7 @@ Restart Claude Code after updating so the new plugin version is applied. npx skills@latest update ``` -If an older installer did not record the source, rerun the `npx skills@latest add liuzhengdongfortest/CodeStable` install command above. Upgrade the complete CodeStable plugin rather than replacing only the root `cs` skill; runtime refresh also requires the matching `cs-onboard` skill and its tools. After the global plugin upgrade, explicitly run `/cs-onboard --mode refresh-runtime` in every onboarded project to refresh and verify its repo-local runtime immediately. If you skip the command, the next CodeStable preflight compares `.codestable/runtime-manifest.json` with the current plugin version and refreshes automatically when the manifest is missing, the version or runtime capabilities do not match, and managed paths are clean; it does not scan repositories in the background. `managed-paths-dirty`, a repository that is not onboarded, or an incomplete skeleton stops the refresh instead of forcing an overwrite. +If an older installer did not record the source, rerun the `npx skills@latest add codestable/CodeStable` install command above. Upgrade the complete CodeStable plugin rather than replacing only the root `cs` skill; runtime refresh also requires the matching `cs-onboard` skill and its tools. After the global plugin upgrade, explicitly run `/cs-onboard --mode refresh-runtime` in every onboarded project to refresh and verify its repo-local runtime immediately. If you skip the command, the next CodeStable preflight compares `.codestable/runtime-manifest.json` with the current plugin version and refreshes automatically when the manifest is missing, the version or runtime capabilities do not match, and managed paths are clean; it does not scan repositories in the background. `managed-paths-dirty`, a repository that is not onboarded, or an incomplete skeleton stops the refresh instead of forcing an overwrite. One command to start working: diff --git a/tests/test_plugin_package.py b/tests/test_plugin_package.py index ce8dbad..96f8f1c 100644 --- a/tests/test_plugin_package.py +++ b/tests/test_plugin_package.py @@ -86,12 +86,12 @@ def make_repo(tmp_path: Path) -> Path: (repo / readme).write_text( "\n".join( [ - "codex plugin marketplace add liuzhengdongfortest/CodeStable", + "codex plugin marketplace add codestable/CodeStable", "codex plugin add codestable@codestable", "codex plugin marketplace upgrade codestable", "/plugin marketplace update", "/plugin update codestable@codestable", - "npx skills@latest add liuzhengdongfortest/CodeStable", + "npx skills@latest add codestable/CodeStable", "npx skills@latest update", "", ] diff --git a/tools/check-plugin-package.py b/tools/check-plugin-package.py index 8ff56d5..8a4f242 100644 --- a/tools/check-plugin-package.py +++ b/tools/check-plugin-package.py @@ -222,12 +222,12 @@ def check_generated_exclusions(root: Path, findings: list[Finding]) -> None: def check_readme_commands(root: Path, findings: list[Finding]) -> None: required = [ - "codex plugin marketplace add liuzhengdongfortest/CodeStable", + "codex plugin marketplace add codestable/CodeStable", "codex plugin add codestable@codestable", "codex plugin marketplace upgrade codestable", "/plugin marketplace update", "/plugin update codestable@codestable", - "npx skills@latest add liuzhengdongfortest/CodeStable", + "npx skills@latest add codestable/CodeStable", "npx skills@latest update", ] forbidden = ["codex plugin install codestable"]