dev-playbooks-cn 4.0.1 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,83 +3,165 @@
3
3
  [![npm](https://img.shields.io/npm/v/dev-playbooks-cn)](https://www.npmjs.com/package/dev-playbooks-cn)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
5
 
6
- ## v4.0 新特性
6
+ **让 AI 写代码从"说完成就完成"变成"有证据才算完成"。**
7
7
 
8
- - **完成合同(Completion Contract)**:把用户意图编译为机读合同,锁定"义务→检查→证据"链条,防止 AI 偷偷降低交付标准
9
- - **7 道闸门(G0-G6)**:从输入就绪到归档裁决,全链路可裁判检查点,任何一道失败都会阻断
10
- - **上游 SSOT 支持**:自动索引项目已有的需求文档,提取可裁判约束;没有需求文档时自动创建最小 SSOT 包
11
- - **Knife 切片协议**:大需求强制切片,每片有复杂度预算,超预算必须再切
12
- - **Void 研究协议**:高熵问题先研究再决策,产出可追溯的决策记录(ADR)
13
- - **证据新鲜度校验**:证据文件必须比被覆盖的交付物更新,防止用旧证据糊弄
14
- - **弱连接义务**:文档、配置、发布说明等"代码外契约"也被编译为可裁判义务
8
+ DevBooks 是一套面向 AI 的工程协议,通过上游真理源(SSOT)、可执行闸门、证据闭环,把 AI 编程从"对话式猜测"升级为"可审计的工程交付"。
15
9
 
16
10
  ---
17
11
 
18
- ## 你用 AI 写代码时,是不是经常遇到这些问题?
12
+ ## 核心理念
19
13
 
20
- **"说改好了,结果没改好"**
21
- AI 说"已修复",你问它确定吗,它说"确定"。上线后炸了。回头看,它写的测试恰好能通过它自己的 Bug。
14
+ 软件工程的本质是**在不可靠的组件之上构建可靠的系统**。传统工程用 RAID 对抗不可靠的硬盘,用 TCP 重传对抗不可靠的网络,用 Code Review 对抗不可靠的人类程序员。AI 工程同样需要用**闸门与证据闭环**约束不可靠的 LLM 输出。
22
15
 
23
- **"改着改着就忘了之前说的"**
24
- 对话开头你说"不要改 X 模块",三十轮对话后它把 X 模块改了。你提醒它,它道歉,然后下一轮又改了。
16
+ DevBooks 不是提示词优化,而是工程约束。
25
17
 
26
- **"需求大了就乱套"**
27
- 小功能还行,稍微复杂一点就开始胡说八道。改了 A 忘了 B,修了 B 又破坏了 C。
18
+ ---
19
+
20
+ ## 上游真理源(SSOT):全开发周期的单一权威
28
21
 
29
- **"不知道它到底做没做完"**
30
- 它说"完成了",但你不敢信。你让它再检查一遍,它说"检查过了,没问题"。你还是不敢信。
22
+ DevBooks 的核心是**上游真理源(Single Source of Truth)**——所有关键知识落盘并版本化,跨对话、跨变更稳定。
23
+
24
+ ```
25
+ 你的需求文档(如果有)
26
+ ↓ 提取约束,建立索引
27
+ specs/(术语、边界、决策、场景)← 跨变更稳定的"项目记忆"
28
+ ↓ 派生变更包
29
+ changes/<id>/(提案、设计、任务、证据)
30
+ ↓ 归档回写
31
+ specs/(更新真理)
32
+ ```
31
33
 
32
- **"每次都要从头教"**
33
- 上次对话里建立的约定,这次对话全忘了。项目的术语、边界、约束,每次都要重新解释。
34
+ **SSOT 解决的问题:**
34
35
 
35
- **"改完不知道改了什么"**
36
- 它改了一堆文件,你问它改了什么,它给你一个摘要。但这个摘要对不对?你不知道。
36
+ | 问题 | 根因 | SSOT 如何解决 |
37
+ |-----|------|--------------|
38
+ | 每次都要从头教 | 对话是临时的,知识没有持久化 | 术语、边界、约束写在文件里,不依赖对话记忆 |
39
+ | 改着改着就忘了之前说的 | 上下文窗口有限,早期信息被挤出去 | 真理工件持久化,强制注入关键约束 |
40
+ | 不知道改了什么 | 没有可审计的变更记录 | 每次变更有完整记录——提案、设计、任务、证据 |
37
41
 
38
42
  ---
39
43
 
40
- ## 这些不是 AI 不够聪明,是结构性问题
44
+ ## 账本与索引:持续追踪完成情况
45
+
46
+ DevBooks 通过**完成合同(Completion Contract)**和**需求索引(Requirements Index)**持续追踪交付状态。
41
47
 
42
- 提示词写得再好也没用。这是 LLM 的工作方式决定的:
48
+ ### 完成合同:把"我要什么"编译成机器可检查的清单
43
49
 
44
- | 问题 | 根本原因 |
45
- |-----|---------|
46
- | 说改好了没改好 | 自己写测试验证自己的代码,当然能过 |
47
- | 改着改着就忘了 | 上下文窗口有限,早期信息被挤出去 |
48
- | 需求大了就乱 | 复杂度超过单次对话能处理的极限 |
49
- | 不知道做没做完 | 没有客观证据,只有它的口头声明 |
50
- | 每次从头教 | 对话是临时的,知识没有持久化 |
51
- | 不知道改了什么 | 没有可审计的变更记录 |
50
+ ```yaml
51
+ obligations:
52
+ - id: O-001
53
+ describes: "用户可以通过邮箱登录"
54
+ severity: must
55
+ checks:
56
+ - id: C-001
57
+ type: test
58
+ covers: [O-001]
59
+ artifacts: ["evidence/gates/login-test.log"]
60
+ ```
61
+
62
+ 不是"大概做完了",而是"这 5 条义务都有证据"。
63
+
64
+ ### 需求索引:把上游文档变成可追溯的义务清单
65
+
66
+ ```yaml
67
+ set_id: ARCH-P3
68
+ source_ref: "truth://specs/architecture/design.md"
69
+ requirements:
70
+ - id: R-001
71
+ severity: must
72
+ statement: "所有 API 必须支持版本化"
73
+ - id: R-002
74
+ severity: should
75
+ statement: "响应时间 < 200ms"
76
+ ```
77
+
78
+ 当变更包宣称"已完成上游任务"时,系统可以裁判这个宣称——不是口头确认,而是机器校验。
52
79
 
53
80
  ---
54
81
 
55
- ## DevBooks:用工程约束解决这些问题
82
+ ## Knife 切片协议:把大需求变成可执行队列
83
+
84
+ 大需求直接交给 AI 会怎样?改了 A 忘了 B,修了 B 又破坏了 C。
85
+
86
+ Knife 协议通过**复杂度预算**和**拓扑排序**,把 Epic 切成可独立验证的原子变更包队列。
87
+
88
+ ### 切片算法
89
+
90
+ ```
91
+ Score = w₁·Files + w₂·Modules + w₃·RiskFlags + w₄·HotspotWeight
92
+ ```
93
+
94
+ | 信号 | 权重 | 说明 |
95
+ |-----|------|-----|
96
+ | files_touched | 1.0 | 每个文件计 1 分 |
97
+ | modules_touched | 5.0 | 跨模块风险高 |
98
+ | risk_flags | 10.0 | 每个风险旗标计 10 分 |
99
+ | hotspot_weight | 2.0 | 高 churn 区域加权 |
100
+
101
+ **超预算必须再切**——禁止"硬做"。
102
+
103
+ ### 切片不变量
104
+
105
+ 1. **MECE 覆盖**:所有切片的验收点并集等于 Epic 的完整验收点集合,且不重叠
106
+ 2. **可独立 Green**:每个切片至少一个确定性验证锚点,不允许"中间态不可编译"
107
+ 3. **拓扑可排序**:依赖图必须无环,执行顺序必须为拓扑序
108
+ 4. **预算熔断**:超预算必须递归切分,或回流补信息
109
+
110
+ ### 并行执行调度
111
+
112
+ 当 Knife Plan 包含多个 Slice 时,可以生成并行执行清单:
56
113
 
57
114
  ```bash
58
- npm install -g dev-playbooks-cn
59
- dev-playbooks-cn init
60
- dev-playbooks-cn delivery
115
+ knife-parallel-schedule.sh <epic-id> --format md --out parallel-schedule.md
61
116
  ```
62
117
 
63
- | 问题 | DevBooks 怎么解决 |
64
- |-----|------------------|
65
- | 自己验证自己 | **角色隔离**:写测试的 Agent 和写代码的 Agent 必须分开,互相看不到对方的思路 |
66
- | 上下文遗忘 | **真理落盘**:术语、边界、约束写在文件里,不依赖对话记忆 |
67
- | 需求太大 | **切片预算**:大需求必须切成小块,每块有复杂度上限,超了就再切 |
68
- | 口头完成 | **证据定义完成**:测试日志、构建输出必须真的存在于磁盘上 |
69
- | 每次从头教 | **SSOT(单一真理源)**:项目知识持久化在 specs/,跨对话、跨变更稳定 |
70
- | 不知道改了什么 | **变更包**:每次变更有完整记录——提案、设计、任务、证据 |
118
+ 输出内容:
119
+ - **最大并行度**:可同时启动的最大 Agent 数量
120
+ - **分层执行清单**:Layer 0(无依赖)→ Layer 1 Layer N
121
+ - **关键路径**:串行依赖深度
122
+ - **启动命令模板**:每个 Slice Agent 启动命令
123
+
124
+ 由于当前 AI 编程工具不支持二级子代理调用,Epic 拆分后需要人类协调多个独立 Agent 并行完成。
125
+
126
+ ---
127
+
128
+ ## 7 道闸门:全链路可裁判检查点
129
+
130
+ | 闸门 | 检查什么 | 失败后果 |
131
+ |-----|---------|---------|
132
+ | G0 | 输入就绪了吗?基线工件齐全吗? | 回流到 Bootstrap |
133
+ | G1 | 该有的文件都有吗?结构正确吗? | 阻断 |
134
+ | G2 | 任务都完成了吗?绿证据存在吗? | 阻断 |
135
+ | G3 | 切片正确吗?锚点齐全吗?(大需求) | 回流到 Knife |
136
+ | G4 | 文档同步了吗?扩展包完整吗? | 阻断 |
137
+ | G5 | 风险覆盖了吗?回滚策略有吗?(高风险) | 阻断 |
138
+ | G6 | 证据完整吗?合同满足吗?可以归档吗? | 阻断 |
139
+
140
+ 任何一道失败,流程阻断。不是警告,是阻断。
71
141
 
72
142
  ---
73
143
 
74
- ## 它是怎么工作的
144
+ ## 角色隔离:防止 AI 自己验证自己
145
+
146
+ | 角色 | 职责 | 硬约束 |
147
+ |-----|------|-------|
148
+ | Test Owner | 从设计推导验收测试 | 禁止看实现代码 |
149
+ | Coder | 按任务实现功能 | 禁止修改 tests/ |
150
+ | Reviewer | 审查可读性与一致性 | 不改测试不改设计 |
151
+
152
+ Test Owner 和 Coder 必须在**不同上下文**执行——不是"不同人",而是"不同对话/不同实例"。两者之间只能通过**落盘工件**交接。
153
+
154
+ ---
75
155
 
76
- 你只需要记住一个命令:
156
+ ## 快速开始
77
157
 
78
158
  ```bash
159
+ npm install -g dev-playbooks-cn
160
+ dev-playbooks-cn init
79
161
  dev-playbooks-cn delivery
80
162
  ```
81
163
 
82
- 系统会问你几个问题,然后生成一份 `RUNBOOK.md`——这是你这次任务的操作手册。照着做就行。
164
+ 你只需要记住一个命令:`delivery`。系统会问你几个问题,然后生成一份 `RUNBOOK.md`——这是你这次任务的操作手册。照着做就行。
83
165
 
84
166
  ```
85
167
  你的需求
@@ -99,66 +181,41 @@ Delivery(判断类型、生成 RUNBOOK)
99
181
 
100
182
  ---
101
183
 
102
- ## 核心机制
103
-
104
- **真理源(SSOT)**
105
-
106
- ```
107
- 你的需求文档(如果有)
108
- ↓ 提取约束
109
- specs/(术语、边界、决策)← 跨变更稳定的"项目记忆"
110
-
111
- changes/<id>/(本次变更的提案、设计、任务、证据)
112
- ↓ 归档
113
- specs/(更新真理)
114
- ```
115
-
116
- 如果你没有需求文档,DevBooks 会帮你创建一个最小的。这反而是好事——逼你把模糊的想法写清楚。
117
-
118
- **完成合同**
119
-
120
- 把"我要什么"编译成机器可检查的清单:
121
- - 5 条义务
122
- - 每条有对应的检查项
123
- - 每条有对应的证据文件
124
-
125
- 不是"大概做完了",而是"这 5 条都有证据"。
126
-
127
- **7 道闸门(G0-G6)**
128
-
129
- | 闸门 | 检查什么 |
130
- |-----|---------|
131
- | G0 | 输入就绪了吗?需求清楚吗? |
132
- | G1 | 该有的文件都有吗? |
133
- | G2 | 任务都完成了吗? |
134
- | G3 | 切片正确吗?(大需求) |
135
- | G4 | 文档同步了吗? |
136
- | G5 | 风险覆盖了吗?(高风险变更) |
137
- | G6 | 证据完整吗?可以归档吗? |
138
-
139
- 任何一道失败,流程阻断。不是警告,是阻断。
140
-
141
- ---
142
-
143
184
  ## 目录结构
144
185
 
145
186
  ```
146
187
  project/
147
- ├── .devbooks/config.yaml # 配置
188
+ ├── .devbooks/config.yaml # 配置入口
148
189
  └── dev-playbooks/
149
- ├── constitution.md # 硬约束(不可绕过的规则)
150
- ├── specs/ # 真理源(跨变更稳定)
151
- └── changes/ # 变更包(每次变更一个目录)
190
+ ├── constitution.md # 硬约束(不可绕过的规则)
191
+ ├── specs/ # 真理源(SSOT)
192
+ │ ├── _meta/
193
+ │ │ ├── glossary.md # 统一语言
194
+ │ │ ├── boundaries.md # 模块边界
195
+ │ │ ├── capabilities.yaml # 能力注册表
196
+ │ │ └── epics/ # Knife 切片计划
197
+ │ └── ...
198
+ └── changes/ # 变更包
152
199
  └── <change-id>/
153
- ├── proposal.md # 为什么做、做什么
154
- ├── design.md # 怎么做、验收标准
155
- ├── tasks.md # 拆成可执行的步骤
156
- ├── verification.md # 怎么证明做对了
157
- └── evidence/ # 测试日志、构建输出
200
+ ├── proposal.md # 为什么做、做什么
201
+ ├── design.md # 怎么做、验收标准
202
+ ├── tasks.md # 拆成可执行的步骤
203
+ ├── completion.contract.yaml # 完成合同
204
+ ├── verification.md # 怎么证明做对了
205
+ └── evidence/ # 测试日志、构建输出
158
206
  ```
159
207
 
160
208
  ---
161
209
 
210
+ ## 适用场景
211
+
212
+ - **存量项目接入**:自动索引现有文档,提取可裁判约束,建立最小 SSOT 包
213
+ - **新项目启动**:引导补齐术语、边界、场景、决策,建立基线
214
+ - **日常变更**:最小充分闭环,可复现验证锚点 + 证据归档
215
+ - **大型重构**:Knife 切片 + 迁移范式(Expand-Contract / Strangler Fig / Branch by Abstraction)
216
+
217
+ ---
218
+
162
219
  ## 下一步
163
220
 
164
221
  - [快速开始](docs/使用指南.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev-playbooks-cn",
3
- "version": "4.0.1",
3
+ "version": "4.0.3",
4
4
  "description": "AI-driven spec-based development workflow",
5
5
  "keywords": [
6
6
  "devbooks",
@@ -115,6 +115,30 @@ Skills 引用的共享资源(如 `_shared/references/`)位于 skills 全局
115
115
  最后给出下一步最短闭环路由 + 升级条件。
116
116
  ```
117
117
 
118
+ ### 并行执行调度(多 Agent 并行)
119
+
120
+ 当 Knife Plan 包含多个 Slice 时,可以生成并行执行清单,让人类协调多个独立 Agent 并行完成:
121
+
122
+ ```bash
123
+ # 生成并行调度清单
124
+ knife-parallel-schedule.sh <epic-id> --format md --out parallel-schedule.md
125
+ ```
126
+
127
+ **输出内容**:
128
+ 1. **最大并行度**:可同时启动的最大 Agent 数量
129
+ 2. **分层执行清单**:Layer 0(无依赖)→ Layer 1(依赖 Layer 0)→ ...
130
+ 3. **关键路径**:串行依赖深度
131
+ 4. **启动命令模板**:每个 Slice 的 Agent 启动命令
132
+ 5. **溯源信息**:Epic ID、Plan ID、Plan Revision
133
+
134
+ **使用场景**:
135
+ 由于当前 AI 编程工具不支持二级子代理调用,Epic 拆分后需要人类协调多个独立 Agent 并行完成:
136
+ 1. 运行 `knife-parallel-schedule.sh` 生成清单
137
+ 2. 根据清单的 Layer 0 启动多个独立 Agent
138
+ 3. 等待 Layer 0 全部完成后,启动 Layer 1
139
+ 4. 重复直到所有 Layer 完成
140
+ 5. 运行 `requirements-ledger-derive.sh` 更新账本
141
+
118
142
  ---
119
143
 
120
144
  ## `devbooks-proposal-author`(Proposal Author)
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # =============================================================================
5
+ # knife-parallel-schedule.sh
6
+ # =============================================================================
7
+ # 从 Knife Plan 生成并行执行调度清单
8
+ #
9
+ # 功能:
10
+ # 1. 解析 Knife Plan 的 slices[] 依赖图
11
+ # 2. 计算最大并行度(DAG 宽度)
12
+ # 3. 生成分层执行清单(Layer 0, 1, 2, ...)
13
+ # 4. 识别关键路径
14
+ # 5. 输出人类可读的并行执行指南
15
+ #
16
+ # 用途:
17
+ # Epic 拆分后,用户可以根据此清单开启多个独立 Agent 并行完成变更包
18
+ # =============================================================================
19
+
20
+ usage() {
21
+ cat <<'EOF' >&2
22
+ usage: knife-parallel-schedule.sh <epic-id> [options]
23
+
24
+ 从 Knife Plan 生成并行执行调度清单。
25
+
26
+ Options:
27
+ --project-root <dir> 项目根目录 (default: pwd)
28
+ --truth-root <dir> 真理根目录 (default: specs)
29
+ --out <path> 输出文件路径 (default: stdout)
30
+ --format <md|json> 输出格式 (default: md)
31
+ -h, --help 显示帮助
32
+
33
+ 输出内容:
34
+ - 最大并行度
35
+ - 分层执行清单(哪些 Slice 可以同时开始)
36
+ - 关键路径
37
+ - 每个 Slice 的启动命令模板
38
+ - 溯源信息
39
+
40
+ Exit codes:
41
+ 0 - 成功
42
+ 1 - Knife Plan 不存在或解析失败
43
+ 2 - 用法错误
44
+ EOF
45
+ }
46
+
47
+ errorf() {
48
+ printf 'ERROR: %s\n' "$*" >&2
49
+ }
50
+
51
+ infof() {
52
+ printf 'INFO: %s\n' "$*" >&2
53
+ }
54
+
55
+ # =============================================================================
56
+ # 参数解析
57
+ # =============================================================================
58
+
59
+ if [[ $# -eq 0 ]]; then
60
+ usage
61
+ exit 2
62
+ fi
63
+
64
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
65
+ usage
66
+ exit 0
67
+ fi
68
+
69
+ epic_id="$1"
70
+ shift
71
+
72
+ project_root="${DEVBOOKS_PROJECT_ROOT:-$(pwd)}"
73
+ truth_root="${DEVBOOKS_TRUTH_ROOT:-specs}"
74
+ out_path=""
75
+ format="md"
76
+
77
+ while [[ $# -gt 0 ]]; do
78
+ case "$1" in
79
+ -h|--help)
80
+ usage
81
+ exit 0
82
+ ;;
83
+ --project-root)
84
+ project_root="${2:-}"
85
+ shift 2
86
+ ;;
87
+ --truth-root)
88
+ truth_root="${2:-}"
89
+ shift 2
90
+ ;;
91
+ --out)
92
+ out_path="${2:-}"
93
+ shift 2
94
+ ;;
95
+ --format)
96
+ format="${2:-}"
97
+ shift 2
98
+ ;;
99
+ *)
100
+ errorf "unknown option: $1"
101
+ usage
102
+ exit 2
103
+ ;;
104
+ esac
105
+ done
106
+
107
+ if [[ -z "$epic_id" || "$epic_id" == "-"* ]]; then
108
+ errorf "invalid epic-id: '$epic_id'"
109
+ exit 2
110
+ fi
111
+
112
+ case "$format" in
113
+ md|json) ;;
114
+ *)
115
+ errorf "invalid --format: $format (must be md or json)"
116
+ exit 2
117
+ ;;
118
+ esac
119
+
120
+ project_root="${project_root%/}"
121
+ truth_root="${truth_root%/}"
122
+
123
+ if [[ "$truth_root" = /* ]]; then
124
+ truth_dir="$truth_root"
125
+ else
126
+ truth_dir="${project_root}/${truth_root}"
127
+ fi
128
+
129
+ # =============================================================================
130
+ # 查找 Knife Plan
131
+ # =============================================================================
132
+
133
+ knife_plan_dir="${truth_dir}/_meta/epics/${epic_id}"
134
+ knife_plan_file=""
135
+
136
+ if [[ -f "${knife_plan_dir}/knife-plan.yaml" ]]; then
137
+ knife_plan_file="${knife_plan_dir}/knife-plan.yaml"
138
+ elif [[ -f "${knife_plan_dir}/knife-plan.json" ]]; then
139
+ knife_plan_file="${knife_plan_dir}/knife-plan.json"
140
+ else
141
+ errorf "Knife Plan not found at: ${knife_plan_dir}/knife-plan.(yaml|json)"
142
+ exit 1
143
+ fi
144
+
145
+ infof "Found Knife Plan: $knife_plan_file"
146
+
147
+ # =============================================================================
148
+ # 解析 Knife Plan(使用 yq 或 jq)
149
+ # =============================================================================
150
+
151
+ # 检查工具可用性
152
+ if command -v yq &>/dev/null; then
153
+ YAML_TOOL="yq"
154
+ elif command -v python3 &>/dev/null; then
155
+ YAML_TOOL="python"
156
+ else
157
+ errorf "需要 yq 或 python3 来解析 YAML"
158
+ exit 1
159
+ fi
160
+
161
+ # 提取 slices 数据
162
+ extract_slices() {
163
+ local file="$1"
164
+
165
+ if [[ "$file" == *.json ]]; then
166
+ jq -r '.slices // []' "$file"
167
+ elif [[ "$YAML_TOOL" == "yq" ]]; then
168
+ yq -o=json '.slices // []' "$file"
169
+ else
170
+ python3 -c "
171
+ import yaml
172
+ import json
173
+ import sys
174
+
175
+ with open('$file', 'r') as f:
176
+ data = yaml.safe_load(f)
177
+ slices = data.get('slices', [])
178
+ print(json.dumps(slices))
179
+ "
180
+ fi
181
+ }
182
+
183
+ # 提取元数据
184
+ extract_metadata() {
185
+ local file="$1"
186
+
187
+ if [[ "$file" == *.json ]]; then
188
+ jq -r '{epic_id, plan_id, plan_revision, risk_level, change_type, ac_ids}' "$file"
189
+ elif [[ "$YAML_TOOL" == "yq" ]]; then
190
+ yq -o=json '{epic_id: .epic_id, plan_id: .plan_id, plan_revision: .plan_revision, risk_level: .risk_level, change_type: .change_type, ac_ids: .ac_ids}' "$file"
191
+ else
192
+ python3 -c "
193
+ import yaml
194
+ import json
195
+ import sys
196
+
197
+ with open('$file', 'r') as f:
198
+ data = yaml.safe_load(f)
199
+ meta = {
200
+ 'epic_id': data.get('epic_id'),
201
+ 'plan_id': data.get('plan_id'),
202
+ 'plan_revision': data.get('plan_revision'),
203
+ 'risk_level': data.get('risk_level'),
204
+ 'change_type': data.get('change_type'),
205
+ 'ac_ids': data.get('ac_ids', [])
206
+ }
207
+ print(json.dumps(meta))
208
+ "
209
+ fi
210
+ }
211
+
212
+ slices_json=$(extract_slices "$knife_plan_file")
213
+ metadata_json=$(extract_metadata "$knife_plan_file")
214
+
215
+ # 验证 slices 不为空
216
+ slice_count=$(echo "$slices_json" | jq 'length')
217
+ if [[ "$slice_count" -eq 0 ]]; then
218
+ errorf "Knife Plan 中没有定义 slices"
219
+ exit 1
220
+ fi
221
+
222
+ infof "Found $slice_count slices"
223
+
224
+ # =============================================================================
225
+ # 拓扑排序与分层计算
226
+ # =============================================================================
227
+
228
+ # 使用 jq 进行拓扑排序和分层计算
229
+ schedule_json=$(echo "$slices_json" | jq '
230
+ # 构建 slice_id -> index 映射
231
+ . as $slices |
232
+ reduce range(length) as $i ({}; . + {($slices[$i].slice_id): $i}) as $id_to_idx |
233
+
234
+ # 计算每个节点的入度
235
+ reduce .[] as $slice (
236
+ (reduce .[] as $s ({}; . + {($s.slice_id): 0}));
237
+ reduce ($slice.depends_on // [])[] as $dep (.; .[$slice.slice_id] = (.[$slice.slice_id] // 0) + 1)
238
+ ) as $in_degree |
239
+
240
+ # Kahn 算法进行拓扑排序并分层
241
+ {
242
+ layers: [],
243
+ remaining: [.[] | .slice_id],
244
+ in_degree: $in_degree,
245
+ slices: $slices
246
+ } |
247
+ until((.remaining | length) == 0;
248
+ # 找出当前入度为 0 的节点
249
+ .remaining as $rem |
250
+ .in_degree as $deg |
251
+ [$rem[] | select($deg[.] == 0)] as $current_layer |
252
+
253
+ if ($current_layer | length) == 0 then
254
+ # 有环,无法继续
255
+ .remaining = []
256
+ else
257
+ # 更新入度
258
+ reduce ($slices[] | select([.slice_id] | inside($current_layer) | not)) as $s (
259
+ .in_degree;
260
+ reduce ($s.depends_on // [])[] as $dep (
261
+ .;
262
+ if ($current_layer | index($dep)) then
263
+ .[$s.slice_id] = (.[$s.slice_id] - 1)
264
+ else . end
265
+ )
266
+ ) as $new_deg |
267
+
268
+ .layers += [$current_layer] |
269
+ .remaining = [.remaining[] | select(. as $id | $current_layer | index($id) | not)] |
270
+ .in_degree = $new_deg
271
+ end
272
+ ) |
273
+
274
+ # 计算关键路径(最长路径)
275
+ .layers as $layers |
276
+ ($layers | length) as $depth |
277
+
278
+ # 构建 slice 详情
279
+ {
280
+ max_parallelism: ($layers | map(length) | max),
281
+ total_layers: ($layers | length),
282
+ layers: [range($layers | length) as $i | {
283
+ layer: $i,
284
+ can_start_immediately: ($i == 0),
285
+ depends_on_layer: (if $i > 0 then $i - 1 else null end),
286
+ slices: $layers[$i]
287
+ }],
288
+ critical_path_length: ($layers | length),
289
+ slices_detail: [.slices[] | {
290
+ slice_id: .slice_id,
291
+ change_id: .change_id,
292
+ ac_subset: .ac_subset,
293
+ depends_on: (.depends_on // []),
294
+ budgets: .budgets,
295
+ verification_anchors: .verification_anchors
296
+ }]
297
+ }
298
+ ')
299
+
300
+ # =============================================================================
301
+ # 输出生成
302
+ # =============================================================================
303
+
304
+ generate_markdown() {
305
+ local meta="$1"
306
+ local schedule="$2"
307
+ local knife_file="$3"
308
+
309
+ local epic_id plan_id plan_revision risk_level change_type
310
+ epic_id=$(echo "$meta" | jq -r '.epic_id // "N/A"')
311
+ plan_id=$(echo "$meta" | jq -r '.plan_id // "N/A"')
312
+ plan_revision=$(echo "$meta" | jq -r '.plan_revision // "N/A"')
313
+ risk_level=$(echo "$meta" | jq -r '.risk_level // "N/A"')
314
+ change_type=$(echo "$meta" | jq -r '.change_type // "N/A"')
315
+
316
+ local max_parallelism total_layers
317
+ max_parallelism=$(echo "$schedule" | jq -r '.max_parallelism')
318
+ total_layers=$(echo "$schedule" | jq -r '.total_layers')
319
+
320
+ cat <<EOF
321
+ # 并行执行调度清单
322
+
323
+ > 由 \`knife-parallel-schedule.sh\` 自动生成
324
+ > 生成时间: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
325
+
326
+ ## 溯源信息
327
+
328
+ | 字段 | 值 |
329
+ |------|-----|
330
+ | Epic ID | \`$epic_id\` |
331
+ | Plan ID | \`$plan_id\` |
332
+ | Plan Revision | \`$plan_revision\` |
333
+ | Risk Level | \`$risk_level\` |
334
+ | Change Type | \`$change_type\` |
335
+ | Knife Plan 路径 | \`$knife_file\` |
336
+
337
+ ## 并行度摘要
338
+
339
+ - **最大并行度**: $max_parallelism(可同时启动的最大 Agent 数量)
340
+ - **总层数**: $total_layers(串行依赖深度)
341
+ - **关键路径长度**: $total_layers 层
342
+
343
+ ## 执行层级
344
+
345
+ EOF
346
+
347
+ # 输出每一层
348
+ echo "$schedule" | jq -r '.layers[] | "### Layer \(.layer)\(if .can_start_immediately then " (可立即开始)" else " (依赖 Layer \(.depends_on_layer))" end)\n\n| Slice ID | Change ID |\n|----------|-----------|" + (.slices | map("\n| `\(.)` | - |") | join(""))'
349
+
350
+ cat <<EOF
351
+
352
+ ## Slice 详情
353
+
354
+ EOF
355
+
356
+ # 输出每个 slice 的详情
357
+ echo "$schedule" | jq -r '.slices_detail[] | "### \(.slice_id)\n\n- **Change ID**: `\(.change_id // "待分配")`\n- **依赖**: \(if (.depends_on | length) == 0 then "无(可独立执行)" else (.depends_on | map("`\(.)`") | join(", ")) end)\n- **AC 子集**: \(.ac_subset | map("`\(.)`") | join(", "))\n- **Token 预算**: \(.budgets.tokens // "未指定")\n- **验证锚点**: \(if (.verification_anchors | length) == 0 then "无" else (.verification_anchors | map("`\(.)`") | join(", ")) end)\n"'
358
+
359
+ cat <<EOF
360
+
361
+ ## 启动命令模板
362
+
363
+ 每个 Slice 对应一个独立的变更包,可以在独立的 Agent 会话中执行:
364
+
365
+ \`\`\`bash
366
+ # Layer 0 的 Slice 可以立即并行启动
367
+ EOF
368
+
369
+ echo "$schedule" | jq -r '.layers[0].slices[] as $sid | .slices_detail[] | select(.slice_id == $sid) | "# Agent for \(.slice_id)\ndevbooks apply --change-id \(.change_id // "<待分配>") --epic-id '"$epic_id"' --slice-id \(.slice_id)"'
370
+
371
+ cat <<EOF
372
+ \`\`\`
373
+
374
+ ## 执行建议
375
+
376
+ 1. **并行启动**: 同一 Layer 内的所有 Slice 可以同时启动独立的 Agent
377
+ 2. **依赖等待**: 下一 Layer 的 Slice 必须等待上一 Layer 全部完成
378
+ 3. **溯源验证**: 每个变更包完成后,使用 \`devbooks archive\` 归档并回写账本
379
+ 4. **进度追踪**: 使用 \`progress-dashboard.sh\` 查看整体进度
380
+
381
+ ## 完成后回写
382
+
383
+ 所有 Slice 完成后,执行以下命令更新账本:
384
+
385
+ \`\`\`bash
386
+ # 派生需求账本
387
+ requirements-ledger-derive.sh --project-root .
388
+
389
+ # 验证 Epic 完整性
390
+ epic-alignment-check.sh $epic_id --mode strict
391
+ \`\`\`
392
+
393
+ ---
394
+
395
+ *此清单由 DevBooks Knife Parallel Schedule 生成*
396
+ *参考: dev-playbooks/specs/knife/spec.md*
397
+ EOF
398
+ }
399
+
400
+ generate_json() {
401
+ local meta="$1"
402
+ local schedule="$2"
403
+ local knife_file="$3"
404
+
405
+ jq -n \
406
+ --argjson meta "$meta" \
407
+ --argjson schedule "$schedule" \
408
+ --arg knife_file "$knife_file" \
409
+ --arg generated_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
410
+ '{
411
+ schema_version: "1.0.0",
412
+ generated_at: $generated_at,
413
+ source: {
414
+ knife_plan_path: $knife_file,
415
+ epic_id: $meta.epic_id,
416
+ plan_id: $meta.plan_id,
417
+ plan_revision: $meta.plan_revision,
418
+ risk_level: $meta.risk_level,
419
+ change_type: $meta.change_type
420
+ },
421
+ summary: {
422
+ max_parallelism: $schedule.max_parallelism,
423
+ total_layers: $schedule.total_layers,
424
+ critical_path_length: $schedule.critical_path_length,
425
+ total_slices: ($schedule.slices_detail | length)
426
+ },
427
+ layers: $schedule.layers,
428
+ slices: $schedule.slices_detail
429
+ }'
430
+ }
431
+
432
+ # =============================================================================
433
+ # 输出
434
+ # =============================================================================
435
+
436
+ output=""
437
+ if [[ "$format" == "md" ]]; then
438
+ output=$(generate_markdown "$metadata_json" "$schedule_json" "$knife_plan_file")
439
+ else
440
+ output=$(generate_json "$metadata_json" "$schedule_json" "$knife_plan_file")
441
+ fi
442
+
443
+ if [[ -n "$out_path" ]]; then
444
+ if [[ "$out_path" = /* ]]; then
445
+ out_file="$out_path"
446
+ else
447
+ out_file="${project_root}/${out_path}"
448
+ fi
449
+ mkdir -p "$(dirname "$out_file")"
450
+ echo "$output" > "$out_file"
451
+ infof "Output written to: $out_file"
452
+ else
453
+ echo "$output"
454
+ fi
455
+
456
+ infof "Parallel schedule generated successfully"
457
+ infof "Max parallelism: $(echo "$schedule_json" | jq -r '.max_parallelism')"
458
+ infof "Total layers: $(echo "$schedule_json" | jq -r '.total_layers')"
@@ -67,7 +67,41 @@ allowed-tools:
67
67
  4. 落盘 Knife Plan 到规定路径,并在内容中显式绑定 `epic_id` / `slice_id`。
68
68
  5. 输出下一步路由建议:进入 `devbooks-delivery-workflow`(或先进入 Proposal/Design/Spec/Plan),并给出升级条件。
69
69
 
70
+ ## 并行执行调度
71
+
72
+ 当 Knife Plan 包含多个 Slice 时,可以使用 `knife-parallel-schedule.sh` 生成并行执行清单:
73
+
74
+ ```bash
75
+ # 生成 Markdown 格式的并行调度清单
76
+ knife-parallel-schedule.sh <epic-id> --format md --out parallel-schedule.md
77
+
78
+ # 生成 JSON 格式(供程序消费)
79
+ knife-parallel-schedule.sh <epic-id> --format json --out parallel-schedule.json
80
+ ```
81
+
82
+ ### 输出内容
83
+
84
+ 1. **最大并行度**:可同时启动的最大 Agent 数量
85
+ 2. **分层执行清单**:
86
+ - Layer 0:无依赖,可立即启动
87
+ - Layer 1:依赖 Layer 0 完成
88
+ - Layer N:依赖 Layer N-1 完成
89
+ 3. **关键路径**:串行依赖深度
90
+ 4. **启动命令模板**:每个 Slice 的 Agent 启动命令
91
+ 5. **溯源信息**:Epic ID、Plan ID、Plan Revision
92
+
93
+ ### 使用场景
94
+
95
+ 由于当前 AI 编程工具不支持二级子代理调用,Epic 拆分后需要人类协调多个独立 Agent 并行完成:
96
+
97
+ 1. 运行 `knife-parallel-schedule.sh` 生成清单
98
+ 2. 根据清单的 Layer 0 启动多个独立 Agent
99
+ 3. 等待 Layer 0 全部完成后,启动 Layer 1
100
+ 4. 重复直到所有 Layer 完成
101
+ 5. 运行 `requirements-ledger-derive.sh` 更新账本
102
+
70
103
  ## 参考
71
104
 
72
105
  - `dev-playbooks/specs/knife/spec.md`(Knife 的规范与闸门接线要求)
73
106
  - `dev-playbooks/specs/_meta/epics/README.md`(Epic 工件目录约束)
107
+ - `skills/devbooks-delivery-workflow/scripts/knife-parallel-schedule.sh`(并行调度脚本)