speccore 6.18.4 → 6.30.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.
Files changed (73) hide show
  1. package/.agents/skills/spec-execute/SKILL.md +7 -3
  2. package/dist/cli.js +1 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/analyze.d.ts +1 -0
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +234 -47
  7. package/dist/commands/analyze.js.map +1 -1
  8. package/dist/commands/context-output.d.ts.map +1 -1
  9. package/dist/commands/context-output.js +19 -10
  10. package/dist/commands/context-output.js.map +1 -1
  11. package/dist/commands/doc2spec.js +1 -1
  12. package/dist/commands/doc2spec.js.map +1 -1
  13. package/dist/commands/execute.d.ts.map +1 -1
  14. package/dist/commands/execute.js +373 -91
  15. package/dist/commands/execute.js.map +1 -1
  16. package/dist/commands/global-status.js +2 -2
  17. package/dist/commands/global-status.js.map +1 -1
  18. package/dist/commands/import.d.ts +1 -1
  19. package/dist/commands/import.js +2 -4
  20. package/dist/commands/import.js.map +1 -1
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +36 -282
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/iteration/create.js +7 -10
  25. package/dist/commands/iteration/create.js.map +1 -1
  26. package/dist/commands/iteration/split.js +296 -131
  27. package/dist/commands/iteration/split.js.map +1 -1
  28. package/dist/commands/iteration-from-global.js +1 -1
  29. package/dist/commands/retro.d.ts.map +1 -1
  30. package/dist/commands/retro.js +60 -3
  31. package/dist/commands/retro.js.map +1 -1
  32. package/dist/commands/status-panel.d.ts.map +1 -1
  33. package/dist/commands/status-panel.js +24 -8
  34. package/dist/commands/status-panel.js.map +1 -1
  35. package/dist/commands/synthesize.d.ts +2 -2
  36. package/dist/commands/synthesize.js +146 -196
  37. package/dist/commands/synthesize.js.map +1 -1
  38. package/dist/commands/task/new.js +43 -26
  39. package/dist/commands/task/new.js.map +1 -1
  40. package/dist/core/ai-context-generator.js +1 -1
  41. package/dist/core/analyze-engine.d.ts.map +1 -1
  42. package/dist/core/analyze-engine.js +307 -9
  43. package/dist/core/analyze-engine.js.map +1 -1
  44. package/dist/core/execution-state.d.ts +24 -0
  45. package/dist/core/execution-state.d.ts.map +1 -1
  46. package/dist/core/execution-state.js +135 -0
  47. package/dist/core/execution-state.js.map +1 -1
  48. package/dist/core/knowledge-graph.d.ts.map +1 -1
  49. package/dist/core/knowledge-graph.js +190 -65
  50. package/dist/core/knowledge-graph.js.map +1 -1
  51. package/dist/core/next-steps.js +2 -2
  52. package/dist/core/next-steps.js.map +1 -1
  53. package/dist/core/prompt-builder.d.ts.map +1 -1
  54. package/dist/core/prompt-builder.js +43 -22
  55. package/dist/core/prompt-builder.js.map +1 -1
  56. package/dist/core/quality-audit.d.ts +41 -0
  57. package/dist/core/quality-audit.d.ts.map +1 -0
  58. package/dist/core/quality-audit.js +345 -0
  59. package/dist/core/quality-audit.js.map +1 -0
  60. package/dist/core/rag-engine.d.ts.map +1 -1
  61. package/dist/core/rag-engine.js +38 -6
  62. package/dist/core/rag-engine.js.map +1 -1
  63. package/dist/core/spec-merger.d.ts.map +1 -1
  64. package/dist/core/spec-merger.js +22 -27
  65. package/dist/core/spec-merger.js.map +1 -1
  66. package/dist/core/validator.d.ts.map +1 -1
  67. package/dist/core/validator.js +43 -0
  68. package/dist/core/validator.js.map +1 -1
  69. package/dist/core/verify-engine.d.ts +1 -1
  70. package/dist/core/verify-engine.d.ts.map +1 -1
  71. package/dist/core/verify-engine.js +71 -8
  72. package/dist/core/verify-engine.js.map +1 -1
  73. package/package.json +1 -1
@@ -460,7 +460,7 @@ async function executeWithProgress(tasks, iteration, base, skip, options) {
460
460
  const taskDir = (0, path_1.join)(iterDirForRetro, '030-tasks', task.id);
461
461
  const taskCodePath = (await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, 'code'))) ? (0, path_1.join)(taskDir, 'code') : absCodePath;
462
462
  // 读取上一轮修复状态(如果有)
463
- const statePath = (0, path_1.join)(taskDir, '99-artifacts', '.verify-state.json');
463
+ const statePath = (0, path_1.join)(taskDir, '.verify-state.json');
464
464
  let currentRound = 1;
