sillyspec 3.19.2 → 3.20.0

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.
@@ -1,6 +1,16 @@
1
- import { existsSync, readFileSync } from 'fs'
1
+ import { existsSync, readFileSync, readdirSync } from 'fs'
2
2
  import path from 'path'
3
3
 
4
+ // 从 plan-postcheck.js 重导出(保持向后兼容)
5
+ export {
6
+ topoSortWaves,
7
+ validateBlueprintConsistency,
8
+ validatePlanArtifacts,
9
+ validatePlanFeasibility
10
+ } from './plan-postcheck.js'
11
+
12
+ // 这些解析函数已迁移到 plan-postcheck.js,此处不再定义
13
+
4
14
  /**
5
15
  * 校验 design.md 是否满足 plan 执行契约
6
16
  * 第一版是轻量 markdown 结构检查,不强 schema。
@@ -66,15 +76,28 @@ export const definition = {
66
76
  steps: null // 动态生成
67
77
  }
68
78
 
69
- // 固定前缀步骤
70
- export const fixedPrefix = [
71
- {
72
- name: '复杂度分类',
73
- prompt: `在生成计划之前,先判定本次需求的复杂度等级(plan_level)。
79
+ // ═══════════════════════════════════════════════════════════════
80
+ // 1 步(LLM):复杂度分类 + 上下文加载(合并原 ①②③④)
81
+ // ═══════════════════════════════════════════════════════════════
82
+
83
+ const stepClassify = {
84
+ id: 'classify',
85
+ name: '复杂度分类与上下文加载',
86
+ prompt: `在生成计划之前,先加载上下文并判定本次需求的复杂度等级(plan_level)。
74
87
 
75
88
  ### 操作
76
- 1. 读取 tasks.md design.md,了解需求范围
77
- 2. 按「分级规则」判定 plan_level
89
+ 1. 运行 \`sillyspec progress show\`,确认 currentStage 为 "plan"
90
+ 2. 读取 CODEBASE-OVERVIEW.md + 各子项目上下文
91
+ 3. 读取 proposal.md、design.md、requirements.md、tasks.md
92
+ 4. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
93
+ - 如果发现 priority=P0/P1 且 status=unresolved/blocking 的决策,停止生成计划,要求先回到 brainstorm 的 Design Grill 修正
94
+ - 如果发现 superseded 决策,只引用最新版本,不引用旧版本
95
+ 5. 读取 CONVENTIONS.md、ARCHITECTURE.md、STACK.md
96
+ 6. 读取 local.yaml 获取构建/测试命令
97
+ 7. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过)
98
+ - 根据 design.md 的文件变更清单匹配模块
99
+ - 读取匹配到的模块文档
100
+ - 利用模块依赖关系辅助分析(depends_on / used_by)
78
101
 
79
102
  ### 分级规则
80
103
  判定 plan_level 为 none 时,需**同时满足**以下所有条件:
@@ -118,72 +141,27 @@ needs_parallel_execution: true | false
118
141
  needs_human_review: true | false
119
142
  \`\`\`
120
143
 
121
- 分类完成后,继续进入下一步。`,
122
- outputHint: '复杂度分类结果',
123
- optional: false
124
- },
125
- {
126
- name: '状态检查',
127
- prompt: `检查当前状态,确认可以执行 plan。
128
-
129
- ### 操作
130
- 1. 运行 \`sillyspec progress show\`
131
- 2. 确认 currentStage 为 "plan"
132
-
133
- ### 输出
134
- 当前状态摘要`,
135
- outputHint: '状态摘要',
136
- optional: false
137
- },
138
- {
139
- name: '加载上下文',
140
- prompt: `加载所有规范文件和代码库上下文。
141
-
142
- ### 操作
143
- 1. 读取 CODEBASE-OVERVIEW.md + 各子项目上下文
144
- 2. 读取 proposal.md、design.md、requirements.md、tasks.md
145
- 3. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
146
- - 如果发现 priority=P0/P1 且 status=unresolved/blocking 的决策,停止生成计划,要求先回到 brainstorm 的 Design Grill 修正
147
- - 如果发现 superseded 决策,只引用最新版本,不引用旧版本
148
- 4. 读取 CONVENTIONS.md、ARCHITECTURE.md、STACK.md
149
- 5. 读取 local.yaml 获取构建/测试命令
150
-
151
- ### 模块文档加载
152
- 6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
153
- 7. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
154
- 8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
155
- 9. 将模块文档作为制定计划的上下文,确保计划符合模块当前设计
156
- 10. **利用模块依赖关系辅助分析**:
157
- - 用 depends_on 判断哪些模块会被间接影响
158
- - 用 used_by 判断变更会不会影响下游模块
159
- - 将依赖关系纳入 Wave 分组决策(依赖同一模块的任务尽量同 Wave)
160
- - 如果变更涉及多个有依赖关系的模块,在 plan.md 的任务总表中标注模块依赖
144
+ 然后列出已加载的文件清单(含 decisions.md 当前版本/未决项状态、模块文档 + 模块依赖关系摘要)。
161
145
 
162
- ### 输出
163
- 已加载的文件清单(含 decisions.md 当前版本/未决项状态、模块文档 + 模块依赖关系摘要)`,
164
- outputHint: '文件清单',
165
- optional: false
166
- },
167
- {
168
- name: '锚定确认',
169
- prompt: `确认已读取的文件。
146
+ 分类完成后,继续进入下一步。`,
147
+ outputHint: '复杂度分类结果 + 文件清单',
148
+ optional: false
149
+ }
170
150
 
171
- ### 操作
172
- 列出已读取的文件,标注存在/不存在。
151
+ // ═══════════════════════════════════════════════════════════════
152
+ // 第 2 步(LLM):生成分级计划 + 自检(合并原 ⑤⑥)
153
+ // ═══════════════════════════════════════════════════════════════
173
154
 
174
- ### 输出
175
- 文件加载确认清单`,
176
- outputHint: '文件确认清单',
177
- optional: false
178
- },
179
- {
180
- name: '按复杂度生成分级计划',
181
- prompt: `根据「复杂度分类」步骤的 plan_level 结果,按对应级别生成计划。
155
+ const stepGeneratePlan = {
156
+ id: 'generate_plan',
157
+ name: '生成分级计划与自检',
158
+ prompt: `根据上一步的 plan_level 结果,按对应级别生成计划,然后立即自检。
182
159
 
183
160
  ### 操作
184
161
  1. 读取上一步输出的 plan_level 分类结果
185
162
  2. 读取 tasks.md 和 design.md 了解需求范围
186
163
  3. 按 plan_level 选择对应模板输出
164
+ 4. 生成后立即自检(见下方自检清单)
187
165
 
188
166
  ---
189
167
 
@@ -247,7 +225,7 @@ plan_level: light
247
225
  light 计划的约束:
248
226
  - **禁止**生成 Mermaid 图
249
227
  - **禁止**估时
250
- - **禁止**泛泛风险 分析(如"需要充分测试")
228
+ - **禁止**泛泛风险分析(如"需要充分测试")
251
229
  - **禁止**放实现细节(函数签名、代码示例)
252
230
  - 来源/目标直接引用已有文档,不重新生成
253
231
  - 如果存在 decisions.md,所有当前版本 D-xxx@vN 必须在 Tasks 或覆盖矩阵中出现
@@ -339,16 +317,10 @@ full 计划的约束:
339
317
  5. 保存到变更目录下的 plan.md(路径格式:\`.sillyspec/changes/<change-name>/plan.md\`,其中 <change-name> 是变更目录名,直接使用,不加子目录。正确路径示例:\`.sillyspec/changes/2026-05-28-agent-log-streaming/plan.md\`)
340
318
  **plan_level 为 none 时生成最小 plan.md(占位),不生成完整蓝图。**
341
319
 
342
- ### 输出
343
- plan_level + 计划内容(none 级别输出建议操作)`,
344
- outputHint: '计划内容',
345
- optional: false
346
- },
347
- {
348
- name: '自检总览',
349
- prompt: `根据 plan_level 检查对应的计划质量。
320
+ ---
321
+
322
+ ### 自检(生成后立即执行,不另开步骤)
350
323
 
351
- ### 操作
352
324
  读取上一步的 plan_level 分类结果,按级别执行对应的自检:
353
325
 
354
326
  #### plan_level = none
@@ -390,213 +362,18 @@ plan_level + 计划内容(none 级别输出建议操作)`,
390
362
  - [ ] 没有泛泛风险分析(如"需要充分测试")
391
363
 
392
364
  ### 输出
393
- 自检通过/不通过(附 plan_level)`,
394
- outputHint: '自检结果',
395
- outputHint: '自检结果',
396
- optional: false
397
- }
398
- ]
399
-
400
- // 固定后缀步骤
401
- export const fixedSuffix = [
402
- {
403
- name: '重排 Wave(基于 depends_on)',
404
- prompt: `根据蓝图的 depends_on 字段重排 Wave 分组,更新 plan.md。
405
-
406
- ### 操作
407
- 1. 读取所有 tasks/task-NN.md 的 frontmatter,提取每个任务的 depends_on 列表
408
- 2. 拓扑排序:无依赖的任务 → Wave 1,依赖 Wave 1 的 → Wave 2,依此类推
409
- 3. 检查是否存在循环依赖,如有则报错暂停
410
- 4. 用重排结果更新 plan.md:
411
- - Wave 分组(含 checkbox 列表)
412
- - 任务总表的 Wave 列
413
- - 依赖关系图(Mermaid)
414
- - 关键路径
415
- 5. 如果 Wave 分组与原始 plan.md 一致,只需确认一致即可,不需要重写
416
-
417
- ### 规则
418
- - **Wave 是执行单元,同 Wave 内任务必须无依赖(可并行)**
419
- - 有 depends_on 关系的任务必须在不同的 Wave
420
- - depends_on 为空的任务放 Wave 1
421
- - 取决于拓扑排序的最大深度决定 Wave 编号
422
-
423
- ### 输出
424
- 重排后的 Wave 分组摘要(如果与原 plan.md 一致则说明一致)`,
425
- outputHint: 'Wave 重排结果',
426
- optional: false
427
- },
428
- {
429
- name: '审查一致性',
430
- prompt: `审查所有 task-N.md 的一致性。
431
-
432
- ### 操作
433
- 1. 读取所有 tasks/task-NN.md
434
- 2. 检查:
435
- - 文件路径有没有冲突(两个任务改同一个文件)
436
- - 依赖关系和 plan.md 的 Wave 分组是否一致
437
- - 验收标准和 plan.md 的全局标准是否矛盾
438
- - 接口定义是否自洽
439
- 5. **生产接线路径检查**(Critical):
440
- - 读取 design.md,搜索以下关键词:
441
- - 注入 / inject / 构造 / constructor / 初始化 / init / 启动路径 / startup / main / cli / entrypoint / daemon start / bootstrap
442
- - 如果命中,提取提到的具体文件名(如 "在 cli.ts 中"、"main.ts 中实例化"、"Daemon 构造函数")
443
- - 收集所有 tasks/task-NN.md 的 frontmatter 中 \`allowed_paths\` 列表
444
- - 检查:design 中提到的生产入口文件是否在某个 task 的 allowed_paths 中?
445
- - 如果 design 提到了入口文件但所有 task 的 allowed_paths 都不含该文件:
446
- - **这是 plan contract 失败**
447
- - 列出具体矛盾:\`design says: X 实例化 Y,但 allowed_paths 不含 Z\`
448
- - **不自动修复**,暂停等待用户决定
449
- - 如果 design 明确说"不需要改入口文件"并给出了理由,视为通过
450
- - 调用:\`sillyspec run plan --wait --reason "生产接线路径检查失败" --options "修改 allowed_paths,修改 design,确认不需要改入口" --output "矛盾清单"\`
451
- 6. **符号影响面检查**(Critical):
452
- - 对每个 task 涉及的文件,检查是否修改了构造函数、接口、DTO、API client 方法签名
453
- - 如果是,搜索所有调用点:
454
- \`\`\`bash
455
- rg "new <ClassName>" src/
456
- rg "<methodName>" src/ --type ts --type js
457
- \`\`\`
458
- - 对比调用点与 plan.md/task 的 allowed_paths
459
- - 发现调用点不在任务范围内 → **plan contract 失败**
460
- - 调用:\`sillyspec run plan --wait --reason "符号影响面检查发现遗漏调用点" --options "扩展 allowed_paths,添加新任务,确认不需要改" --output "遗漏调用点清单"\`
461
- 3. 发现问题 → 列出问题清单,暂停等待用户决定
462
- - 调用:\`sillyspec run plan --wait --reason "审查发现一致性问题" --options "自动修复,手动修复,忽略并继续" --output "问题清单"\`
463
- - **绝对禁止**:自己决定修复方向然后自动修复
464
- 4. 无问题 → 正常完成
465
-
466
- ### 输出
467
- 一致性审查结果`,
468
- outputHint: '审查结果',
469
- optional: false
470
- },
471
- {
472
- name: '保存并更新进度',
473
- prompt: `确认所有文件已保存,更新进度。
474
-
475
- ### 操作
476
- 1. 确认 plan.md 和所有 tasks/task-NN.md 已存在
477
-
478
- ### 输出
479
- 文件列表 + 下一步命令`,
480
- outputHint: '文件列表',
481
- optional: false
482
- }
483
- ]
484
-
485
- /**
486
- * 解析 plan.md 获取任务数量
487
- */
488
- function parseTaskCount(planContent) {
489
- if (!planContent || typeof planContent !== 'string') return 0
490
- const matches = planContent.match(/^[-*]\s*\[[ x]\]\s*task-\d+/gm)
491
- return matches ? matches.length : 0
365
+ plan_level + 计划内容 + 自检结果(一次输出)`,
366
+ outputHint: '计划内容 + 自检结果',
367
+ optional: false
492
368
  }
493
369
 
494
- /**
495
- * 生成单个任务的蓝图写作 prompt
496
- */
497
- function buildTaskPrompt(taskNum, taskName, changeDir) {
498
- const num = String(taskNum).padStart(2, '0')
499
- return `编写任务蓝图 tasks/task-${num}.md
500
-
501
- 当前时间:<now-datetime>(frontmatter 的 created_at 使用此值)
502
- 当前用户:<git-user>(frontmatter 的 author 使用此值)
503
-
504
- ### 任务
505
- ${taskName}
506
-
507
- ### 文件路径
508
- \`.sillyspec/changes/<change-name>/tasks/task-${num}.md\`
509
-
510
- ### 格式要求(必须严格遵守)
511
- \`\`\`markdown
512
- ---
513
- id: task-${num}
514
- title: ${taskName}
515
- priority: P0
516
- estimated_hours: N
517
- depends_on: []
518
- blocks: []
519
- requirement_ids: [FR-01]
520
- decision_ids: [D-001@v1]
521
- allowed_paths:
522
- - 允许修改的路径范围
523
- ---
524
-
525
- # task-${num}: ${taskName}
526
-
527
- ## 修改文件(必填)
528
- - 精确到文件路径,列出所有需要新增或修改的文件
529
-
530
- ## 覆盖来源
531
- - Requirements: FR-xx(来自 requirements.md)
532
- - Decisions: D-xxx@vN(如存在 decisions.md)
533
-
534
- ## 实现要求
535
- 1. 具体做什么,写清楚
536
- 2. ...
537
-
538
- ## 接口定义(代码类任务必填)
539
- 写方法签名、数据结构、控制流伪代码。AI executor 应能照着直接编码。
540
-
541
- ## 边界处理(必填)
542
- - null/空值行为
543
- - 兼容旧行为(brownfield:未配置新功能时行为不变)
544
- - 异常不静默吞掉(明确返回值或抛出)
545
- - 不修改传入参数
546
- - 歧义/冲突场景的处理策略
547
-
548
- ## 非目标(本任务不做的事)
549
- - 明确列出边界,防止 scope creep
550
-
551
- ## 参考
552
- - 已有代码可参考的模式
553
- - 相关的 CONVENTIONS.md 条目
554
-
555
- ## TDD 步骤
556
- 1. 写 XxxTest,覆盖场景 A/B/C
557
- 2. 运行 <test-cmd> 确认测试失败
558
- 3. 实现 Xxx
559
- 4. 运行 <test-cmd> 确认测试通过
560
- 5. 运行全量测试确认无回退
561
- (纯配置/文档类任务简化为:1. 实现 2. 验证)
562
-
563
- ## 验收标准
564
- | # | 验证步骤 | 通过标准 |
565
- |---|---|---|
566
- | AC-01 | 具体操作 | 期望结果 |
567
- | AC-02 | ... | ... |
568
- \`\`\`
569
-
570
- ### frontmatter 元数据说明
571
- - \`priority\`: P0(必须)/ P1(重要)/ P2(可选)
572
- - \`estimated_hours\`: 预估工时,单个 task ≤ 8h
573
- - \`depends_on\`: 依赖的前序 task 编号列表
574
- - \`blocks\`: 被本 task 阻塞的后续 task 编号列表
575
- - \`requirement_ids\`: 本任务覆盖的 FR-xxx 列表
576
- - \`decision_ids\`: 本任务覆盖的当前版本 D-xxx@vN 列表;无 decisions.md 时可为空数组
577
- - \`allowed_paths\`: AI executor 可以修改的文件路径范围(安全边界)
578
-
579
- ### 关键规则
580
- - task-N.md 必须独立完整,execute 子代理只读这一个文件就能干活
581
- - 不要依赖其他 task-N.md 的内容
582
- - 接口定义写到"搬砖工照着做"的程度
583
- - 边界处理至少覆盖 5 条规则
584
- - 验收标准用表格格式,每条可点击验证,禁止"功能可演示"类笼统表述
585
- - 如果存在 decisions.md,不允许丢失当前版本 D-xxx@vN;无法覆盖的 D-xxx@vN 必须写入非目标或剩余风险
586
- - 写完后保存到文件
587
-
588
- ### 操作
589
- 1. 读取 design.md 和 plan.md 了解上下文
590
- 2. 读取相关源文件了解现有代码
591
- 3. 编写任务蓝图
592
- 4. 保存到 tasks/task-${num}.md
593
-
594
- ### 输出
595
- 任务蓝图内容摘要`
596
- }
370
+ // ═══════════════════════════════════════════════════════════════
371
+ // 3 步(LLM):生成紧凑 TaskCard(子代理并行)
372
+ // ═══════════════════════════════════════════════════════════════
597
373
 
