dev-playbooks-cn 4.0.2 → 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
|
@@ -107,6 +107,22 @@ Score = w₁·Files + w₂·Modules + w₃·RiskFlags + w₄·HotspotWeight
|
|
|
107
107
|
3. **拓扑可排序**:依赖图必须无环,执行顺序必须为拓扑序
|
|
108
108
|
4. **预算熔断**:超预算必须递归切分,或回流补信息
|
|
109
109
|
|
|
110
|
+
### 并行执行调度
|
|
111
|
+
|
|
112
|
+
当 Knife Plan 包含多个 Slice 时,可以生成并行执行清单:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
knife-parallel-schedule.sh <epic-id> --format md --out parallel-schedule.md
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
输出内容:
|
|
119
|
+
- **最大并行度**:可同时启动的最大 Agent 数量
|
|
120
|
+
- **分层执行清单**:Layer 0(无依赖)→ Layer 1 → Layer N
|
|
121
|
+
- **关键路径**:串行依赖深度
|
|
122
|
+
- **启动命令模板**:每个 Slice 的 Agent 启动命令
|
|
123
|
+
|
|
124
|
+
由于当前 AI 编程工具不支持二级子代理调用,Epic 拆分后需要人类协调多个独立 Agent 并行完成。
|
|
125
|
+
|
|
110
126
|
---
|
|
111
127
|
|
|
112
128
|
## 7 道闸门:全链路可裁判检查点
|
package/package.json
CHANGED
|
@@ -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`(并行调度脚本)
|