coding.mjs
73.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
// workflows/coding.mjs
//
// 整个 ERP Coding(B 阶段)= 一个静默、全自动的 Workflow 脚本。
// 设计原则见仓库根 README.md「阶段 B」与「设计原则」节;featureLoop 顺序 for-await 的取舍
// 详见 featureLoop 函数处的注释。运行时禁用日期 / 随机数 builtin,所有"今天"由子代理解析。
export const meta = {
name: 'erp-coding',
description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.',
phases: [
{ title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' },
{ title: 'Gate' }, { title: 'Milestone' },
],
}
const ROUTER_SCHEMA = { type:'object', additionalProperties:false,
required:['modules'], properties:{ modules:{ type:'array', items:{
type:'object', additionalProperties:false,
required:['id','done','reqs','feItems'],
properties:{ id:{type:'string'}, done:{type:'boolean'},
reqs:{type:'array',items:{type:'string'}},
feItems:{type:'array',items:{type:'string'}} } } } } }
// REVIEW_SCHEMA:reviewer 裁决;issues 结构化对象(summary/locator/severity)驱动 fix。
const REVIEW_SCHEMA = { type:'object', additionalProperties:false,
required:['verdict','round','issues'], properties:{
verdict:{type:'string',enum:['approve','request-changes']},
round:{type:'integer'},
issues:{ type:'array', items:{
type:'object', additionalProperties:false,
required:['summary','locator','severity'],
properties:{
summary:{type:'string'},
locator:{type:'string'},
severity:{type:'string', enum:['blocker','high','medium','low']} } } } } }
// STAGE_RESULT_SCHEMA:派生 stage 统一返回,status=halt 时 JS 立即 throw HALT。
const STAGE_RESULT_SCHEMA = { type:'object', additionalProperties:false,
required:['status'], properties:{
status:{type:'string', enum:['ok','halt']},
reason:{type:'string'},
artifactPath:{type:'string'},
summary:{type:'string'} } }
const GATE_SCHEMA = { type:'object', additionalProperties:false,
required:['status'], properties:{ status:{type:'string',enum:['green','red']},
failures:{type:'array',items:{type:'string'}} } }
// ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)─────────
const WT_SCHEMA = { type:'object', additionalProperties:false,
required:['clean'], properties:{
clean:{type:'boolean'},
dirty:{type:'array', items:{type:'string'}} } }
const DEFAULT_BRANCH_SCHEMA = { type:'object', additionalProperties:false,
required:['branch'], properties:{ branch:{type:'string'} } }
const EXISTS_SCHEMA = { type:'object', additionalProperties:false,
required:['exists'], properties:{ exists:{type:'boolean'} } }
const FIELD_VALUE_SCHEMA = { type:'object', additionalProperties:false,
required:['found','value'], properties:{
found:{type:'boolean'},
value:{type:'string'},
lineNumber:{type:'integer'} } }
// CHECKBOX_STATE_SCHEMA:docs/08 功能行勾选态;state 必填——只 require found 时 cb.state 缺失会静默走 checked 分支。
const CHECKBOX_STATE_SCHEMA = { type:'object', additionalProperties:false,
required:['found','state'], properties:{
found:{type:'boolean'},
state:{type:'string', enum:['checked','unchecked']},
lineNumber:{type:'integer'} } }
const ALREADY_MERGED_SCHEMA = { type:'object', additionalProperties:false,
required:['alreadyMerged'], properties:{ alreadyMerged:{type:'boolean'} } }
const REPORT_PATH_SCHEMA = { type:'object', additionalProperties:false,
required:['found'], properties:{
found:{type:'boolean'},
path:{type:'string'},
currentTagValue:{type:'string'} } }
const CHANGED_FILES_SCHEMA = { type:'object', additionalProperties:false,
required:['files'], properties:{
files:{type:'array', items:{type:'object', additionalProperties:false,
required:['status','path'],
properties:{ status:{type:'string'}, path:{type:'string'} } } } } }
const CROSS_CLASSIFY_SCHEMA = { type:'object', additionalProperties:false,
required:['crossModule'], properties:{
crossModule:{type:'array', items:{type:'object', additionalProperties:false,
required:['file','targetModule','reason','impact'],
properties:{ file:{type:'string'}, targetModule:{type:'string'},
reason:{type:'string'}, impact:{type:'string'} } } } } }
// 所有 action 步骤(写文件 / git 改写仓库状态)统一返回 success/error;JS 据此抛错 halt。
const ACTION_RESULT_SCHEMA = { type:'object', additionalProperties:false,
required:['success'], properties:{
success:{type:'boolean'},
error:{type:'string'},
detail:{type:'string'} } }
const ROOT = args?.projectRoot || '.'
// ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。
if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) {
throw new Error(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`)
}
// ── Feature-loop stage prompts(共享非交互契约见 featureStageContract)──
function isFrontend(phase) { return phase === 'frontend' }
// 从 spec/plan 等 artifactPath 文件名提取 `YYYY-MM-DD` 前缀,下游所有日期相关产物(plan / verify /
// review report)一律复用同一日期,避免长跑或次日 resume 时各 sub-agent 各自解析"今天"导致路径分叉。
// 纯字符串运算,不触发非确定性内建(Workflow runtime 仅禁用 time/random builtin)。
function dateFromArtifactPath(artifactPath) {
const fname = (artifactPath || '').split('/').pop() || ''
const m = fname.match(/^(\d{4}-\d{2}-\d{2})-/)
if (!m) throw new Error(`HALT invalid-artifactPath: 文件名缺少 YYYY-MM-DD 前缀 (${JSON.stringify(artifactPath)})`)
// 进一步排查 pattern 合法但语义无效的日期(如 9999-99-99-foo.md):
// 正则只判位数;下面校验年/月/日落在真实日历范围内,防止下游 plan/verify 以无意义日期级联生成产物。
const [, yStr, moStr, dStr] = m[0].match(/^(\d{4})-(\d{2})-(\d{2})-/) || []
const y = Number(yStr), mo = Number(moStr), d = Number(dStr)
if (!(y >= 2024 && y <= 2099) || !(mo >= 1 && mo <= 12) || !(d >= 1 && d <= 31)) {
throw new Error(`HALT invalid-date-prefix: 文件名日期前缀语义无效 (${JSON.stringify(artifactPath)}),年须在 2024-2099、月 1-12、日 1-31`)
}
return m[1]
}
// 所有子代理共享的"非交互静默"硬约束。
function featureStageContract(phase) {
const fe = isFrontend(phase)
return [
'## 硬约束(非交互子代理)',
'- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。',
'- 缺值查找顺序:`config-vars.yaml` → `docs/07-环境配置.md` → `docs/04-技术规范.md` → `docs/05-API接口契约.md` → `prototype/`(前端布局/交互权威)→ `src/styles/tokens.css`(前端色值)→ `CLAUDE.md` → 现有代码。',
'- 仍查不到 → **不要编造、不要留 `【人工填写:】` / `TBD` / `TODO` 占位**;把具体阻塞点(缺哪个值、应在哪个 Plan 闸门锁定、为何无法继续)写进产物。',
'- 然后让本步骤以非零结果 / 显式 throw 结束,由上层 Workflow 转为带诊断的 halt(fail-fast)。',
'- 全部输出文档**使用中文**。',
`- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe
? '实现文件必须落在 `frontend/`(或 `docs/09-项目目录结构.md § 前端目录结构` 声明的前端根)下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。'
: '产出范围限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约;**禁止**写 `frontend/` 路径下的实现(UI 推迟到前端阶段)。'}`,
`- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `REQ-XXX-NNN`。'}`,
].join('\n')
}
// commitBlock:spec/plan/verify/review 共用的"写完 → add → commit → 失败 halt"四行块。
function commitBlock(addPath, msg, tail = '- commit 失败 → halt,把 stderr 摘要写进 reason。') {
return [
'## commit',
`- 写完后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`,
` 1. \`git -C ${ROOT} add ${addPath}\``,
` 2. \`git -C ${ROOT} commit -m "${msg}"\``,
tail,
].join('\n')
}
// Router:读 docs/08 §二/§三 + git tag,重算进度,返回 ROUTER_SCHEMA。
function routerPrompt(root) {
return [
'# Coding Router — 从账本重算进度',
'',
`项目根:\`${root}\``,
'',
'你是 Coding 阶段的路由子代理。**只读不写**(不改任何代码 / 文档),仅从状态账本重算"哪些模块还要跑",返回结构化结果。',
'',
'## 读取来源(账本 = docs/08 + git tag,里程碑和功能级完成都以 tag 为真值)',
'1. `docs/08-模块任务管理.md § 二`(后端模块元数据):逐个模块取 `id`(英文蛇形 module id)、本模块的 REQ 列表(按 `docs/02-开发计划.md § 二 开发顺序清单` 的顺序,A5 约束保证同模块 REQ 连续),以及该模块的 `里程碑:` 字段。',
'2. `docs/08-模块任务管理.md § 三`(前端阶段元数据):取 `整体里程碑:` 字段,以及 `功能:` 项下所有 `- [ ] FE-NN ...` / `- [x] FE-NN ...` 行(FE 清单)。前端 item 形如 `FE-NN`。',
'3. `git -C <root> tag -l "milestone/*"`:列出已打的里程碑 tag。',
'4. `git -C <root> tag -l "req-done/*"`:列出已通过 review 并落地的功能级完成 tag。`docs/08` checkbox 只作可视化,不作为跳过功能的真值。',
'',
'## 完成判定(每个模块独立)',
'- 后端模块 `done = true` 当且仅当:§二 该模块 `里程碑:` 字段 == `milestone/<module_id>` **且** `git tag -l "milestone/<module_id>"` 能查到该 tag。任一缺失 → `done = false`。',
'- 前端 item(FE-NN)归属一个"逻辑前端模块"。前端阶段整体 `done` 当且仅当 §三 `整体里程碑:` == `milestone/frontend-phase` 且 `git tag -l "milestone/frontend-phase"` 存在。',
'- 后端 REQ / 前端 FE 的功能级完成判定:仅当 `git tag -l "req-done/<id>"` 能查到该 tag 才视为已 approve。不要因为存在 review markdown 或 docs/08 checkbox 已勾就跳过;若 tag 缺失,必须把该 id 放回待跑列表。',
'',
'## 输出(必须符合下发的 JSON schema)',
'- `modules`: 数组。**先**按 `docs/02 § 二` 的模块顺序列出全部后端模块,**再在末尾追加唯一一个前端聚合模块**(仅当存在前端 FE 时)。每项:',
' - `id`: 模块标识(后端为英文蛇形 module id;前端聚合模块固定用 `frontend-phase`)。',
' - `done`: 该模块/前端阶段是否已完成(按上面的 milestone 判定)。',
' - `reqs`: **仅后端模块**填本模块**缺少 `req-done/<REQ>` tag** 的后端 REQ 有序列表;模块已 done → 空数组。**前端聚合模块 `reqs` 恒为空数组**。',
' - `feItems`: **仅前端聚合模块**填——把**全部模块**缺少 `req-done/<FE-NN>` tag 的前端 FE-NN 汇总为一个有序列表放进 `frontend-phase` 这一项。**后端模块 `feItems` 恒为空数组**(前端不分摊到后端模块)。',
'- 即:后端模块只承载 `reqs`、`feItems=[]`;末尾的 `frontend-phase` 模块只承载 `feItems`、`reqs=[]`。整个项目至多一个前端聚合模块,对应至多一个 `milestone/frontend-phase` tag。',
'- 不要返回任何额外字段(schema 为 `additionalProperties:false`)。',
'',
'## 缺值处理',
'- docs/08 §二/§三 缺失 / 格式不符 / 无法解析 → **不要猜**:把具体的解析失败点写入返回前的诊断并使本步骤失败(让 Workflow halt),由人工修复 Plan 产物后重跑 `coding-start`。',
].join('\n')
}
// ---- 功能内循环 stage 1:派生 spec(原 feature-brainstorm / fe-feature-brainstorm)----
function deriveSpecPrompt(id, phase) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-brainstorm' : 'feature-brainstorm'} — 派生规格 ${id}`,
'',
featureStageContract(phase),
'',
'## 目标',
`静默派生 \`${id}\` 的实现规格(无 Q&A)。需求歧义本应在 Plan 期的结构化 per-REQ 表单锁定,前端布局/交互以 \`${ROOT}/prototype/\` 为权威;这里**只消费已锁定的事实**,不再澄清。`,
'',
'## 收集上下文',
fe
? [
`- 关联 REQ 卡片:\`${ROOT}/docs/01-需求清单/<module>/<REQ>.md\`(提取业务校验规则、acceptance、UI 描述)。`,
`- 关联 prototype:Read \`${ROOT}/prototype/**/*.html\`(含 anchor 时聚焦相应区域),作为页面布局权威。`,
`- API 契约:\`${ROOT}/docs/05-API接口契约.md\`,按本 FE 关联的 REQ 过滤出消费的端点。`,
`- Design Tokens:\`${ROOT}/src/styles/tokens.css\`(色值 / 状态色单一来源;只用 var(--color-*),禁硬编码 hex)。`,
`- 前端组件库:\`${ROOT}/docs/04-技术规范.md § 零\` 的 \`frontend.ui_lib\`,决定组件选型。`,
].join('\n')
: [
`- REQ 卡片:\`${ROOT}/docs/01-需求清单/<module>/${id}.md\`。**忽略 UI 描述**(控件类型 / 按钮位置 / 列表布局),但校验规则、业务规则仍要落到后端 DTO + service。`,
`- 涉及的数据表定义:\`${ROOT}/docs/03-数据库设计文档.md\`(必要时实时查 mysql 只读)。`,
`- API 契约:\`${ROOT}/docs/05-API接口契约.md\` 中本 REQ 相关端点。`,
].join('\n'),
'',
'## 写 spec',
`- 落盘路径:\`docs/superpowers/specs/<当天日期 YYYY-MM-DD>-${id}.md\`(项目根相对)。当天日期由你在自身上下文解析;**spec 是本功能链上唯一会解析"今天"的 stage**,下游 plan/verify/review 的产物日期一律复用本 spec 文件名前缀(脚本会从 artifactPath 读取)。`,
`- 若已经存在 \`docs/superpowers/specs/*-${id}.md\`(resume 场景),**复用最新一份的日期前缀**,不要起新日期前缀的文件;按需 Edit 已存在的 spec 而不是另起新文件。`,
fe
? '- 规格至少含:关联 REQ + 关联原型;组件树(按页面 / 区域分块,推导自 prototype DOM);页面状态机(loading / empty / error / 正常 / 表单提交中 至少 5 态);消费的后端端点(对齐 docs/05);业务规则前端复刻清单(逐条:规则 / 触发时机 / 报错文案 / 来源 REQ);Design Tokens 引用清单(`var(--color-*)`)。'
: '- 规格覆盖:goal / 输入输出 / 业务规则 / 约束 / schema / API 引用 / acceptance criteria。',
'',
commitBlock('<spec artifactPath>', `docs(spec:${id}): 派生规格`),
'',
'## 自审(inline 修,无须等待)',
`- 占位符扫描:\`TBD\` / \`TODO\` / \`【人工填写:】\`${fe ? ' / `controller` / `service` / `SQL` / `migration`(前端 spec 不应出现后端字样)' : ''} → 命中即修;修不掉的缺值按硬约束失败。`,
'- 内部一致性 / 范围检查(单 plan 能消化吗)/ 歧义检查(任一 requirement 两种解读 → 挑一个写明)。',
'',
'## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
'- 成功:`{ "status": "ok", "artifactPath": "docs/superpowers/specs/YYYY-MM-DD-' + id + '.md", "summary": "<1-2 句中文摘要>" }`',
'- 失败:`{ "status": "halt", "reason": "<缺值阻塞点:缺哪个值 / 应在哪个 Plan 闸门锁定 / 为何无法继续>" }`',
'- `artifactPath` 必须为项目根相对路径(无前导斜杠),文件名首段必须是 `YYYY-MM-DD`;schema 是 `additionalProperties:false`,不要返回额外字段。',
].join('\n')
}
// ---- stage 2:spec → 任务级 TDD 计划(原 feature-plan / fe-feature-plan)----
// specPath:调用方传入的 spec artifactPath(含 YYYY-MM-DD 前缀),plan 复用该日期。
function planPrompt(id, phase, specPath) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-plan' : 'feature-plan'} — 任务级计划 ${id}`,
'',
featureStageContract(phase),
'',
'## 输入',
`- 上游 spec:\`${specPath}\`(已由 spec stage 落盘;不存在则 halt)。**plan 文件名日期前缀必须与 spec 一致**:取 spec 文件名首段 \`YYYY-MM-DD\`,写到 plan 路径,不要重新解析"今天"。`,
fe
? `- \`${ROOT}/docs/04-技术规范.md § 一 前端架构\`(路由 / 状态库 / 组件目录约定 / 测试栈);\`${ROOT}/docs/09-项目目录结构.md § 前端目录结构\`(落盘位置)。用 Grep 在 \`${ROOT}/frontend/\` 定位现有文件。`
: `- \`${ROOT}/docs/04-技术规范.md\` 与 \`${ROOT}/docs/09-项目目录结构.md\`(编码规范 + 目录规范)。用 Grep 在现有代码定位待修改文件。`,
'',
'## 计划写作原则',
'- Plan 告诉 TDD 执行者**做什么**,不是**怎么写代码**(执行者是同模型、全上下文的 tdd stage)。',
`- Plan 锁定**文件边界 + 测试意图 + ${fe ? 'props 契约' : 'API 形状'} + 完成判据**;代码由 TDD 红绿循环产出。`,
'- **禁止 dump 整个文件内容**(pom.xml / entity / config / 组件源码)到 plan——避免双 source of truth 漂移。',
fe ? '- 每个任务标注"测试先行类型" = **jsdom 组件测试** OR **Playwright E2E**。' : '',
'- DRY、YAGNI、TDD、frequent commits。',
'',
'## 任务结构(每个 task = 一个 red-green-commit 单元,4 step)',
'1. 写失败测试(给 `test_file::test_name` + 测试意图);2. 实现最小代码(给 `impl_file`);3. 子会话验证 PASS;4. commit。任务粒度 2-5 分钟。',
fe
? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\`(或 docs/09 声明的前端根)开头;命中 \`backend/\` / \`sql/\` / \`scripts/\` → 修正后重渲染。`
: `- **硬护栏**:任务粒度限定后端文件(controller / service / repository / DTO / 校验 / SQL migration);**禁止**生成 \`frontend/\` 路径任务。`,
'- 允许写死的少数场景:DDL / migration 语句、合同级常量(错误码 / JWT claim / Redis key / 路由 path / API client 签名 / Design Tokens 名)、可选的测试断言 sketch。其余一律散文 + 签名描述。',
'- 首次出现的类 / 方法 / 组件 / hook / API client 函数必须给出签名;跨 task 的签名 / 错误码 / props 类型必须一致。',
'',
'## 写 plan + 自审',
`- 落盘路径:\`docs/superpowers/plans/<同 spec 的 YYYY-MM-DD>-${id}.md\`,文件头含 Goal / Architecture / Tech Stack + checkbox 任务。`,
'- 自审:占位符扫描(按硬约束清单);spec coverage(spec 每节至少指向一个 task,补 gap);类型一致性(签名 / 方法名 / 错误码 / props 一致)。',
'',
commitBlock('<plan artifactPath>', `docs(plan:${id}): 任务级 TDD 计划`),
'',
'## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
'- 成功:`{ "status": "ok", "artifactPath": "docs/superpowers/plans/YYYY-MM-DD-' + id + '.md", "summary": "<1-2 句中文摘要:任务数 / 涉及文件作用域>" }`',
'- 失败:`{ "status": "halt", "reason": "<阻塞点描述>" }`',
'- 日期前缀必须与 spec 同;schema 是 `additionalProperties:false`。',
].filter(Boolean).join('\n')
}
// ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)----
// planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。
function tddPrompt(id, phase, planPath) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`,
'',
featureStageContract(phase),
'',
'## 输入',
`- 计划文件:\`${planPath}\`(不存在则 halt)。`,
`- 测试命令来源:\`${ROOT}/docs/04-技术规范.md § 零\`${fe
? ' 的 `frontend.unit_test_runner` / `frontend.e2e_runner` / `frontend.test_command` / `frontend.e2e_command`(缺失则默认 `pnpm test:ci` / `pnpm e2e:ci`)。'
: ' 确认的后端测试命令(如 Maven profile / `./scripts/test.mjs`);缺失则默认 `node scripts/test.mjs`(与 test-gate 一致)。'}`,
'',
'## 流程',
fe ? '' : '- **Schema 改动前置**(仅当 plan 声明需要):第一个任务写 migration 文件 `V<n>__<snake_case>.sql`(`<n>` = 现有 `sql/migrations/V*.sql` 最大版本号 + 1,只含 DDL),**同步**把新 CREATE / ALTER 反向更新到 `docs/03-数据库设计文档.md` 对应表小节(docs/03 是 schema 的 SSoT),migration + docs/03 改动同一 commit。',
'- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。',
fe
? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。'
: '',
'',
'## 护栏',
'- **绝不**在主会话直接跑测试(mvn / pnpm / playwright / scripts/test.mjs)——必须通过 Agent 子会话。',
fe
? '- **绝不**写非 `frontend/`(或 docs/09 前端根)路径的 `impl_file`;命中 `backend/` / `sql/` / `scripts/` → 硬停并打印 `不允许写非前端文件:<impl_file>`。'
: '- **后端阶段路径硬护栏**:任意 `impl_file` 以 `frontend/` 开头 → 硬停并打印 `后端阶段不允许写前端代码:<impl_file>`,不再继续 TDD。',
'- 每次 commit 含 REQ/FE 标签,不混合无关改同。',
'',
'## 同测试重试账本(硬上限 10 次 / 测试)',
'- 你必须**显式**为每个出现过红色的测试维护一个内存账本 `attempts[<test_file>::<test_name>] = N`,每次该测试的"写失败实现 → 再跑"算 1 次。',
'- 每次失败跑后,**在自身输出中显式打印一行** JSON:`{ "attempts": { "<test_file>::<test_name>": N } }`(便于 review/审计追溯)。',
'- 任一测试的 `attempts >= 10` → **立刻 halt**:返回 `{status:"halt", reason:"tdd-test-stuck: <test_file>::<test_name> 已尝试 10 次"}`,把"该测试名 / 最近一次 failing_assertion / 已尝试的修复摘要"写进 reason,**不要**无限重试。',
'',
'## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
'- 全部任务通过:`{ "status": "ok", "summary": "<完成的任务数 / 引入的文件清单摘要>" }`(artifactPath 可省)。',
'- 任意护栏 / 账本上限 / 缺值 → `{ "status": "halt", "reason": "<具体阻塞点>" }`。',
].filter(Boolean).join('\n')
}
// ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)----
// specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件,
// 避免 reverify 覆盖前轮证据)。
function verifyPrompt(id, phase, implSummary, specPath, round = 0) {
const fe = isFrontend(phase)
const suffix = round === 0 ? 'verify' : `verify-r${round}`
return [
`# ${fe ? 'fe-feature-verify' : 'feature-verify'} — 证据验证 ${id}${round > 0 ? `(第 ${round} 轮 fix 后复验)` : ''}`,
'',
featureStageContract(phase),
'',
'## 目标',
`把 \`${id}\` 的功能测试**派发到 Agent 子会话**执行,按结构化结果渲染证据。**主会话从不直接跑测试,也不自由编写证据。**`,
`- 上游 spec:\`${specPath}\`(日期前缀来源);本次产物文件名前缀必须 = spec 文件名首段 \`YYYY-MM-DD\`。`,
implSummary ? `- 上游 TDD 摘要:${implSummary}` : '',
'',
'## 流程',
fe
? [
`- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 取(缺失默认 \`pnpm test:ci\` / \`pnpm e2e:ci\`)。`,
'- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。',
'- **任一目标 `exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt,不进入 review。',
].join('\n')
: [
`- 测试目标:从 plan 或项目标准命令确定(Maven profile / pnpm script / pytest path / \`${ROOT}/docs/04-技术规范.md § 零\` 的后端命令)。`,
'- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行,不塞全文 stdout)。',
'- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt,不进入 review。',
].join('\n'),
`- 证据落盘路径固定为 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}-${suffix}.md\`(与 review 报告同目录;round=0 → \`-verify.md\`;round>=1 → \`-verify-r<N>.md\`,**每轮独立文件不覆盖前轮**)。同时把核心结构化结果摘要打印到会话便于上层 review stage 引用,**不要**自行另起目录或自由命名文件。`,
'',
commitBlock('<证据 artifactPath>', `docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证`,
'- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'),
'',
'## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
`- 全部通过:\`{ "status": "ok", "artifactPath": "docs/superpowers/reviews/YYYY-MM-DD-${id}-${suffix}.md", "summary": "<exit_code / passed / failed / failed_list 摘要 ≤ 200 字>" }\`。`,
'- 任一红色 / 越界 / 缺值 → `{ "status": "halt", "reason": "<具体阻塞点>", "artifactPath": "<已写入的证据路径(如有)>" }`。',
].filter(Boolean).join('\n')
}
// ---- stage 5a:AI 自审 diff(原 feature-review / fe-feature-review)——委托统一 reviewer agent ----
// lastVerifySummary:round>1 时传入上轮 fix 后复验摘要,让 reviewer 看到"上轮 must-fix 真的修了什么"。
// specPath:spec artifactPath(日期前缀来源 + reviewer 上下文输入)。
function reviewPrompt(id, phase, round, lastVerifySummary, specPath) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-review' : 'feature-review'} — AI 自审 ${id}(第 ${round} 轮)`,
'',
featureStageContract(phase),
'',
'## 目标',
`对 \`${id}\` 本轮引入的代码 diff 做 AI 自审,给出 \`approve\` 或 \`request-changes\` 裁决。`,
'',
'## 输入给 reviewer',
`- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`,
fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '',
`- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`,
round > 1 && lastVerifySummary
? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。`
: '',
'',
'## 输出(必须符合下发的 REVIEW JSON schema)',
`- \`verdict\`: \`approve\` | \`request-changes\`;\`round\`: 整数(本轮 = ${round})。`,
`- \`issues\`: 结构化 must-fix 数组。\`approve\` 时必须为空数组 \`[]\`;\`request-changes\` 时**必须非空**,每项形如 \`{ "summary": "<一句问题>", "locator": "<文件路径或 file:line>", "severity": "blocker|high|medium|low" }\`。`,
`- \`locator\` **必须含可定位文件路径**(项目根相对,例如 \`backend/src/main/java/.../FooController.java\` 或 \`frontend/src/views/Bar.vue:42\`);没有具体文件无法定位 → 该项不是 must-fix(降级为口头建议,不要塞进 issues)。`,
`- 渲染审阅报告写入 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md\`(\`verdict\` 字段与返回值一致)。报告内可写更丰富的建议 / 风险 / 亮点;issues 数组只放硬性 must-fix。`,
`- **不要**在本步骤里编辑 docs/08 的 \`- [ ]\` checkbox——该 side effect 由上层 Workflow 的 micro step 在 approve 后另行落盘(你只负责裁决)。`,
'- 不要返回额外字段(schema 是 `additionalProperties:false`)。',
'',
commitBlock(`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md`,
`docs(review:${id}:r${round}): <verdict>`,
'- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。'),
].filter(Boolean).join('\n')
}
// ---- stage 5b:按 review must-fix 修复并重新 commit(review 循环的 fix 步)----
// issues:结构化对象数组 {summary, locator, severity}(见 REVIEW_SCHEMA)。
function fixPrompt(id, phase, issues) {
const fe = isFrontend(phase)
const list = Array.isArray(issues) && issues.length
? issues.map((x, i) => ` ${i + 1}. [${x.severity}] ${x.summary} — locator: \`${x.locator}\``).join('\n')
: ' (上一轮 review 未提供 must-fix 清单——不应出现,调用方会先 halt)'
return [
`# ${fe ? 'fe-feature' : 'feature'} fix — 修复 review must-fix ${id}`,
'',
featureStageContract(phase),
'',
'## 待修复 must-fix(已结构化)',
list,
'',
'## 流程',
'- 逐项编辑 locator 指向的代码文件(遵守阶段路径作用域护栏)。',
`- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT} cat-file -e HEAD:<locator 的文件部分>\`(locator 形如 \`path:line\` 时取 \`path\`)。文件不存在 → halt,把 locator 写进 reason,不要"修一个不存在的文件"。`,
`- 修复后 commit:\`fix(<scope>): 修复 review must-fix ${fe ? `FE: ${id}` : `REQ: ${id}`}\`(不混合无关改动)。`,
'- 修复完成后本步骤即结束;上层 Workflow 会重新跑 verify + review(下一轮)。',
'',
'## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
`- 全部修完:\`{ "status": "ok", "summary": "<已修复 ${Array.isArray(issues) ? issues.length : 0} 项的 1-2 句摘要>" }\`。`,
'- 任意阻塞(locator 文件不存在 / 越界 / 缺值)→ `{ "status": "halt", "reason": "<具体阻塞点 + locator>" }`。',
].filter(Boolean).join('\n')
}
// ---- 测试闸(原 test-gate)----
// attempt:1 = 首次跑;2 = 上轮 red 后的 flake 重试。每次 attempt 写到独立证据文件,避免 retry
// 把首次 red 证据覆盖掉(report § ⑤ 失去 flake 信号)。
function gatePrompt(module, phase, attempt = 1) {
const fe = isFrontend(phase)
const id = module?.id ?? '<module>'
const phaseId = fe ? 'frontend-phase' : id
return [
`# test-gate — ${fe ? '前端阶段' : `模块 ${id}`} 硬测试闸(phase=${phase}, attempt=${attempt})`,
'',
featureStageContract(phase),
'',
'## 目标',
`打里程碑 tag 前的唯一硬测试门。**派发 Agent 子会话**跑测试,绿则通过,红则失败。**绝不**在主会话直接跑测试,红色时**绝不**跳过。`,
attempt > 1 ? `- 本次 = 第 ${attempt} 次(上一次 red,本轮用于辨识 flaky);证据**写到独立文件**不要覆盖前一次。` : '',
'',
'## 命令',
fe
? `- 前端:命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 拼接(缺失则 \`pnpm test:ci && pnpm e2e:ci\`),跑 vitest + playwright(含全 FE 回归)。`
: `- 后端:跑 \`${ROOT}/scripts/test.mjs\`(跨平台 Node 测试入口;含本模块新增 + 已合并模块回归)。`,
'- 子会话只返回结构化 JSON:`{command, exit_code, passed, failed, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行含 FAIL 摘要)。',
'',
'## 证据 + commit',
`- 渲染证据写入 \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r${attempt}.md\` 并 commit 到当前分支(每个 attempt 独立文件,retry 不覆盖前一次 red 证据)。`,
`- 文件头注明 \`attempt: ${attempt}\` + 命令 + 时间窗口(如可从子会话拿到),便于 report § ⑤ 识别 flake。`,
'',
'## 输出(必须符合下发的 GATE JSON schema)',
'- `status`: `green`(`exit_code = 0` 且 `failed = 0`)| `red`;`failures`: 失败用例摘要(green 时可省略 / 空数组)。',
'- 不要返回额外字段。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。',
].filter(Boolean).join('\n')
}
// ---- 微步骤 prompt builders(runBranchSetup / runMilestone / runCrossModule 用)----
// 每个 prompt 单职责、短文本;返回严格 schema;执行(action)步统一返回 ACTION_RESULT_SCHEMA。
function microStepContract() {
return [
'## 硬约束(非交互子代理)',
'- 你是 Workflow 派生的**非交互子代理**,绝不弹问。',
'- 全部输出**使用中文**。',
`- 项目根 = \`${ROOT}\`。所有 git 命令必须用 \`git -C ${ROOT} ...\`;Read/Edit/Write 的路径都以 \`${ROOT}\` 为根。`,
'- 严格按下方"输出"段返回 schema 字段;**不要**在 schema 外追加自由叙述。',
].join('\n')
}
// ── 微步骤:可重用 read(多个 orchestrator 共用)──
function detectDefaultBranchPromptM() {
return [
'# 检测本地默认分支',
microStepContract(),
'',
`用 \`git -C ${ROOT} rev-parse --verify refs/heads/<name>\` 依次试 \`main\` / \`master\`,取第一个 exit=0 的为默认分支。`,
'## 输出(DEFAULT_BRANCH_SCHEMA)',
'- 两者其一存在:`{ "branch": "main" }` 或 `{ "branch": "master" }`',
'- 都不存在:本步骤失败(返回 schema 失败即可,调用方会 halt)。',
].join('\n')
}
function worktreeCleanPromptM() {
return [
'# 检查工作树是否干净',
microStepContract(),
'',
`跑 \`git -C ${ROOT} status --porcelain\`,按行解析 dirty 文件路径(第 4 字符起)。`,
'## 输出(WT_SCHEMA)',
'- 干净:`{ "clean": true }`',
'- 不干净:`{ "clean": false, "dirty": ["<path>", ...] }`',
].join('\n')
}
function checkBranchExistsPromptM(branch) {
return [
`# 本地分支 \`${branch}\` 是否存在`,
microStepContract(),
'',
`跑 \`git -C ${ROOT} rev-parse --verify refs/heads/${branch}\`(用 2>/dev/null 抑制 stderr)。`,
'## 输出(EXISTS_SCHEMA)',
'- exit=0 → `{ "exists": true }`;非 0 → `{ "exists": false }`',
].join('\n')
}
function currentBranchPromptM() {
return [
'# 当前所在分支',
microStepContract(),
'',
`跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。`,
'## 输出(BRANCH_NAME_SCHEMA)',
'- `{ "branch": "<stdout 第一行去空白>" }`',
].join('\n')
}
// ── 微步骤:分支生命周期 action ──
function checkoutExistingBranchPromptM(branch) {
return [
`# 切到已存在的本地分支 \`${branch}\``,
microStepContract(),
'',
`跑 \`git -C ${ROOT} checkout ${branch}\`。`,
'## 输出(ACTION_RESULT_SCHEMA)',
'- 成功:`{ "success": true }`',
'- 失败:`{ "success": false, "error": "<stderr 摘要>" }`',
].join('\n')
}
function createBranchFromPromptM(fromBranch, newBranch) {
return [
`# 从 \`${fromBranch}\` 新建并切到 \`${newBranch}\``,
microStepContract(),
'',
`按序跑:\`git -C ${ROOT} checkout ${fromBranch}\`,然后 \`git -C ${ROOT} checkout -b ${newBranch}\`。`,
'## 输出(ACTION_RESULT_SCHEMA)',
'- 全成功:`{ "success": true }`;任一失败:`{ "success": false, "error": "<which step + stderr>" }`',
].join('\n')
}
// ── 微步骤:REQ/FE 完成态 git tag(featureLoop dedup 的唯一 ground truth)──
// req-done/<id> 是功能级 git tag,approve 时打一次;featureLoop 入口先 check,存在就 skip,
// 避免 Router LLM 自审失误导致已 approve 的 REQ 被重新 spec→plan→tdd(撞 V<n>、污染源码)。
function checkReqDoneTagPromptM(id) {
return [
`# tag \`req-done/${id}\` 是否存在(功能级 dedup 真值)`,
microStepContract(),
'',
`跑 \`git -C ${ROOT} tag -l req-done/${id}\`。`,
'## 输出(EXISTS_SCHEMA)',
'- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`',
].join('\n')
}
function createReqDoneTagPromptM(id, phase) {
return [
`# 打 annotated tag \`req-done/${id}\`(${phase==='frontend'?'前端 FE':'后端 REQ'} approve 后落地)`,
microStepContract(),
'',
`跑 \`git -C ${ROOT} tag -a req-done/${id} -m "feature(${id}): approved by code-reviewer (phase=${phase})"\`。`,
`先用 \`git -C ${ROOT} tag -l req-done/${id}\` 检查;已存在则视为成功(幂等)直接返回 success。`,
'## 输出(ACTION_RESULT_SCHEMA)',
'- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`',
].join('\n')
}
// ── 微步骤:milestone 专用 ──
function checkAlreadyMergedPromptM(branch, defaultBranch) {
return [
`# \`${branch}\` 是否已合入 \`${defaultBranch}\``,
microStepContract(),
'',
`先跑 \`git -C ${ROOT} checkout ${defaultBranch}\` 确保 HEAD 在 ${defaultBranch};然后跑 \`git -C ${ROOT} merge-base --is-ancestor ${branch} HEAD\`。`,
'## 输出(ALREADY_MERGED_SCHEMA)',
'- 第二条 exit=0 → `{ "alreadyMerged": true }`(功能分支已是 HEAD 祖先,无需再 merge)',
'- 非 0 → `{ "alreadyMerged": false }`',
'- checkout 自身失败 → 整步失败(schema 失败即可)。',
].join('\n')
}
function executeMergePromptM(defaultBranch, branch, phaseId) {
return [
`# 把 \`${branch}\` 合并进 \`${defaultBranch}\`(已确认尚未合入,已在默认分支)`,
microStepContract(),
'',
`跑 \`git -C ${ROOT} merge --no-ff ${branch} -m "merge(${phaseId}): integrate ${branch}"\`。`,
'- 成功 → `{ "success": true }`',
'- 合并冲突 / 其它失败 → `{ "success": false, "error": "<simplified message>", "detail": "<conflict files newline-separated, 或 stderr 前 30 行>" }`',
'- **不要**自动 \`git merge --abort\` / 自动 stash / 自动改文件——把树留给人工处理。',
'## 输出(ACTION_RESULT_SCHEMA)',
].join('\n')
}
function readDocs08FieldPromptM(fe, id) {
const section = fe ? '§ 三' : '§ 二'
const title = fe
? '# 读 docs/08 § 三 `整体里程碑:` 字段当前值'
: `# 读 docs/08 § 二 模块 \`${id}\` 的 \`里程碑:\` 字段当前值`
const locator = fe
? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`- 整体里程碑: <value>\` 行。`
: `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section} 中 module id == \`${id}\` 的 bullet 段,取其 \` - 里程碑: <value>\` 子项。`
const missing = fe
? '- § 三 或该行不存在:`{ "found": false, "value": "" }`'
: `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\``
return [
title,
microStepContract(),
'',
locator,
'## 输出(FIELD_VALUE_SCHEMA)',
'- 命中:`{ "found": true, "value": "<冒号后去空白的当前值>", "lineNumber": <行号> }`',
missing,
].join('\n')
}
function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) {
const scope = fe ? `§ 三 整体里程碑` : `§ 二 模块 ${id} 里程碑`
const oldStr = fe ? '- 整体里程碑: —' : ' - 里程碑: —'
const newStr = fe ? `- 整体里程碑: ${targetValue}` : ` - 里程碑: ${targetValue}`
// 后端模块多个 bullet 同时含 ` - 里程碑: —`:必须按调用方传入的精确行号定位,否则在多模块 docs/08
// 里 Edit 会替换到第一处(通常不是本模块),把别的模块误标 milestone-complete。
const lineGuard = (typeof lineNumber === 'number' && Number.isFinite(lineNumber))
? `先 Read \`${ROOT}/docs/08-模块任务管理.md\` 第 ${lineNumber} 行(1-based),确认该行字面量等于 \`${oldStr}\`;不等则 halt(返回 \`{success:false, error:"line-${lineNumber}-mismatch: actual=<actual>"}\`)。然后仅替换第 ${lineNumber} 行;其余位置同名行**严禁**改动。`
: `严禁全局替换:通过定位上下文(${fe ? '§ 三' : `§ 二 中 module_id == \`${id}\` 的 bullet 段`})找到该 bullet 的 \`里程碑\` 子项行,仅替换这一行。`
return [
`# 把 docs/08 ${scope} 从 \`—\` 改为 \`${targetValue}\` 并 commit`,
microStepContract(),
'',
`调用方已确认字段当前值 = \`—\`(你不必再读一遍)。`,
`1. ${lineGuard} Edit \`${ROOT}/docs/08-模块任务管理.md\`:把整行 \`${oldStr}\` 替换为 \`${newStr}\`(精确字符串替换;**只动一处**)。`,
`2. 跑 \`git -C ${ROOT} add docs/08-模块任务管理.md\`。`,
`3. 跑 \`git -C ${ROOT} commit -m "chore(${phaseId}): record ${targetValue} in docs/08"\`。`,
'## 输出(ACTION_RESULT_SCHEMA)',
'- 三步全 OK:`{ "success": true }`;任一失败:`{ "success": false, "error": "<step + reason>" }`',
].join('\n')
}
// ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect;read-then-write)──
function readDocs08CheckboxPromptM(fe, id) {
const section = fe ? '§ 三' : '§ 二'
const kind = fe ? '功能' : 'REQ'
const locator = fe
? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。`
: `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section},找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。`
return [
`# 读 docs/08 ${section} ${kind} \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`,
microStepContract(),
'',
locator,
'## 输出(CHECKBOX_STATE_SCHEMA)',
`- 命中 \`- [x] ${id} ...\`:\`{ "found": true, "state": "checked", "lineNumber": <行号> }\``,
`- 命中 \`- [ ] ${id} ...\`:\`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``,
'- 找不到:`{ "found": false, "state": "unchecked" }`(state 仍必填,避免 schema 失败掩盖真实缺口)。',
].join('\n')
}
function writeDocs08CheckboxPromptM(fe, id, phase, lineNumber) {
const scope = fe ? `§ 三 功能 ${id}` : `§ 二 REQ ${id}`
const lineGuard = (typeof lineNumber === 'number' && Number.isFinite(lineNumber))
? `先 Read \`${ROOT}/docs/08-模块任务管理.md\` 第 ${lineNumber} 行(1-based),确认该行去掉行首空白后以 \`- [ ] ${id} \` 开头;不满足则返回 \`{success:false, error:"line-${lineNumber}-mismatch: actual=<actual>"}\`。然后只替换第 ${lineNumber} 行的第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。`
: `定位 docs/08 ${scope} 中去掉行首空白后以 \`- [ ] ${id} \` 开头的唯一一行,只替换该行第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。`
return [
`# 把 docs/08 ${scope} 的 \`[ ]\` 勾选为 \`[x]\` 并 commit`,
microStepContract(),
'',
`调用方已读到状态 = \`unchecked\`(你不必再读一遍)。`,
`1. ${lineGuard}`,
`2. 跑 \`git -C ${ROOT} add docs/08-模块任务管理.md\`。`,
`3. 跑 \`git -C ${ROOT} commit -m "chore(${phase}:${id}): mark ${id} approved in docs/08"\`。`,
'## 输出(ACTION_RESULT_SCHEMA)',
'- 三步全 OK:`{ "success": true }`;任一失败:`{ "success": false, "error": "<step + reason>" }`',
].join('\n')
}
function checkTagExistsPromptM(tagName) {
return [
`# tag \`${tagName}\` 是否存在`,
microStepContract(),
'',
`跑 \`git -C ${ROOT} tag -l ${tagName}\`。`,
'## 输出(EXISTS_SCHEMA)',
'- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`',
].join('\n')
}
function createTagPromptM(phaseId, fe) {
return [
`# 打 annotated tag \`milestone/${phaseId}\``,
microStepContract(),
'',
`跑 \`git -C ${ROOT} tag -a milestone/${phaseId} -m "milestone(${phaseId}): ${fe ? '前端' : '后端'}阶段完成"\`。`,
'## 输出(ACTION_RESULT_SCHEMA)',
'- 成功:`{ "success": true }`;失败:`{ "success": false, "error": "<stderr>" }`',
].join('\n')
}
function findReportPromptM(phaseId) {
return [
`# 找最新的 \`${phaseId}\` 完成报告并读取 § ⑫ 的 milestone tag 字段当前值`,
microStepContract(),
'',
`用 Glob 在 \`${ROOT}/docs/superpowers/module-reports/\` 查找 \`*-${phaseId}.md\`(按文件名 YYYY-MM-DD 日期前缀降序取最新一份)。`,
'Read 该文件,定位 § ⑫("里程碑"小节)。',
'## 输出(REPORT_PATH_SCHEMA)',
`- 找到:\`{ "found": true, "path": "docs/superpowers/module-reports/<file>", "currentTagValue": "<§ ⑫ 当前的字面值(应为 \\\`{{milestone_tag}}\\\` 或 \\\`milestone/${phaseId}\\\` 之一)>" }\``,
'- 完全没有匹配文件:`{ "found": false }`',
].join('\n')
}
function updateReportPromptM(reportPath, targetTag, phaseId) {
return [
`# 把 \`${reportPath}\` § ⑫ 的 \`{{milestone_tag}}\` 替换为 \`${targetTag}\` 并 commit`,
microStepContract(),
'',
`1. Edit \`${ROOT}/${reportPath}\`:把字面量 \`{{milestone_tag}}\` 替换为 \`${targetTag}\`(精确替换;如多处出现就全部替换)。`,
`2. \`git -C ${ROOT} add ${reportPath}\`;3. \`git -C ${ROOT} commit -m "docs(${phaseId}): record ${targetTag} in completion report"\`。`,
'## 输出(ACTION_RESULT_SCHEMA)',
'- 全 OK:`{ "success": true }`;失败:`{ "success": false, "error": "<which step + reason>" }`',
].join('\n')
}
// ── 微步骤:cross-module 专用 ──
function collectCrossModuleChangedPromptM(defaultBranch) {
return [
`# 收集功能分支自 \`${defaultBranch}\` 分叉以来的全部改动文件`,
microStepContract(),
'',
`跑 \`git -C ${ROOT} diff --name-status ${defaultBranch}...HEAD\`(三点 diff)。按行解析每行 \`<status>\\t<path>\`(status 通常为 M/A/D/R/C 等)。`,
'## 输出(CHANGED_FILES_SCHEMA)',
'- `{ "files": [ { "status": "M", "path": "backend/.../X.java" }, ... ] }`',
'- diff 为空 → `{ "files": [] }`',
].join('\n')
}
function classifyCrossModulePromptM(moduleId, files) {
const filesText = files.map(f => `- ${f.status} ${f.path}`).join('\n')
return [
`# 把改动文件分类:哪些落在**非本模块 \`${moduleId}\`** 的目录下`,
microStepContract(),
'',
`本模块目录归属请以 \`${ROOT}/docs/09-项目目录结构.md\` 与 \`${ROOT}/docs/08-模块任务管理.md § 二\` 中本模块 bullet 的 \`路径:\` 字段为准。Read 这两份文档以建立"路径 → 模块"映射。`,
'',
'## 改动文件清单',
filesText,
'',
'## 判定规则',
`- 落在本模块路径(\`${moduleId}\`)下 → **不算**跨模块。`,
'- 落在其它模块路径下 → 算跨模块,给出该文件归属的目标模块 id。',
'- 落在共享根(如 `docs/`、`scripts/`、`sql/migrations/`、`README.md` 等)→ **不算**跨模块。',
'',
'## 输出(CROSS_CLASSIFY_SCHEMA)',
'- `{ "crossModule": [ { "file": "...", "targetModule": "module_x", "reason": "<本模块哪个 REQ-XXX-NNN 迫使改它,1 句>", "impact": "<目标模块哪些 API/行为/调用方/测试受影响,1-3 句>" }, ... ] }`',
'- 无跨模块改动:`{ "crossModule": [] }`',
'- **不要留 `TBD(CC 补)`**:本步骤就是补齐的唯一时机;推不出原因 / 影响 → 整步失败(schema 失败即可,调用方会 halt)。',
].join('\n')
}
// dedup-and-rewrite 不再 append:resume / 多次跑同一模块时,append 会产生重复行污染 § ⑦。
// 改为整体重写:读现有行 → 与本次 items 合并 → 按 (file, targetModule) dedup(本次 items 覆盖旧值)
// → 按 (targetModule, file) 排序 → 整表重写。commit 前用 `git diff --quiet` 判定,无变更则跳过 commit。
function writeCrossModuleLogPromptM(moduleId, items) {
const newRowsJson = JSON.stringify(items, null, 2)
return [
`# 把跨模块改动以 dedup-and-rewrite 方式写入 \`docs/superpowers/module-reports/${moduleId}-cross-module.md\``,
microStepContract(),
'',
`目标文件(项目根相对):\`docs/superpowers/module-reports/${moduleId}-cross-module.md\`。`,
'',
'## 流程',
`1. **读现有行**:如果文件存在,用 Read 取出表格内已有的数据行(跳过表头与分隔行)。把每行解析为 \`{ file, targetModule, reason, impact }\`,得到 \`existingRows\`。文件不存在 → \`existingRows = []\`。`,
'2. **合并 + dedup**:把"本次新增行 JSON"中的项加入 `existingRows`,按 `(file + "\\u0001" + targetModule)` 作为 dedup key——**本次新增项覆盖旧项**(同一 file × targetModule 的最新原因 / 影响为准)。',
'3. **排序**:按 `(targetModule, file)` 字典序升序。',
'4. **整体重写**:用 Write 把整个文件重写为:',
' ```',
' # 跨模块改动日志',
' ',
' | 文件 | 目标模块 | 原因 | 影响 |',
' |---|---|---|---|',
' <已排序的全部行>',
' ```',
`5. **空变更跳过 commit**:跑 \`git -C ${ROOT} diff --quiet -- docs/superpowers/module-reports/${moduleId}-cross-module.md\`。`,
' - exit_code = 0(无变更)→ 不要 commit,直接返回 `{ "success": true, "detail": "no-diff-skip-commit" }`。',
` - exit_code != 0(有变更)→ \`git add\` + \`git commit -m "chore(${moduleId}): record cross-module log"\`。`,
'',
'## 本次新增行(JSON,作为合并输入)',
'```json',
newRowsJson,
'```',
'',
'## 输出(ACTION_RESULT_SCHEMA)',
'- 写成功且有/无 commit:`{ "success": true, "detail": "<written|no-diff-skip-commit>" }`',
'- 任一步失败:`{ "success": false, "error": "<step + reason>" }`',
].join('\n')
}
// ---- 模块完成报告(原 module-report)----
function reportPrompt(module) {
const id = module?.id ?? '<module>'
const fe = id === 'frontend-phase'
const phaseId = fe ? 'frontend-phase' : id
return [
`# module-report — ${fe ? '前端阶段' : `模块 ${id}`} 12 节完成报告`,
'',
featureStageContract(fe ? 'frontend' : 'backend'),
'',
'## 目标',
`test-gate 绿后渲染标准化 **12 节**完成报告,commit 到当前分支(供 milestone 标记)。**只读 git 摘要,不读 diff 正文进上下文。**`,
'',
'## 前置',
`- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`,
'',
'## 收集输入(取摘要而非正文)',
fe
? [
'- § ① `module_id = frontend-phase`,`module_name = 前端阶段(整体)`。',
`- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`,
`- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`,
'- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。',
`- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。`,
'- § ⑧ 偏离清单:额外审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出。',
'- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。',
].join('\n')
: [
`- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\` / \`--name-status\` / \`git log <默认分支>..HEAD --oneline\`(区间 = 功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)。`,
`- § ② / § ⑨:读 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-<本模块 REQ>.md\`。`,
`- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/${id}-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。`,
`- § ⑥ Migration:\`git -C ${ROOT} diff --name-only --diff-filter=A -- 'sql/migrations/V*.sql'\` 列新增,每个读第一行作说明。`,
`- § ⑦ 跨模块改动:读 \`${ROOT}/docs/superpowers/module-reports/${id}-cross-module.md\`(如存在;其中不应再有 \`TBD(CC 补)\`,上一步 cross-module-log 已补齐)。`,
'- § ④ 读写的表:grep 定位涉 SQL 文件后按需读片段,**不全量读 docs/03**。',
].join('\n'),
'',
'## 渲染 + 验证 + commit',
'- 渲染 12 节。硬验证:§ ⑧ 必须列举所有偏离(无则写"无偏离")。',
`- 写入 \`docs/superpowers/module-reports/<当天日期 YYYY-MM-DD>-${phaseId}.md\`(项目根相对;resume 时复用已存在的 \`*-${phaseId}.md\` 最新日期前缀,不要起新文件),连同跨模块日志(如存在)一起 commit 到当前分支(milestone 的 worktree-clean 前置依赖此 commit)。`,
'',
'## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
`- 成功:\`{ "status": "ok", "artifactPath": "docs/superpowers/module-reports/YYYY-MM-DD-${phaseId}.md", "summary": "<1-2 句中文摘要:测试是否 flake / 主要变更 / 是否有偏离>" }\`。`,
'- 失败:`{ "status": "halt", "reason": "<阻塞点(如最后一次 test-gate red / 跨模块日志缺失)>" }`。',
].join('\n')
}
// ---- runBranchSetup:原 branchSetupPrompt 的散文流程 → JS 编排 + 微步骤 agent ----
// 幂等:分支已存在则 checkout,否则从默认分支新建。条件分支由 JS 判定,子代理只负责执行单一动作。
async function runBranchSetup(module) {
const id = module?.id ?? '<module>'
const fe = id === 'frontend-phase'
const branch = fe ? 'frontend-phase' : `module-${id}`
const lbl = (k) => `branch:${k}:${id}`
const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA})
if (!wt.clean) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${(wt.dirty || []).join(', ')}`)
const exists = await agent(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA})
if (exists.exists) {
const r = await agent(checkoutExistingBranchPromptM(branch), {label: lbl('checkout'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
if (!r.success) throw new Error(`HALT branchSetup-checkout ${branch}: ${r.error || ''}`)
} else {
const r = await agent(createBranchFromPromptM(def.branch, branch), {label: lbl('create'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
if (!r.success) throw new Error(`HALT branchSetup-create ${branch}: ${r.error || ''}`)
}
const head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: HEAD on ${head.branch}`)
log(`branch-setup: ${id} → ${branch}`)
}
// ---- runMilestone:原 milestonePrompt 的 6 步散文流程 → JS 编排 ----
// 所有"已是目标态则跳过"的条件由 JS 在 read 结果上判定,子代理只执行确定性动作。
async function runMilestone(module) {
const id = module?.id ?? '<module>'
const fe = id === 'frontend-phase'
const phaseId = fe ? 'frontend-phase' : id
const branch = fe ? 'frontend-phase' : `module-${id}`
const targetTag = `milestone/${phaseId}`
const lbl = (k) => `milestone:${k}:${phaseId}`
// step 1: worktree clean precondition
const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA})
if (!wt.clean) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${(wt.dirty || []).join(', ')}`)
// step 2: detect default branch
const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
// step 3: merge (idempotent — skip if already an ancestor)
const merged = await agent(checkAlreadyMergedPromptM(branch, def.branch), {label: lbl('merged?'), phase: 'Milestone', schema: ALREADY_MERGED_SCHEMA})
if (!merged.alreadyMerged) {
const r = await agent(executeMergePromptM(def.branch, branch, phaseId), {label: lbl('merge'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
if (!r.success) throw new Error(`HALT milestone-merge ${phaseId}: ${r.error || ''}${r.detail ? '\n' + r.detail : ''}`)
}
// step 4: docs/08 field (idempotent — read first, only write if at initial '—')
const field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA})
if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: 字段不存在(docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`})`)
if (field.value === '—') {
const r = await agent(writeDocs08FieldPromptM(fe, id, targetTag, phaseId, field.lineNumber), {label: lbl('field-write'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
if (!r.success) throw new Error(`HALT milestone-docs08-write ${phaseId}: ${r.error || ''}`)
} else if (field.value !== targetTag) {
throw new Error(`HALT milestone-docs08-unexpected ${phaseId}: 字段当前 = ${JSON.stringify(field.value)}(行 ${field.lineNumber || '?'}),期望 '—' 或 '${targetTag}'`)
}
// else: 已是 targetTag → 静默跳过(续跑场景)
// step 5: report § ⑫ FIRST(关键顺序:tag 必须指向"§ ⑫ 已落地"的 commit,否则
// `git checkout milestone/<id>` 看到的报告 § ⑫ 仍是 placeholder。原版顺序 tag → § ⑫ 是已知 bug,
// 此处显式倒过来;下面 step 6 的 tag 才会指向新鲜 commit。)
const rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA})
if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: 没有找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件`)
if (rpt.currentTagValue === '{{milestone_tag}}') {
const r = await agent(updateReportPromptM(rpt.path, targetTag, phaseId), {label: lbl('report'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
if (!r.success) throw new Error(`HALT milestone-report-update ${phaseId}: ${r.error || ''}`)
} else if (rpt.currentTagValue !== targetTag) {
throw new Error(`HALT milestone-report-unexpected ${phaseId}: ${rpt.path} § ⑫ 当前 = ${JSON.stringify(rpt.currentTagValue)}`)
}
// else: 已是 targetTag → 静默跳过(resume 幂等)
// step 6: annotated tag (idempotent — tag exists 时静默跳过)
const tag = await agent(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA})
if (!tag.exists) {
const r = await agent(createTagPromptM(phaseId, fe), {label: lbl('tag'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
if (!r.success) throw new Error(`HALT milestone-tag ${phaseId}: ${r.error || ''}`)
}
log(`milestone: ${phaseId} → ${targetTag}`)
}
// ---- runCrossModule:原 crossModulePrompt 的"diff → 分类 → 写日志" → JS 编排 ----
// diff 和写文件是机械动作;"按 docs/09 路径归属判定哪些是跨模块"需要 LLM 判断,独立成一步。
async function runCrossModule(module) {
const id = module?.id ?? '<module>'
const lbl = (k) => `xmod:${k}:${id}`
const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
const changed = await agent(collectCrossModuleChangedPromptM(def.branch), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA})
if (!changed.files.length) {
log(`cross-module-log: 模块 ${id} 无文件改动,跳过`)
return
}
const classified = await agent(classifyCrossModulePromptM(id, changed.files), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA})
if (!classified.crossModule.length) {
log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`)
return
}
const r = await agent(writeCrossModuleLogPromptM(id, classified.crossModule), {label: lbl('write'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
if (!r.success) throw new Error(`HALT crossModule-write ${id}: ${r.error || ''}`)
log(`cross-module-log: 模块 ${id} 更新 ${classified.crossModule.length} 行`)
}
// ============================================================================
// 编排逻辑(结构按 plan 骨架;featureLoop / reviewWithFixLoop / testGate / 顶层循环)
// ============================================================================
// ---- 单功能链(后端 / 前端同构)----
// **顺序 for-await**(不是 pipeline)。理由:
// - tdd / fix stage 会编辑共享工作树并 git commit;并发会争 .git/index.lock、撞 migration V<n>。
// - pipeline 的"stage throw → item 掉 null、pipeline 永不 reject"语义会吞掉 reviewWithFixLoop /
// verify / tdd 的 HALT throw,让模块主循环 try/catch 捕获不到,残缺模块照样被推进到 milestone。
// 顺序 for-await 让 throw 自然冒泡到主循环 try → catch → break,使 fail-fast 真正生效。
//
// 派生 stage 全部 schema 化:spec/plan/tdd/verify/fix 共用 STAGE_RESULT_SCHEMA,
// sub-agent 写 `{status:'halt', reason}` 时 JS 立即抛 HALT,让"无法继续"不再混入"成功返回"。
// 功能级 dedup 真值 = `req-done/<id>` git tag:featureLoop 入口先 check,存在则 skip(Router 文档/
// LLM 自审失误不再导致已 approve 的 REQ 被重新 spec→plan→tdd 污染源码 / 撞 V<n>)。
//
// 语义边界(重要):`req-done/<id>` 表示"该功能在写 tag 时已通过 reviewer approve",**不**表示
// "实现自此再未变化"。若 testGate / 后续模块工作中人工或子代理改动了已 approve 功能的代码,重跑
// coding-start 时此 dedup 会跳过 spec/plan/tdd/verify/review,**不会**再次审阅这些后期改动。
// 这是有意的设计:避免在共享工作树里因为别的模块的 cross-cut 改动反复重跑前面所有 REQ。
// 若需要"approve 后改动必须再次走 review"的语义,请在改动前手动删除对应 `req-done/<id>` tag。
async function featureLoop(items, phase) {
const grp = phase === 'backend' ? 'Backend' : 'Frontend'
for (const id of items) {
// 入口 dedup:req-done/<id> 已存在 → 已 approve,整段 skip。
const done = await agent(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA})
if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue }
const spec = await agent(deriveSpecPrompt(id, phase), {label:`spec:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA})
if (spec.status === 'halt') throw new Error(`HALT spec ${phase}:${id}: ${spec.reason || ''}`)
if (!spec.artifactPath) throw new Error(`HALT spec-no-artifactPath ${phase}:${id}: spec returned ok but no artifactPath`)
// 日期一致性自校验:spec 文件名首段必须可被解析为 YYYY-MM-DD(dateFromArtifactPath 会抛)。
dateFromArtifactPath(spec.artifactPath)
const plan = await agent(planPrompt(id, phase, spec.artifactPath), {label:`plan:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA})
if (plan.status === 'halt') throw new Error(`HALT plan ${phase}:${id}: ${plan.reason || ''}`)
if (!plan.artifactPath) throw new Error(`HALT plan-no-artifactPath ${phase}:${id}`)
if (dateFromArtifactPath(plan.artifactPath) !== dateFromArtifactPath(spec.artifactPath)) {
throw new Error(`HALT plan-date-mismatch ${phase}:${id}: plan ${plan.artifactPath} 与 spec ${spec.artifactPath} 日期前缀不一致`)
}
const impl = await agent(tddPrompt(id, phase, plan.artifactPath), {label:`tdd:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA})
if (impl.status === 'halt') throw new Error(`HALT tdd ${phase}:${id}: ${impl.reason || ''}`)
const v0 = await agent(verifyPrompt(id, phase, impl.summary || '', spec.artifactPath, 0), {label:`verify:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA})
if (v0.status === 'halt') throw new Error(`HALT verify ${phase}:${id}: ${v0.reason || ''}`)
const reviewResult = await reviewWithFixLoop(id, phase, v0, spec.artifactPath)
log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`)
// approve 后落地 dedup 真值:req-done/<id> tag。
const tagR = await agent(createReqDoneTagPromptM(id, phase), {label:`reqdone:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA})
if (!tagR.success) throw new Error(`HALT req-done-tag ${phase}:${id}: ${tagR.error || ''}`)
}
}
// 有界 5 轮修复;超出 → throw(终止态,非对话框)。approve 后独立 micro step flip docs/08 checkbox。
async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
const grp = phase === 'backend' ? 'Backend' : 'Frontend'
const fe = isFrontend(phase)
let lastVerify = verifyResult
let lastIssuesCount = 0
for (let round = 1; round <= 5; round++) {
const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || ''
// opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。
const r = await agent(
reviewPrompt(id, phase, round, lastVerifySummary, specPath),
{label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'code-reviewer'}
)
if (r.verdict === 'approve') {
const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA})
if (!cb.found) throw new Error(`HALT docs08-checkbox-missing ${phase}:${id}: docs/08 ${fe?'§ 三':'§ 二'} 中找不到 \`- [ ] ${id} ...\` / \`- [x] ${id} ...\` 行`)
if (cb.state !== 'checked' && cb.state !== 'unchecked') {
throw new Error(`HALT docs08-checkbox-state-invalid ${phase}:${id}: cb.state = ${JSON.stringify(cb.state)}`)
}
if (cb.state === 'unchecked') {
const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA})
if (!wr.success) throw new Error(`HALT docs08-checkbox-write ${phase}:${id}: ${wr.error || ''}`)
}
return { id, phase, approved:true, rounds:round }
}
// request-changes 必须带 must-fix 清单(结构化对象数组);否则 fix 步无法定位 → 直接 halt 暴露 reviewer 契约违例。
if (!Array.isArray(r.issues) || r.issues.length === 0) {
throw new Error(`HALT review-empty-issues ${phase}:${id} r${round}: reviewer 返回 request-changes 但 issues 为空,无法驱动 fix 步`)
}
lastIssuesCount = r.issues.length
// 每个 issue 必须含 locator(locator 校验由 fix sub-agent 在 git cat-file 阶段再做一次硬把关)。
const missingLocator = r.issues.filter(x => !x || typeof x.locator !== 'string' || !x.locator.trim())
if (missingLocator.length) {
throw new Error(`HALT review-issue-no-locator ${phase}:${id} r${round}: ${missingLocator.length} 个 issue 缺 locator,reviewer 契约违例(issue summaries: ${missingLocator.map(x=>x?.summary||'').join(' | ')})`)
}
const fixR = await agent(fixPrompt(id, phase, r.issues), {label:`fix:${phase}:${id}:r${round}`, phase: grp, schema: STAGE_RESULT_SCHEMA})
if (fixR.status === 'halt') throw new Error(`HALT fix ${phase}:${id} r${round}: ${fixR.reason || ''}`)
lastVerify = await agent(
verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验,上轮 must-fix: ${r.issues.length} 项)`, specPath, round),
{label:`reverify:${phase}:${id}:r${round}`, phase: grp, schema: STAGE_RESULT_SCHEMA}
)
if (lastVerify.status === 'halt') throw new Error(`HALT reverify ${phase}:${id} r${round}: ${lastVerify.reason || ''}`)
}
throw new Error(`HALT review-unresolved ${phase}:${id}: 5 轮 review 仍未 approve(最后一次 reverify ${lastVerify?.status || '?'},最后一轮 must-fix ${lastIssuesCount} 项)`)
}
// flake 重试 1 次:attempt=2 写到独立证据文件 `<id>-test-gate-r2.md`,不覆盖 r1 的 red 证据(report § ⑤ 用得到)。
async function testGate(module, phase) {
let g = await agent(gatePrompt(module, phase, 1), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})
if (g.status === 'red') { // 自动重试 1 次(防 flaky)
g = await agent(gatePrompt(module, phase, 2), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})
}
if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${(g.failures||[]).join('; ')}`)
return g
}
phase('Router')
const routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA})
// Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。
const ID_PATTERN = /^[A-Za-z0-9_-]+$/
function assertSafeId(kind, value) {
if (typeof value !== 'string' || !ID_PATTERN.test(value)) {
throw new Error(`HALT router-invalid-${kind}: ${JSON.stringify(value)}(必须匹配 /^[A-Za-z0-9_-]+$/,用于安全地拼入 git 命令)`)
}
}
for (const m of routed.modules) {
assertSafeId('module-id', m.id)
for (const r of m.reqs || []) assertSafeId('req-id', r)
for (const f of m.feItems || []) assertSafeId('fe-id', f)
const isFE = m.id === 'frontend-phase'
if (isFE && Array.isArray(m.reqs) && m.reqs.length) {
throw new Error(`HALT router-violation: frontend-phase 聚合模块的 reqs 必须为空,实测含 ${m.reqs.length} 项 (${m.reqs.join(',')})`)
}
if (!isFE && Array.isArray(m.feItems) && m.feItems.length) {
throw new Error(`HALT router-violation: 后端模块 ${m.id} 的 feItems 必须为空(前端只在 frontend-phase 聚合),实测含 ${m.feItems.length} 项 (${m.feItems.join(',')})`)
}
}
const todo = routed.modules.filter(m => !m.done)
log(`coding: ${todo.length}/${routed.modules.length} modules to run`)
const results = []
let haltedAtIdx = -1
for (const [idx, module] of todo.entries()) {
try {
phase('Milestone')
await runBranchSetup(module)
if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过)
phase('Backend')
await featureLoop(module.reqs, 'backend')
phase('Gate')
await testGate(module, 'backend')
phase('Milestone')
await runCrossModule(module) // 替代被删 hook,JS 编排:diff → 分类 → 写日志
}
if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块)
phase('Frontend')
await featureLoop(module.feItems, 'frontend')
phase('Gate')
await testGate(module, 'frontend')
}
phase('Milestone')
const rep = await agent(reportPrompt(module), {label:`report:${module.id}`, phase:'Milestone', schema: STAGE_RESULT_SCHEMA})
if (rep.status === 'halt') throw new Error(`HALT report ${module.id}: ${rep.reason || ''}`)
await runMilestone(module)
results.push({ module: module.id, status:'done' })
} catch (e) {
results.push({ module: module.id, status:'halted', reason: String(e.message||e) })
haltedAtIdx = idx
break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start
}
}
// pending:halt 后被跳过的剩余模块(M5)。caller / coding-start 可据此告知用户"修好后还有哪些待跑",
// 而不是仅看到一个 halted 模块就误以为只剩一个。
const pending = haltedAtIdx >= 0
? todo.slice(haltedAtIdx + 1).map(m => ({ module: m.id, status: 'pending' }))
: []
// Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表。
// 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行,
// 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。
// 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`,
// 否则 Workflow 拿不到 results / pending。
return { results, pending }