598
374
  /**
599
- * 构建任务蓝图协调器步骤(单步,子代理并行写蓝图)
375
+ * 构建紧凑 TaskCard 协调器步骤(单步,子代理并行写卡片)
376
+ * 每个 task 生成 20~40 行紧凑可执行卡片
600
377
  */
601
378
  export function buildCoordinatorStep(changeDir, taskNames) {
602
379
  const taskList = taskNames.map((name, i) => {
@@ -615,76 +392,54 @@ export function buildCoordinatorStep(changeDir, taskNames) {
615
392
 
616
393
  操作:
617
394
  1. 读取 ${changeDir}/design.md 和 ${changeDir}/plan.md 了解上下文
618
- 2. 读取相关模块文档(如存在):
619
- - \
620
- ls .sillyspec/docs/*/modules/_module-map.yaml 2>/dev/null\
621
- 确认模块划分,读取涉及模块的 \
622
- ls .sillyspec/docs/*/modules/*.md 2>/dev/null\
623
- 3. 读取相关源文件了解现有代码
624
- 4. 按以下格式编写任务蓝图并保存到 ${changeDir}/tasks/task-${num}.md:
395
+ 2. 读取相关源文件了解现有代码
396
+ 3. 生成紧凑 TaskCard(20~40 行),格式如下:
625
397
 
626
398
  ---
627
399
  id: task-${num}
628
400
  title: ${name}
629
- priority: P0/P1/P2
630
- estimated_hours: N
631
- depends_on: [task-XX]
632
- blocks: [task-XX]
401
+ author: <git-user>
402
+ created_at: <now-datetime>
403
+ priority: P0
404
+ depends_on: []
405
+ blocks: []
633
406
  requirement_ids: [FR-XX]
634
407
  decision_ids: [D-XXX@vN]
635
408
  allowed_paths:
636
- - ...
409
+ - frontend/src/lib/errors.ts
410
+ goal: >
411
+ 一句话说明这个 task 要做什么、为什么。
412
+ implementation:
413
+ - 具体步骤 1
414
+ - 具体步骤 2
415
+ - 具体步骤 3
416
+ acceptance:
417
+ - 可验证的验收条件 1
418
+ - 可验证的验收条件 2
419
+ - 可验证的验收条件 3
420
+ verify:
421
+ - cd frontend && pnpm exec tsc --noEmit
422
+ constraints:
423
+ - 边界约束 1(如:不加测试)
424
+ - 边界约束 2(如:不修改传入参数)
637
425
  ---
638
426
 
639
- # task-${num}: ${name}
640
-
641
- ## 修改文件(必填)
642
- - 精确到文件路径
643
-
644
- ## 覆盖来源
645
- - Requirements: FR-xx
646
- - Decisions: D-xxx@vN(如存在)
647
-
648
- ## 实现要求
649
- 1. 具体做什么
650
-
651
- ## 接口定义(代码类任务必填)
652
- 方法签名、数据结构、控制流伪代码
653
-
654
- ## 边界处理(必填)
655
- - null/空值行为
656
- - 兼容旧行为(brownfield:未配置新功能时行为不变)
657
- - 异常不静默吞掉(明确返回值或抛出)
658
- - 不修改传入参数
659
- - 歧义/冲突场景的处理策略
660
-
661
- ## 非目标(本任务不做的事)
662
- - 明确边界,防止 scope creep
663
-
664
- ## 参考
665
- - 可参考的模式
666
-
667
- ## TDD 步骤
668
- 1. 写测试 → 2. 确认失败 → 3. 写代码 → 4. 确认通过 → 5. 回归
669
-
670
- ## 验收标准
671
- | # | 验证步骤 | 通过标准 |
672
- |---|---|---|
673
- | AC-01 | ... | ... |
674
-
675
- 关键规则:
676
- - 必须独立完整,execute 子代理只读这一个文件就能干活
677
- - 不要依赖其他 task-N.md 的内容
678
- - 接口定义写到"搬砖工照着做"的程度
679
- - 边界处理至少 5 条
680
- - 验收标准用表格,禁止笼统表述
681
- - 如果存在 decisions.md,不允许丢失当前版本 D-xxx@vN;无法覆盖的 D-xxx@vN 必须写入非目标或剩余风险
427
+ TaskCard 格式规则(必须严格遵守):
428
+ - 总长度 20~40 行,不要写成长文档
429
+ - frontmatter 只含必要字段,不加 estimated_hours
430
+ - goal: 一句话,用 > 多行字符串
431
+ - implementation: 列表,每条一个具体步骤
432
+ - acceptance: 列表,每条可独立验证(不是表格)
433
+ - verify: 列表,实际可执行的命令
434
+ - constraints: 列表,明确边界(含 brownfield 兼容、异常处理)
435
+ - 不需要:修改文件章节、覆盖来源章节、接口定义章节、TDD 步骤章节、参考章节
436
+ - 如果存在 decisions.md,无法覆盖的 D-xxx@vN 在 constraints 中标注
682
437
  - 写完后用 Write tool 保存到文件
683
438
  \`\`\``
684
439
  }).join('\n\n')
685
440
 
686
441
 
687
- const prompt = `为 plan.md 中的每个任务生成独立蓝图文件。
442
+ const prompt = `为 plan.md 中的每个任务生成紧凑 TaskCard。
688
443
 
689
444
  ## 任务清单
690
445
  ${taskList}
@@ -695,7 +450,7 @@ ${taskList}
695
450
 
696
451
  ## 执行方式(必须严格遵守)
697
452
 
698
- **你必须使用 Agent tool 启动子代理来写每个蓝图,不要自己写。**
453
+ **你必须使用 Agent tool 启动子代理来写每个卡片,不要自己写。**
699
454
 
700
455
  1. 确认 \`${changeDir}/tasks/\` 目录存在(不存在则创建)
701
456
  2. 为每个任务启动一个独立子代理(Agent tool),可并行启动多个
@@ -708,20 +463,67 @@ ${taskList}
708
463
 
709
464
  ${subagentPrompts}
710
465
 
711
- ## 验收
466
+ ## 验收(生成后自查,不另开步骤)
712
467
  - 每个 task-N.md 文件存在且非空
713
- - 包含 YAML frontmatter(id、title、priority、depends_on、blocks、requirement_ids、decision_ids、allowed_paths
714
- - 包含所有必要章节:修改文件、覆盖来源、实现要求、接口定义、边界处理(≥5条)、非目标、TDD 步骤、验收标准(表格格式)
715
- - 边界处理覆盖:null/空值、兼容性、异常处理、参数不可变、歧义场景`
468
+ - frontmatter 包含:id、title、author、created_at、priority、depends_on、blocks、allowed_paths
469
+ - body 包含:goal、implementation、acceptance、verify、constraints
470
+ - 每个 task 总长度 20~40 行
471
+ - **一致性自查**:
472
+ - allowed_paths 有无冲突
473
+ - depends_on 与 plan.md Wave 分组是否一致
474
+ - 如发现矛盾,列出问题清单,不要自动修复`
716
475
 
717
476
  return {
718
- name: '生成任务蓝图(子代理并行)',
477
+ id: 'generate_blueprints',
478
+ name: '生成 TaskCard(子代理并行)',
719
479
  prompt,
720
- outputHint: '蓝图生成结果',
480
+ outputHint: 'TaskCard 生成结果',
721
481
  optional: false
722
482
  }
723
483
  }
724
484
 
485
+ // ═══════════════════════════════════════════════════════════════
486
+ // 第 4 步(noAI):Wave 重排 + 一致性校验 + 保存(合并原 ⑧⑨⑩,全代码化)
487
+ // 核心逻辑已迁移到 plan-postcheck.js,此处只保留步骤定义
488
+ // ═══════════════════════════════════════════════════════════════
489
+
490
+ /**
491
+ * noAI postcheck 步骤:Wave 重排 + 一致性校验 + 可行性校验 + 保存确认
492
+ * 核心逻辑见 plan-postcheck.js
493
+ */
494
+ export function buildPostcheckStep(changeDir) {
495
+ return {
496
+ id: 'postcheck',
497
+ name: 'Wave 重排与可行性校验',
498
+ prompt: '', // noAI 步骤不需要 prompt
499
+ outputHint: 'Wave 重排 + 校验结果',
500
+ optional: false,
501
+ noAI: true,
502
+ _cliAction: 'planPostcheck'
503
+ }
504
+ }
505
+
506
+ // ═══════════════════════════════════════════════════════════════
507
+ // 向后兼容:导出 fixedPrefix / fixedSuffix(供 run.js 切片用)
508
+ // ═══════════════════════════════════════════════════════════════
509
+
510
+ export const fixedPrefix = [stepClassify, stepGeneratePlan]
511
+
512
+ export const fixedSuffix = [] // postcheck 是动态生成的(需要 changeDir)
513
+
514
+ // ═══════════════════════════════════════════════════════════════
515
+ // 工具函数(保持导出兼容)
516
+ // ═══════════════════════════════════════════════════════════════
517
+
518
+ /**
519
+ * 解析 plan.md 获取任务数量
520
+ */
521
+ function parseTaskCount(planContent) {
522
+ if (!planContent || typeof planContent !== 'string') return 0
523
+ const matches = planContent.match(/^[-*]\s*\[[ x]\]\s*task-\d+/gm)
524
+ return matches ? matches.length : 0
525
+ }
526
+
725
527
  /**
726
528
  * 从 plan.md 解析任务名列表
727
529
  */
@@ -737,6 +539,8 @@ function parseTaskNames(planContent) {
737
539
 
738
540
  /**
739
541
  * 动态构建 plan 步骤列表
542
+ * 新架构:3 个 LLM 步骤 + 1 个 noAI 步骤 = 4 阶段
543
+ *
740
544
  * @param {string|null} changeDir - 变更目录路径
741
545
  * @param {string|null} planContent - plan.md 内容(可选,用于解析任务数)
742
546
  * @returns {Array} 步骤列表
@@ -754,9 +558,10 @@ export function buildPlanSteps(changeDir = null, planContent = null) {
754
558
  }
755
559
  }
756
560
 
757
- // 没有任务数则用固定步骤(兼容旧流程)
561
+ // 没有任务数则用固定步骤(兼容旧流程,无蓝图步骤无 postcheck)
758
562
  if (taskCount === 0) {
759
- return [...fixedPrefix, ...fixedSuffix]
563
+ const postcheck = changeDir ? [buildPostcheckStep(changeDir)] : []
564
+ return [...fixedPrefix, ...postcheck]
760
565
  }
761
566
 
762
567
  // 解析任务名
@@ -770,7 +575,8 @@ export function buildPlanSteps(changeDir = null, planContent = null) {
770
575
  }
771
576
  }
772
577
 
773
- // 生成单个协调器步骤(子代理并行写蓝图)
578
+ // 生成协调器步骤(TaskCard 生成)+ postcheck
774
579
  const coordinatorStep = buildCoordinatorStep(changeDir, taskNames)
775
- return [...fixedPrefix, coordinatorStep, ...fixedSuffix]
580
+ const postcheckStep = buildPostcheckStep(changeDir)
581
+ return [...fixedPrefix, coordinatorStep, postcheckStep]
776
582
  }