465
465
  if (await (0, fs_extra_1.pathExists)(statePath)) {
466
466
  try {
@@ -491,8 +491,6 @@ async function executeWithProgress(tasks, iteration, base, skip, options) {
491
491
  const MAX_ROUNDS = 3;
492
492
  if (currentRound < MAX_ROUNDS) {
493
493
  // 保存轮次状态 + 输出 [SPECCORE_EXEC] 让 AI 修复
494
- const { ensureDir } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
495
- await ensureDir((0, path_1.join)(taskDir, '99-artifacts'));
496
494
  await (0, fs_extra_1.writeFile)(statePath, JSON.stringify({ round: currentRound, taskId: task.id, timestamp: new Date().toISOString() }));
497
495
  logger_1.logger.info(` 🤖 请求 AI 修复(第 ${currentRound}/${MAX_ROUNDS} 轮)...`);
498
496
  (0, verify_engine_1.outputFixTag)(gate.report, taskDir, currentRound);
@@ -500,7 +498,7 @@ async function executeWithProgress(tasks, iteration, base, skip, options) {
500
498
  else {
501
499
  // 3 轮都失败
502
500
  logger_1.logger.error(` 💀 ${task.id}: ${MAX_ROUNDS} 轮修复后质量门禁仍未通过`);
503
- logger_1.logger.info(` 📄 报告: ${(0, path_1.join)(taskDir, '99-artifacts', 'VERIFY_REPORT.md')}`);
501
+ logger_1.logger.info(` 📄 报告: ${(0, path_1.join)(taskDir, 'VERIFY_REPORT.md')}`);
504
502
  logger_1.logger.info(` 💡 请人工检查并修复`);
505
503
  }
506
504
  }
@@ -608,7 +606,7 @@ async function processBatch(tasks, state, iteration) {
608
606
  for (const task of tasks) {
609
607
  logger_1.logger.info(` ${task.id}:`);
610
608
  const tDir = await resolveTaskDir(iterDir, task.id);
611
- for (const specPath of ['_shared/CONTEXT.md', '_shared/REQ.md', '_shared/TECH.md', '00-specs/REQ.md', '00-specs/TECH.md']) {
609
+ for (const specPath of ['00-specs/REQ.md', '00-specs/TECH.md', '00-specs/CONTEXT.md', '_shared/REQ.md', '_shared/TECH.md']) {
612
610
  const p = (0, path_1.join)(tDir, specPath);
613
611
  if (await (0, fs_extra_1.pathExists)(p)) {
614
612
  const content = await (0, fs_extra_1.readFile)(p, 'utf-8');
@@ -635,6 +633,18 @@ async function processBatch(tasks, state, iteration) {
635
633
  logger_1.logger.info(` 🔄 执行中...`);
636
634
  await generateTaskSkeleton(task, iteration);
637
635
  completed.push(task.id);
636
+ // 写入任务摘要到 execution-state(文件即记忆)
637
+ const taskSummary = {
638
+ taskId: task.id,
639
+ taskName: task.name || task.id,
640
+ type: task.type || 'feature',
641
+ status: 'completed',
642
+ summary: `${task.name || task.id} 骨架已生成`,
643
+ outputs: ['00-specs/', '10-backend/', '20-frontend/'],
644
+ dependencies: task.dependencies || [],
645
+ completedAt: new Date().toISOString(),
646
+ };
647
+ (0, execution_state_1.addTaskSummary)(state, taskSummary);
638
648
  logger_1.logger.info(` ✅ ${task.id || task} completed`);
639
649
  const elapsed = Math.round((Date.now() - startTime) / 1000);
640
650
  const estRemaining = Math.round((elapsed / (i + 1)) * (total - i - 1));
@@ -642,8 +652,11 @@ async function processBatch(tasks, state, iteration) {
642
652
  }
643
653
  // Mark batch complete
644
654
  (0, execution_state_1.completeBatch)(state, batchNum, completed);
655
+ // 写入上下文摘要文件(供新会话快速恢复)
656
+ const summaryPath = await (0, execution_state_1.writeContextSummaryFile)(state);
645
657
  logger_1.logger.info(``);
646
658
  logger_1.logger.info(`✅ Batch ${batchNum} complete (${completed.length} tasks)`);
659
+ logger_1.logger.info(`📄 上下文摘要: ${summaryPath}`);
647
660
  // Context reset note
648
661
  logger_1.logger.info(`🔄 Resetting context for next batch...`);
649
662
  logger_1.logger.info(``);
@@ -687,12 +700,35 @@ async function generateTaskSkeleton(task, iteration) {
687
700
  const specRules = await (0, spec_rules_1.loadSpecRules)();
688
701
  const techStack = await (0, spec_rules_1.loadTechStack)();
689
702
  logger_1.logger.info(` Tech Stack: ${techStack.backendFramework} + ${techStack.frontendFramework}`);
690
- // 读取后端 Spec 生成代码骨架
691
- const backendDir = (0, path_1.join)(taskDir, '10-backend');
692
- if (await (0, fs_extra_1.pathExists)(backendDir)) {
693
- const reqPath = (await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '_shared', 'REQ.md')))
694
- ? (0, path_1.join)(taskDir, '_shared', 'REQ.md')
695
- : (0, path_1.join)(taskDir, '00-specs', 'REQ.md');
703
+ // 读取后端 Spec 生成代码骨架(扫描 10-backend/{服务}/{子任务}/ 三级嵌套)
704
+ const { readdir: rd } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
705
+ const taskEntries = await rd(taskDir, { withFileTypes: true });
706
+ const backendSubtaskDirs = [];
707
+ // 新结构: 10-backend/{服务名}/{子任务}/
708
+ const backendCategoryDir = (0, path_1.join)(taskDir, '10-backend');
709
+ if (await (0, fs_extra_1.pathExists)(backendCategoryDir)) {
710
+ const serviceEntries = await rd(backendCategoryDir, { withFileTypes: true });
711
+ for (const service of serviceEntries) {
712
+ if (!service.isDirectory())
713
+ continue;
714
+ const serviceDir = (0, path_1.join)(backendCategoryDir, service.name);
715
+ const subtaskEntries = await rd(serviceDir, { withFileTypes: true });
716
+ for (const st of subtaskEntries) {
717
+ if (st.isDirectory() && !st.name.startsWith('.')) {
718
+ backendSubtaskDirs.push((0, path_1.join)(serviceDir, st.name));
719
+ }
720
+ }
721
+ }
722
+ }
723
+ // 回退: 旧结构 10-*/ 直接在任务根目录
724
+ if (backendSubtaskDirs.length === 0) {
725
+ const legacy = taskEntries
726
+ .filter((d) => d.isDirectory() && d.name.startsWith('10-'))
727
+ .map((d) => (0, path_1.join)(taskDir, d.name));
728
+ backendSubtaskDirs.push(...legacy);
729
+ }
730
+ for (const backendDir of backendSubtaskDirs) {
731
+ const reqPath = (0, path_1.join)(taskDir, '00-specs', 'REQ.md');
696
732
  let className = convertToClassName(task.name || task.id);
697
733
  // 从 CONSTITUTION.md 读取包名,回退到默认值
698
734
  let packageName = `com.example.${className.toLowerCase()}`;
@@ -724,8 +760,8 @@ async function generateTaskSkeleton(task, iteration) {
724
760
  const repoPath = (0, path_1.join)(srcDir, `${className}Repository.java`);
725
761
  tx.write(repoPath, repoCode);
726
762
  filesUpdated++;
727
- // 更新 TASK.md 状态
728
- const taskMdPath = (0, path_1.join)(taskDir, '00-specs', 'TASK.md');
763
+ // 更新子任务 TASK.md 状态
764
+ const taskMdPath = (0, path_1.join)(backendDir, 'TASK.md');
729
765
  if (await (0, fs_extra_1.pathExists)(taskMdPath)) {
730
766
  const content = await (0, fs_extra_1.readFile)(taskMdPath, 'utf-8');
731
767
  const updated = content.replace('状态: 🔲 待开发', '状态: 🔄 进行中');
@@ -733,30 +769,47 @@ async function generateTaskSkeleton(task, iteration) {
733
769
  filesUpdated++;
734
770
  }
735
771
  }
736
- // 前端各平台代码生成
737
- const frontendDir = (0, path_1.join)(taskDir, '20-frontend');
738
- if (await (0, fs_extra_1.pathExists)(frontendDir)) {
739
- const { readdir: rd } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
740
- const platformDirs = await rd(frontendDir, { withFileTypes: true });
741
- for (const pd of platformDirs) {
742
- if (pd.isDirectory()) {
743
- const componentName = convertToClassName(task.name || task.id);
744
- const vueCode = generateVueComponent(componentName);
745
- const srcDir = (0, path_1.join)(frontendDir, pd.name, 'src');
746
- await (0, fs_extra_1.ensureDir)(srcDir);
747
- const vuePath = (0, path_1.join)(srcDir, `${componentName}.vue`);
748
- tx.write(vuePath, vueCode);
749
- filesUpdated++;
750
- const ftaskPath = (0, path_1.join)(frontendDir, pd.name, 'TASK.md');
751
- if (await (0, fs_extra_1.pathExists)(ftaskPath)) {
752
- const content = await (0, fs_extra_1.readFile)(ftaskPath, 'utf-8');
753
- const updated = content.replace('状态: 🔲 待开发', '状态: 🔄 进行中');
754
- tx.write(ftaskPath, updated);
755
- filesUpdated++;
772
+ // 前端各平台代码生成(扫描 20-frontend/{端名}/{子任务}/ 三级嵌套)
773
+ const frontendSubtaskDirs = [];
774
+ // 新结构: 20-frontend/{端名}/{子任务}/
775
+ const frontendCategoryDir = (0, path_1.join)(taskDir, '20-frontend');
776
+ if (await (0, fs_extra_1.pathExists)(frontendCategoryDir)) {
777
+ const platformEntries = await rd(frontendCategoryDir, { withFileTypes: true });
778
+ for (const plat of platformEntries) {
779
+ if (!plat.isDirectory())
780
+ continue;
781
+ const platformDir = (0, path_1.join)(frontendCategoryDir, plat.name);
782
+ const subtaskEntries = await rd(platformDir, { withFileTypes: true });
783
+ for (const st of subtaskEntries) {
784
+ if (st.isDirectory() && !st.name.startsWith('.')) {
785
+ frontendSubtaskDirs.push((0, path_1.join)(platformDir, st.name));
756
786
  }
757
787
  }
758
788
  }
759
789
  }
790
+ // 回退: 旧结构 20-*/ 直接在任务根目录
791
+ if (frontendSubtaskDirs.length === 0) {
792
+ const legacy = taskEntries
793
+ .filter((d) => d.isDirectory() && d.name.startsWith('20-'))
794
+ .map((d) => (0, path_1.join)(taskDir, d.name));
795
+ frontendSubtaskDirs.push(...legacy);
796
+ }
797
+ for (const frontendDir of frontendSubtaskDirs) {
798
+ const componentName = convertToClassName(task.name || task.id);
799
+ const vueCode = generateVueComponent(componentName);
800
+ const srcDir = (0, path_1.join)(frontendDir, 'src');
801
+ await (0, fs_extra_1.ensureDir)(srcDir);
802
+ const vuePath = (0, path_1.join)(srcDir, `${componentName}.vue`);
803
+ tx.write(vuePath, vueCode);
804
+ filesUpdated++;
805
+ const ftaskPath = (0, path_1.join)(frontendDir, 'TASK.md');
806
+ if (await (0, fs_extra_1.pathExists)(ftaskPath)) {
807
+ const content = await (0, fs_extra_1.readFile)(ftaskPath, 'utf-8');
808
+ const updated = content.replace('状态: 🔲 待开发', '状态: 🔄 进行中');
809
+ tx.write(ftaskPath, updated);
810
+ filesUpdated++;
811
+ }
812
+ }
760
813
  if (tx.length > 0) {
761
814
  await tx.commit();
762
815
  }
@@ -946,16 +999,40 @@ async function filterByPlatform(tasks, iteration, platform) {
946
999
  filtered.push(task);
947
1000
  continue;
948
1001
  }
949
- // 新结构: 用 resolveTaskDir 查找任务目录(支持类型子目录),再查 {platform}/
1002
+ // 新结构: 用 resolveTaskDir 查找任务目录(支持类型子目录),再查 10-backend/{}/ 或 20-frontend/{端}/
950
1003
  const taskDir = await resolveTaskDir(iterDir, task.id);
951
- const platformDir = (0, path_1.join)(taskDir, platform);
1004
+ const isBackend = platform === 'backend' || platform.startsWith('后台');
1005
+ const categoryDir = isBackend ? (0, path_1.join)(taskDir, '10-backend') : (0, path_1.join)(taskDir, '20-frontend');
1006
+ const serviceName = isBackend && platform === 'backend' ? 'api' : platform;
1007
+ // 新结构: 10-backend/{端}/ 或 20-frontend/{端}/
1008
+ const platformDir = (0, path_1.join)(categoryDir, serviceName);
952
1009
  if (await (0, fs_extra_1.pathExists)(platformDir)) {
953
1010
  filtered.push(task);
954
1011
  continue;
955
1012
  }
956
- // 旧结构回退: frontend/{platform}/
957
- const legacyDir = (0, path_1.join)(taskDir, 'frontend', platform);
958
- if (await (0, fs_extra_1.pathExists)(legacyDir))
1013
+ // 回退: 扫描 category 目录下是否有匹配的子目录
1014
+ try {
1015
+ if (await (0, fs_extra_1.pathExists)(categoryDir)) {
1016
+ const { readdir: readDir } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
1017
+ const entries = await readDir(categoryDir, { withFileTypes: true });
1018
+ const matched = entries.find((d) => d.isDirectory() && d.name === serviceName);
1019
+ if (matched) {
1020
+ filtered.push(task);
1021
+ continue;
1022
+ }
1023
+ }
1024
+ }
1025
+ catch { /* ignore */ }
1026
+ // 旧结构回退: 10-{端}/ 或 20-{端}/ 直接在任务根目录
1027
+ const subtaskPrefix = isBackend ? '10-' : '20-';
1028
+ const legacyDir = (0, path_1.join)(taskDir, `${subtaskPrefix}${serviceName}`);
1029
+ if (await (0, fs_extra_1.pathExists)(legacyDir)) {
1030
+ filtered.push(task);
1031
+ continue;
1032
+ }
1033
+ // 更旧结构回退: frontend/{platform}/
1034
+ const legacyDir2 = (0, path_1.join)(taskDir, 'frontend', platform);
1035
+ if (await (0, fs_extra_1.pathExists)(legacyDir2))
959
1036
  filtered.push(task);
960
1037
  }
961
1038
  return filtered;
@@ -1024,25 +1101,142 @@ async function preFlightCheck(tasks, iteration, options) {
1024
1101
  else {
1025
1102
  issues.push('缺少 TECH.md');
1026
1103
  }
1027
- // 3. Test cases
1028
- const testPath = (0, path_1.join)(taskDir, '99-artifacts', 'TEST.md');
1029
- if (await (0, fs_extra_1.pathExists)(testPath)) {
1030
- const test = await (0, fs_extra_1.readFile)(testPath, 'utf-8');
1031
- const n = (test.match(/⬜|✅|❌/g) || []).length;
1032
- logger_1.logger.info(` 3. 测试: ${n} 用例`);
1033
- }
1034
- // 4. Review
1035
- logger_1.logger.info(` 4. 审查: ${await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '99-artifacts', 'REVIEW.md')) ? '✅' : '❌'}`);
1036
- // 5. API
1037
- logger_1.logger.info(` 5. 契约: ${await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml')) ? '✅' : '⚠️'}`);
1038
- // 6. Platform
1039
- const fd = (0, path_1.join)(taskDir, '20-frontend');
1040
- if (await (0, fs_extra_1.pathExists)(fd)) {
1041
- const pf = (0, fs_1.readdirSync)(fd, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1042
- logger_1.logger.info(` 6. 端: ${pf.join(', ')}`);
1104
+ // 3. Test cases(扫描子任务目录)
1105
+ let testFound = false;
1106
+ for (const catDir of ['10-backend', '20-frontend']) {
1107
+ const catPath = (0, path_1.join)(taskDir, catDir);
1108
+ if (await (0, fs_extra_1.pathExists)(catPath)) {
1109
+ try {
1110
+ for (const svc of await (0, fs_extra_1.readdir)(catPath, { withFileTypes: true })) {
1111
+ if (!svc.isDirectory())
1112
+ continue;
1113
+ for (const sub of await (0, fs_extra_1.readdir)((0, path_1.join)(catPath, svc.name), { withFileTypes: true })) {
1114
+ if (!sub.isDirectory())
1115
+ continue;
1116
+ const tp = (0, path_1.join)(catPath, svc.name, sub.name, 'TEST.md');
1117
+ if (await (0, fs_extra_1.pathExists)(tp)) {
1118
+ const test = await (0, fs_extra_1.readFile)(tp, 'utf-8');
1119
+ const n = (test.match(/⬜|✅|❌/g) || []).length;
1120
+ logger_1.logger.info(` 3. 测试[${svc.name}/${sub.name}]: ${n} 用例`);
1121
+ testFound = true;
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ catch { /* 跳过 */ }
1127
+ }
1128
+ }
1129
+ if (!testFound) {
1130
+ // 旧结构回退
1131
+ const testPath = (0, path_1.join)(taskDir, '99-artifacts', 'TEST.md');
1132
+ if (await (0, fs_extra_1.pathExists)(testPath)) {
1133
+ const test = await (0, fs_extra_1.readFile)(testPath, 'utf-8');
1134
+ const n = (test.match(/⬜|✅|❌/g) || []).length;
1135
+ logger_1.logger.info(` 3. 测试: ${n} 用例`);
1136
+ }
1137
+ }
1138
+ // 4. Review(扫描子任务目录)
1139
+ let reviewFound = false;
1140
+ for (const catDir of ['10-backend', '20-frontend']) {
1141
+ const catPath = (0, path_1.join)(taskDir, catDir);
1142
+ if (await (0, fs_extra_1.pathExists)(catPath)) {
1143
+ try {
1144
+ for (const svc of await (0, fs_extra_1.readdir)(catPath, { withFileTypes: true })) {
1145
+ if (!svc.isDirectory())
1146
+ continue;
1147
+ for (const sub of await (0, fs_extra_1.readdir)((0, path_1.join)(catPath, svc.name), { withFileTypes: true })) {
1148
+ if (!sub.isDirectory())
1149
+ continue;
1150
+ if (await (0, fs_extra_1.pathExists)((0, path_1.join)(catPath, svc.name, sub.name, 'REVIEW.md'))) {
1151
+ logger_1.logger.info(` 4. 审查[${svc.name}/${sub.name}]: ✅`);
1152
+ reviewFound = true;
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ catch { /* 跳过 */ }
1158
+ }
1159
+ }
1160
+ if (!reviewFound) {
1161
+ logger_1.logger.info(` 4. 审查: ${await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '99-artifacts', 'REVIEW.md')) ? '✅' : '❌'}`);
1162
+ }
1163
+ // 5. API(00-specs/ 优先,_shared/ 回退)
1164
+ const hasApiContract = await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '00-specs', 'API_CONTRACT.yaml')) || await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'));
1165
+ logger_1.logger.info(` 5. 契约: ${hasApiContract ? '✅' : '⚠️'}`);
1166
+ // 6. Platform(扫描 10-backend/*/ 和 20-frontend/*/ 子任务目录)
1167
+ const subtaskDirs = [];
1168
+ for (const catDir of ['10-backend', '20-frontend']) {
1169
+ const catPath = (0, path_1.join)(taskDir, catDir);
1170
+ if (await (0, fs_extra_1.pathExists)(catPath)) {
1171
+ try {
1172
+ const platEntries = (0, fs_1.readdirSync)(catPath, { withFileTypes: true });
1173
+ for (const pe of platEntries) {
1174
+ if (pe.isDirectory()) {
1175
+ const stEntries = (0, fs_1.readdirSync)((0, path_1.join)(catPath, pe.name), { withFileTypes: true });
1176
+ for (const st of stEntries) {
1177
+ if (st.isDirectory() && !st.name.startsWith('.')) {
1178
+ subtaskDirs.push(`${catDir}/${pe.name}/${st.name}`);
1179
+ }
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ catch { /* ignore */ }
1185
+ }
1186
+ }
1187
+ // 回退: 旧结构 10-*/20-*
1188
+ if (subtaskDirs.length === 0) {
1189
+ const legacy = (0, fs_1.readdirSync)(taskDir, { withFileTypes: true })
1190
+ .filter((d) => d.isDirectory() && (d.name.startsWith('10-') || d.name.startsWith('20-')))
1191
+ .map((d) => d.name);
1192
+ subtaskDirs.push(...legacy);
1193
+ }
1194
+ if (subtaskDirs.length > 0) {
1195
+ logger_1.logger.info(` 6. 子任务: ${subtaskDirs.join(', ')}`);
1043
1196
  }
1044
1197
  // 7. Constitution
1045
1198
  logger_1.logger.info(` 7. 合规: 待 validate ${issues.length > 0 ? '⚠️ ' + issues.join(', ') : ''}`);
1199
+ // 8. UI 规格检查(00-specs/ 优先,_shared/ 回退)
1200
+ const uiSpecPath = (0, path_1.join)(taskDir, '00-specs', 'UI_SPEC.md');
1201
+ const uiSpecFallback = (0, path_1.join)(taskDir, '_shared', 'UI_SPEC.md');
1202
+ const iterUiSpec = (0, path_1.join)(iterDir, '020-specs', 'UI_SPEC.md');
1203
+ let uiSpecContent = '';
1204
+ if (await (0, fs_extra_1.pathExists)(uiSpecPath)) {
1205
+ uiSpecContent = await (0, fs_extra_1.readFile)(uiSpecPath, 'utf-8');
1206
+ }
1207
+ else if (await (0, fs_extra_1.pathExists)(uiSpecFallback)) {
1208
+ uiSpecContent = await (0, fs_extra_1.readFile)(uiSpecFallback, 'utf-8');
1209
+ }
1210
+ else if (await (0, fs_extra_1.pathExists)(iterUiSpec)) {
1211
+ uiSpecContent = await (0, fs_extra_1.readFile)(iterUiSpec, 'utf-8');
1212
+ }
1213
+ if (uiSpecContent) {
1214
+ const uiPages = (uiSpecContent.match(/页面/g) || []).length;
1215
+ const uiComponents = (uiSpecContent.match(/组件/g) || []).length;
1216
+ logger_1.logger.info(` 8. UI规格: ${uiPages} 页面引用 / ${uiComponents} 组件引用`);
1217
+ }
1218
+ else {
1219
+ logger_1.logger.info(` 8. UI规格: ⚠️ 无 UI_SPEC.md`);
1220
+ }
1221
+ // 9. 前后端契约对标(00-specs/ 优先,_shared/ 回退)
1222
+ const apiContractPath = await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '00-specs', 'API_CONTRACT.yaml'))
1223
+ ? (0, path_1.join)(taskDir, '00-specs', 'API_CONTRACT.yaml')
1224
+ : (0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml');
1225
+ if (await (0, fs_extra_1.pathExists)(apiContractPath) && uiSpecContent) {
1226
+ const contract = await (0, fs_extra_1.readFile)(apiContractPath, 'utf-8');
1227
+ const contractFields = (contract.match(/fields:/g) || []).length;
1228
+ const uiFields = (uiSpecContent.match(/字段/g) || []).length;
1229
+ if (contractFields === 0 && uiFields > 0) {
1230
+ issues.push('API_CONTRACT.yaml 缺少字段定义,但 UI_SPEC.md 有字段映射 — 前后端契约未对齐');
1231
+ logger_1.logger.info(` 9. 契约: ⚠️ API 无字段定义但 UI 有 ${uiFields} 处字段引用`);
1232
+ }
1233
+ else {
1234
+ logger_1.logger.info(` 9. 契约: API ${contractFields} 组字段 / UI ${uiFields} 处引用`);
1235
+ }
1236
+ }
1237
+ else {
1238
+ logger_1.logger.info(` 9. 契约: ⏭️ 跳过(缺 API_CONTRACT 或 UI_SPEC)`);
1239
+ }
1046
1240
  // 写入问题文件,供 AI 辅助修复
1047
1241
  if (issues.length > 0) {
1048
1242
  const issuesMd = ['# 执行问题清单', '', `## ${task.id}`, ''];
@@ -1217,17 +1411,18 @@ function buildAgentContext(tasks, agent) {
1217
1411
  ctx += `Status: ${task.status} | Priority: ${task.priority} | Type: ${task.type}\n\n`;
1218
1412
  // Progressive loading order
1219
1413
  ctx += `**Read in this order (progressive disclosure):**\n`;
1220
- ctx += `1. \`Iteration-*/${task.id}/00-specs/TASK.md\` — task overview + deliverables checklist\n`;
1221
- ctx += `2. \`Iteration-*/${task.id}/00-specs/REQ.md\` — requirements + acceptance criteria\n`;
1222
- ctx += `3. \`Iteration-*/${task.id}/00-specs/TECH.md\` — tech design + architecture\n`;
1223
- ctx += `4. \`Iteration-*/${task.id}/_shared/API_CONTRACT.yaml\` — API contract (if exists)\n\n`;
1414
+ ctx += `1. \`Iteration-*/${task.id}/10-backend/{service}/{subtask}/TASK.md\` — 后端子任务概览\n`;
1415
+ ctx += `2. \`Iteration-*/${task.id}/20-frontend/{platform}/{subtask}/TASK.md\` — 前端子任务概览\n`;
1416
+ ctx += `3. \`Iteration-*/${task.id}/00-specs/REQ.md\` — 需求 + 验收标准\n`;
1417
+ ctx += `4. \`Iteration-*/${task.id}/00-specs/TECH.md\` — 技术设计\n`;
1418
+ ctx += `5. \`Iteration-*/${task.id}/00-specs/API_CONTRACT.yaml\` — API 契约 (if exists)\n\n`;
1224
1419
  ctx += `**Supplementary (read only if needed):**\n`;
1225
- ctx += `- 99-artifacts/TEST.md (test cases) | 99-artifacts/REVIEW.md (review checklist)\n`;
1226
- ctx += `- 00-specs/SCHEMA.md (DB schema) | 99-artifacts/ADR.md (arch decisions)\n`;
1227
- ctx += `- 99-artifacts/RISK.md (risks+rollback) | 99-artifacts/DEPS.md (dependencies) | 99-artifacts/MONITOR.md (monitoring)\n`;
1228
- ctx += `- 99-artifacts/ERROR_CODES.md (error codes) | 99-artifacts/DEPLOY.md (deployment)\n\n`;
1229
- ctx += `**Frontend (if exists):**\n`;
1230
- ctx += `- 20-20-frontend/{platform}/COMPONENT_TREE.md | ROUTES.md | STATE.md | STYLE_GUIDE.md\n\n`;
1420
+ ctx += `- 子任务目录/TEST.md (测试用例) | 子任务目录/REVIEW.md (评审清单)\n`;
1421
+ ctx += `- 00-specs/SCHEMA.md (DB schema) | 子任务目录/ADR.md (架构决策)\n`;
1422
+ ctx += `- 子任务目录/RISK.md (风险+回滚) | 子任务目录/DEPS.md (依赖) | 子任务目录/MONITOR.md (监控)\n`;
1423
+ ctx += `- 子任务目录/ERROR_CODES.md (错误码) | 子任务目录/DEPLOY.md (部署)\n\n`;
1424
+ ctx += `**Frontend design docs (if exists):**\n`;
1425
+ ctx += `- 20-{platform}/COMPONENT_TREE.md | ROUTES.md | STATE.md | STYLE_GUIDE.md\n\n`;
1231
1426
  }
1232
1427
  // 3. Global rules (load last, only if needed)
1233
1428
  ctx += `### Step N: Global rules (load last)\n`;
@@ -1251,52 +1446,126 @@ async function executionVerifyLoop(tasks, iteration, options) {
1251
1446
  if (round > 1)
1252
1447
  logger_1.logger.info(` 🔄 第 ${round} 轮修复...`);
1253
1448
  allPassed = true;
1254
- // 1. 检查 TEST.md
1255
- const testPath = (0, path_1.join)(taskDir, '99-artifacts', 'TEST.md');
1256
- if (await (0, fs_extra_1.pathExists)(testPath)) {
1449
+ // 1. 检查 TEST.md(扫描子任务目录,合并结果)
1450
+ const testFiles = [];
1451
+ for (const catDir of ['10-backend', '20-frontend']) {
1452
+ const catPath = (0, path_1.join)(taskDir, catDir);
1453
+ if (await (0, fs_extra_1.pathExists)(catPath)) {
1454
+ try {
1455
+ for (const svc of await (0, fs_extra_1.readdir)(catPath, { withFileTypes: true })) {
1456
+ if (!svc.isDirectory())
1457
+ continue;
1458
+ for (const sub of await (0, fs_extra_1.readdir)((0, path_1.join)(catPath, svc.name), { withFileTypes: true })) {
1459
+ if (!sub.isDirectory())
1460
+ continue;
1461
+ const fp = (0, path_1.join)(catPath, svc.name, sub.name, 'TEST.md');
1462
+ if (await (0, fs_extra_1.pathExists)(fp))
1463
+ testFiles.push(fp);
1464
+ }
1465
+ }
1466
+ }
1467
+ catch { /* 跳过 */ }
1468
+ }
1469
+ }
1470
+ if (testFiles.length === 0) {
1471
+ // 旧结构回退
1472
+ const legacy = (0, path_1.join)(taskDir, '99-artifacts', 'TEST.md');
1473
+ if (await (0, fs_extra_1.pathExists)(legacy))
1474
+ testFiles.push(legacy);
1475
+ }
1476
+ for (const testPath of testFiles) {
1257
1477
  const testContent = await (0, fs_extra_1.readFile)(testPath, 'utf-8');
1258
1478
  const total = (testContent.match(/\[[ x]\]/g) || []).length;
1259
1479
  const done = (testContent.match(/\[x\]/g) || []).length;
1480
+ const label = testPath.replace(taskDir + '/', '');
1260
1481
  if (done < total) {
1261
- logger_1.logger.info(` 🧪 TEST.md: ${done}/${total} 通过`);
1262
- if (round === maxRounds) {
1263
- logger_1.logger.warn(` ⚠️ 仍有 ${total - done} 项未通过(已达最大重试次数)`);
1264
- }
1482
+ logger_1.logger.info(` 🧪 ${label}: ${done}/${total} 通过`);
1483
+ if (round === maxRounds)
1484
+ logger_1.logger.warn(` ⚠️ 仍有 ${total - done} 项未通过`);
1265
1485
  allPassed = false;
1266
1486
  }
1267
1487
  else {
1268
- logger_1.logger.info(` 🧪 TEST.md: ${done}/${total} ✅`);
1488
+ logger_1.logger.info(` 🧪 ${label}: ${done}/${total} ✅`);
1269
1489
  }
1270
1490
  }
1271
- // 2. 检查 REVIEW.md
1272
- const reviewPath = (0, path_1.join)(taskDir, '99-artifacts', 'REVIEW.md');
1273
- if (await (0, fs_extra_1.pathExists)(reviewPath)) {
1491
+ // 2. 检查 REVIEW.md(扫描子任务目录)
1492
+ const reviewFiles = [];
1493
+ for (const catDir of ['10-backend', '20-frontend']) {
1494
+ const catPath = (0, path_1.join)(taskDir, catDir);
1495
+ if (await (0, fs_extra_1.pathExists)(catPath)) {
1496
+ try {
1497
+ for (const svc of await (0, fs_extra_1.readdir)(catPath, { withFileTypes: true })) {
1498
+ if (!svc.isDirectory())
1499
+ continue;
1500
+ for (const sub of await (0, fs_extra_1.readdir)((0, path_1.join)(catPath, svc.name), { withFileTypes: true })) {
1501
+ if (!sub.isDirectory())
1502
+ continue;
1503
+ const fp = (0, path_1.join)(catPath, svc.name, sub.name, 'REVIEW.md');
1504
+ if (await (0, fs_extra_1.pathExists)(fp))
1505
+ reviewFiles.push(fp);
1506
+ }
1507
+ }
1508
+ }
1509
+ catch { /* 跳过 */ }
1510
+ }
1511
+ }
1512
+ if (reviewFiles.length === 0) {
1513
+ const legacy = (0, path_1.join)(taskDir, '99-artifacts', 'REVIEW.md');
1514
+ if (await (0, fs_extra_1.pathExists)(legacy))
1515
+ reviewFiles.push(legacy);
1516
+ }
1517
+ for (const reviewPath of reviewFiles) {
1274
1518
  const reviewContent = await (0, fs_extra_1.readFile)(reviewPath, 'utf-8');
1275
1519
  const total = (reviewContent.match(/\[[ x]\]/g) || []).length;
1276
1520
  const doneR = (reviewContent.match(/\[x\]/gi) || []).length;
1521
+ const label = reviewPath.replace(taskDir + '/', '');
1277
1522
  if (doneR < total) {
1278
- logger_1.logger.info(` 📋 REVIEW.md: ${doneR}/${total} 通过`);
1279
- if (round === maxRounds) {
1280
- logger_1.logger.warn(` ⚠️ 仍有 ${total - doneR} 项未审查(已达最大重试次数)`);
1281
- }
1523
+ logger_1.logger.info(` 📋 ${label}: ${doneR}/${total} 通过`);
1524
+ if (round === maxRounds)
1525
+ logger_1.logger.warn(` ⚠️ 仍有 ${total - doneR} 项未审查`);
1282
1526
  allPassed = false;
1283
1527
  }
1284
1528
  else {
1285
- logger_1.logger.info(` 📋 REVIEW.md: ${doneR}/${total} ✅`);
1529
+ logger_1.logger.info(` 📋 ${label}: ${doneR}/${total} ✅`);
1286
1530
  }
1287
1531
  }
1288
- // 3. 检查 DEPLOY.md
1289
- const deployPath = (0, path_1.join)(taskDir, '99-artifacts', 'DEPLOY.md');
1290
- if (await (0, fs_extra_1.pathExists)(deployPath)) {
1532
+ // 3. 检查 DEPLOY.md(扫描子任务目录)
1533
+ const deployFiles = [];
1534
+ for (const catDir of ['10-backend', '20-frontend']) {
1535
+ const catPath = (0, path_1.join)(taskDir, catDir);
1536
+ if (await (0, fs_extra_1.pathExists)(catPath)) {
1537
+ try {
1538
+ for (const svc of await (0, fs_extra_1.readdir)(catPath, { withFileTypes: true })) {
1539
+ if (!svc.isDirectory())
1540
+ continue;
1541
+ for (const sub of await (0, fs_extra_1.readdir)((0, path_1.join)(catPath, svc.name), { withFileTypes: true })) {
1542
+ if (!sub.isDirectory())
1543
+ continue;
1544
+ const fp = (0, path_1.join)(catPath, svc.name, sub.name, 'DEPLOY.md');
1545
+ if (await (0, fs_extra_1.pathExists)(fp))
1546
+ deployFiles.push(fp);
1547
+ }
1548
+ }
1549
+ }
1550
+ catch { /* 跳过 */ }
1551
+ }
1552
+ }
1553
+ if (deployFiles.length === 0) {
1554
+ const legacy = (0, path_1.join)(taskDir, '99-artifacts', 'DEPLOY.md');
1555
+ if (await (0, fs_extra_1.pathExists)(legacy))
1556
+ deployFiles.push(legacy);
1557
+ }
1558
+ for (const deployPath of deployFiles) {
1291
1559
  const depContent = await (0, fs_extra_1.readFile)(deployPath, 'utf-8');
1292
1560
  const total = (depContent.match(/\[[ x]\]/g) || []).length;
1293
1561
  const doneD = (depContent.match(/\[x\]/g) || []).length;
1562
+ const label = deployPath.replace(taskDir + '/', '');
1294
1563
  if (doneD < total) {
1295
- logger_1.logger.info(` 🚀 DEPLOY.md: ${doneD}/${total} 通过`);
1564
+ logger_1.logger.info(` 🚀 ${label}: ${doneD}/${total} 通过`);
1296
1565
  allPassed = false;
1297
1566
  }
1298
1567
  else {
1299
- logger_1.logger.info(` 🚀 DEPLOY.md: ${doneD}/${total} ✅`);
1568
+ logger_1.logger.info(` 🚀 ${label}: ${doneD}/${total} ✅`);
1300
1569
  }
1301
1570
  }
1302
1571
  if (allPassed)
@@ -1425,18 +1694,22 @@ async function runPromptMode(iteration, options) {
1425
1694
  }
1426
1695
  const taskDir = await resolveTaskDir(await (0, context_1.getIterationDir)(iteration), task);
1427
1696
  // ── 前置检查:任务必须有有效需求或分析内容 ──
1428
- // 路径优先级: _shared/(新结构)→ 00-specs/(旧结构)→ 根目录(legacy)
1697
+ // 路径优先级: 00-specs/(标准)→ _shared/(兼容)→ 子任务目录 → 根目录(legacy)
1429
1698
  const candidates = [
1699
+ (0, path_1.join)(taskDir, '00-specs', 'REQ.md'),
1700
+ (0, path_1.join)(taskDir, '00-specs', 'TECH.md'),
1430
1701
  (0, path_1.join)(taskDir, '_shared', 'REQ.md'),
1431
1702
  (0, path_1.join)(taskDir, '_shared', 'TECH.md'),
1432
1703
  (0, path_1.join)(taskDir, '00-specs', 'ANALYSIS.md'),
1433
- (0, path_1.join)(taskDir, '00-specs', 'REQ.md'),
1434
1704
  (0, path_1.join)(taskDir, 'ANALYSIS.md'),
1435
1705
  (0, path_1.join)(taskDir, 'REQUIREMENT.md'),
1436
1706
  ];
1437
- // 按端执行时,额外检查该端的 TASK.md
1707
+ // 按端执行时,额外检查该端子任务的 TASK.md
1438
1708
  if (options.platform) {
1439
- candidates.unshift((0, path_1.join)(taskDir, options.platform, 'TASK.md'));
1709
+ const isBackend = options.platform === 'backend' || options.platform.startsWith('后台');
1710
+ const prefix = isBackend ? '10-' : '20-';
1711
+ const subtaskName = isBackend && options.platform === 'backend' ? 'api' : options.platform;
1712
+ candidates.unshift((0, path_1.join)(taskDir, `${prefix}${subtaskName}`, 'TASK.md'));
1440
1713
  }
1441
1714
  // 读取文件内容验证有效性(接受 ANALYSIS.md / REQ.md / REQUIREMENT.md)
1442
1715
  // Bug 任务的 REQ.md(问题描述)本身就可以作为AI生成代码的依据
@@ -1456,7 +1729,7 @@ async function runPromptMode(iteration, options) {
1456
1729
  }
1457
1730
  }
1458
1731
  if (!effectiveAnalysis) {
1459
- const reason = `任务 ${task} 缺少有效的需求或分析内容\n(_shared/REQ.md、_shared/TECH.md、00-specs/ANALYSIS.md 均不存在或内容无效)`;
1732
+ const reason = `任务 ${task} 缺少有效的需求或分析内容\n(00-specs/REQ.md、00-specs/TECH.md、00-specs/ANALYSIS.md 均不存在或内容无效)`;
1460
1733
  (0, prompt_builder_1.outputNeedsInfo)({
1461
1734
  command: 'execute',
1462
1735
  missing: ['analysis'],
@@ -1525,9 +1798,18 @@ async function runPromptMode(iteration, options) {
1525
1798
  promptText += `- 当前批次: ${currentBatch}/${totalBatches}\n`;
1526
1799
  promptText += `- 批次大小: ${bs}\n`;
1527
1800
  if (isBatchEnd && nextTask) {
1528
- promptText += `\n[SPECCORE_BATCH_COMPLETE]\n`;
1801
+ // 写入上下文摘要文件(如果 execution-state 存在)
1802
+ let summaryPath = '';
1803
+ const existingState = (0, execution_state_1.loadExecutionState)();
1804
+ if (existingState) {
1805
+ summaryPath = await (0, execution_state_1.writeContextSummaryFile)(existingState);
1806
+ }
1807
+ promptText += `\n[SPECCORE_CONTINUE${summaryPath ? `: ${summaryPath}` : ''}]\n`;
1529
1808
  promptText += `本批次已完成。请开始新的对话,然后执行:\n`;
1530
1809
  promptText += `speccore execute --prompt --task=${nextTask.id} -i ${iteration}\n`;
1810
+ if (summaryPath) {
1811
+ promptText += `\n💡 新会话会自动读取摘要文件恢复上下文\n`;
1812
+ }
1531
1813
  }
1532
1814
  else if (nextTask) {
1533
1815
  promptText += `\n下一个任务: ${nextTask.id}(继续当前对话)\n`;