speccore 6.76.1 → 6.77.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 (47) hide show
  1. package/.agents/skills/spec-analyze/SKILL.md +174 -4
  2. package/.agents/skills/spec-change/SKILL.md +156 -20
  3. package/.agents/skills/spec-doc2spec/SKILL.md +121 -4
  4. package/.agents/skills/spec-execute/SKILL.md +148 -5
  5. package/.agents/skills/spec-iteration-create/SKILL.md +128 -9
  6. package/.agents/skills/spec-plan/SKILL.md +123 -4
  7. package/.agents/skills/spec-spec2doc/SKILL.md +126 -4
  8. package/.agents/skills/spec-split/SKILL.md +214 -4
  9. package/.agents/skills/spec-task-create/SKILL.md +156 -9
  10. package/.agents/skills/speccore-router/SKILL.md +21 -1
  11. package/dist/cli.js +22 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/analyze.d.ts +2 -0
  14. package/dist/commands/analyze.d.ts.map +1 -1
  15. package/dist/commands/analyze.js +198 -14
  16. package/dist/commands/analyze.js.map +1 -1
  17. package/dist/commands/clarify.d.ts +11 -0
  18. package/dist/commands/clarify.d.ts.map +1 -0
  19. package/dist/commands/clarify.js +138 -0
  20. package/dist/commands/clarify.js.map +1 -0
  21. package/dist/commands/execute.d.ts +1 -0
  22. package/dist/commands/execute.d.ts.map +1 -1
  23. package/dist/commands/execute.js +15 -0
  24. package/dist/commands/execute.js.map +1 -1
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +4 -8
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/iteration/split.d.ts +3 -1
  29. package/dist/commands/iteration/split.d.ts.map +1 -1
  30. package/dist/commands/iteration/split.js +125 -153
  31. package/dist/commands/iteration/split.js.map +1 -1
  32. package/dist/commands/update.js +0 -1
  33. package/dist/commands/update.js.map +1 -1
  34. package/dist/core/ask-engine.d.ts.map +1 -1
  35. package/dist/core/ask-engine.js +3 -2
  36. package/dist/core/ask-engine.js.map +1 -1
  37. package/dist/core/change-detector.d.ts +25 -0
  38. package/dist/core/change-detector.d.ts.map +1 -0
  39. package/dist/core/change-detector.js +170 -0
  40. package/dist/core/change-detector.js.map +1 -0
  41. package/dist/core/requirement-clarifier.d.ts +41 -0
  42. package/dist/core/requirement-clarifier.d.ts.map +1 -0
  43. package/dist/core/requirement-clarifier.js +181 -0
  44. package/dist/core/requirement-clarifier.js.map +1 -0
  45. package/package.json +1 -1
  46. package/.agents/skills/spec-dev/SKILL.md +0 -10
  47. package/.agents/skills/spec-synthesize/SKILL.md +0 -10
@@ -142,45 +142,11 @@ function generateSubtaskId(parentTaskId, platform) {
142
142
  // 因为每个任务每个端只有一个子任务,所以 {taskId}-{platform} 已经唯一
143
143
  return `Task-${parentTaskId}-${platform}`;
144
144
  }
145
- /** 粒度约束常量 */
146
- const GRANULARITY_RULES = {
147
- macro: { label: '粗粒度 (macro)', minHours: 20, maxHours: 80, maxApis: 15, maxTables: 5, maxPages: 5, desc: '每个任务 1-2 周,按业务方向合并' },
148
- module: { label: '中粒度 (module)', minHours: 12, maxHours: 40, maxApis: 8, maxTables: 3, maxPages: 3, desc: '每个任务 3-5 天,按功能/端拆分' },
149
- atomic: { label: '细粒度 (atomic)', minHours: 4, maxHours: 24, maxApis: 3, maxTables: 2, maxPages: 1, desc: '每个任务 1-3 天,按接口/表拆分' },
145
+ /** 拆分约束常量(简化版:每个功能单元按涉及的端拆分,每端1个子任务) */
146
+ const SPLIT_RULES = {
147
+ maxTasksPerIteration: 20, // 单次迭代总任务数上限
148
+ maxTasksPerUnit: 3, // 每个功能单元最多任务数
150
149
  };
151
- /** 校验任务工时是否在粒度范围内(按单人 max 工时计算) */
152
- function validateGranularity(gran, hoursByPlatform, apiCount, tableCount) {
153
- const rule = GRANULARITY_RULES[gran];
154
- const warnings = [];
155
- const platformEntries = Object.entries(hoursByPlatform);
156
- const maxPerPerson = platformEntries.length > 0 ? Math.max(...platformEntries.map(([, h]) => h)) : 0;
157
- const totalHours = platformEntries.reduce((sum, [, h]) => sum + h, 0);
158
- const maxPlatform = platformEntries.length > 0 ? platformEntries.reduce((a, b) => (b[1] > a[1] ? b : a))[0] : '';
159
- if (maxPerPerson > rule.maxHours) {
160
- warnings.push(`⚠️ 单人最大工时 ${maxPerPerson}h(${maxPlatform})超出上限 ${rule.maxHours}h → 建议再拆`);
161
- }
162
- else if (maxPerPerson < rule.minHours) {
163
- warnings.push(`⚠️ 单人最大工时 ${maxPerPerson}h(${maxPlatform})低于下限 ${rule.minHours}h → 建议合并到关联任务`);
164
- }
165
- if (apiCount > rule.maxApis)
166
- warnings.push(`⚠️ 接口 ${apiCount} 个超出上限 ${rule.maxApis} → 建议按业务领域拆分`);
167
- if (tableCount > rule.maxTables)
168
- warnings.push(`⚠️ 数据表 ${tableCount} 张超出上限 ${rule.maxTables} → 建议按数据层拆分`);
169
- // 返回额外信息供展示
170
- if (platformEntries.length > 1) {
171
- const breakdown = platformEntries.map(([p, h]) => `${p}:${h}h`).join(' + ');
172
- warnings.unshift(`ℹ️ 工时分布: ${breakdown} = ${totalHours}h(max per person: ${maxPerPerson}h)`);
173
- }
174
- return warnings;
175
- }
176
- /** 根据团队规模推荐粒度 */
177
- function recommendGranularity(teamSize) {
178
- if (teamSize <= 3)
179
- return 'macro';
180
- if (teamSize <= 8)
181
- return 'module';
182
- return 'atomic';
183
- }
184
150
  function promptUser(question) {
185
151
  const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
186
152
  return new Promise(resolve => {
@@ -359,16 +325,10 @@ async function iterationSplitCommand(options) {
359
325
  };
360
326
  section._complexity = {
361
327
  estimatedHours: task.estimatedHours || 8,
362
- hoursByPlatform: (task.hoursByPlatform && typeof task.hoursByPlatform === 'object') ? task.hoursByPlatform : {},
363
328
  priority: task.priority || 'medium',
364
- complexity: task.risk === 'high' ? 'high' : task.risk === 'low' ? 'low' : 'medium',
365
- apiCount: (task.apis || []).length,
366
- dbCount: (task.tables || []).length,
367
- pageCount: 0,
368
- wordCount: content.length,
369
329
  };
370
330
  section._owner = task.owner || '未分配';
371
- section._taskType = (task.type && ['feature', 'bugfix', 'refactor', 'research'].includes(task.type)) ? task.type : 'feature';
331
+ section._taskType = (task.type && ['feature', 'bugfix', 'refactor', 'research', 'security', 'performance'].includes(task.type)) ? task.type : 'feature';
372
332
  // 保存 topic slug,用于生成任务目录名
373
333
  section._topic = task.topic || slugify(task.name || `Task ${i + 1}`);
374
334
  // 保存 AI 生成的实际内容(用于写入 REQ.md / TECH.md)
@@ -406,13 +366,8 @@ async function iterationSplitCommand(options) {
406
366
  logger_1.logger.info(` 🗑 已清理旧任务目录`);
407
367
  }
408
368
  }
409
- // 确定粒度
410
- const staffing2 = readStaffing(iterDirFull);
411
- const teamSize2 = staffing2 ? staffing2.length : 0;
412
- const granularity = options.granularity || recommendGranularity(teamSize2);
413
- const granRule = GRANULARITY_RULES[granularity];
414
369
  // 🚨 全局任务数硬限制(安全网:防止 AI 输出爆炸)
415
- const MAX_TASKS_HARD = 20;
370
+ const MAX_TASKS_HARD = SPLIT_RULES.maxTasksPerIteration;
416
371
  if (sections.length > MAX_TASKS_HARD && !options.force) {
417
372
  logger_1.logger.error(`\n ❌ 任务数爆炸!AI 输出了 ${sections.length} 个任务(上限 ${MAX_TASKS_HARD})`);
418
373
  logger_1.logger.error(` 💡 这说明拆分粒度过细,必须合并。请告诉 AI:"任务太多,请合并相关功能,总数控制在 ${MAX_TASKS_HARD} 以内"`);
@@ -471,7 +426,6 @@ async function iterationSplitCommand(options) {
471
426
  }
472
427
  // 交互模式判断:显式 --interactive 或 stdin 是 TTY(--force 时跳过交互,直接执行)
473
428
  const isInteractive = (options.interactive || process.stdin.isTTY) && !options.force;
474
- logger_1.logger.info(` 📏 粒度: ${granRule.label}${options.granularity ? ' (用户指定)' : ` (${teamSize2} 人团队自动推荐)`}`);
475
429
  // 非交互模式:显示任务总览摘要
476
430
  if (!isInteractive) {
477
431
  logger_1.logger.info(`\n 📋 任务总览(共 ${sections.length} 个):`);
@@ -501,19 +455,8 @@ async function iterationSplitCommand(options) {
501
455
  logger_1.logger.info(`\n ━━━━ 任务 ${i + 1}/${sections.length} ━━━━`);
502
456
  logger_1.logger.info(` 📌 ${sec.name}`);
503
457
  logger_1.logger.info(` 🏷 类型: ${taskType} | 🎯 优先级: ${complexity.priority || 'medium'}`);
504
- // 按端展示工时分布
505
- const hbp = complexity.hoursByPlatform || {};
506
- const hbpEntries = Object.entries(hbp);
507
- if (hbpEntries.length > 0) {
508
- const breakdown = hbpEntries.map(([p, h]) => `${p}:${h}h`).join(' + ');
509
- const maxPerPerson = Math.max(...hbpEntries.map(([, h]) => h));
510
- logger_1.logger.info(` ⏱ 工时: ${breakdown} = ${complexity.estimatedHours}h(max per person: ${maxPerPerson}h)`);
511
- }
512
- else {
458
+ if (complexity.estimatedHours)
513
459
  logger_1.logger.info(` ⏱ 预估: ${complexity.estimatedHours}h`);
514
- }
515
- if (complexity.apiCount)
516
- logger_1.logger.info(` 🔌 接口: ${complexity.apiCount} 个 | 🗄 数据表: ${complexity.dbCount || 0} 张`);
517
460
  if (deps.length > 0)
518
461
  logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
519
462
  if (acs.length > 0) {
@@ -521,16 +464,6 @@ async function iterationSplitCommand(options) {
521
464
  for (const ac of acs.slice(0, 5))
522
465
  logger_1.logger.info(` ${ac}`);
523
466
  }
524
- // 粒度校验(按单人 max 工时)
525
- const warnings = validateGranularity(granularity, hbp, complexity.apiCount || 0, complexity.dbCount || 0);
526
- if (warnings.length > 0) {
527
- for (const w of warnings) {
528
- if (w.startsWith('ℹ️'))
529
- logger_1.logger.info(` ${w}`);
530
- else
531
- logger_1.logger.warn(` ${w}`);
532
- }
533
- }
534
467
  // 交互确认(仅确认,调整应回到 AI 对话重新生成方案)
535
468
  if (isInteractive) {
536
469
  const answer = await promptUser(` 确认创建?(y/回车确认,n 调整方案):`);
@@ -615,6 +548,12 @@ async function iterationSplitCommand(options) {
615
548
  return;
616
549
  }
617
550
  const iterationDir = await (0, context_1.getIterationDir)(iteration);
551
+ // v6.76.0+: 变更检测 — 检查 020-specs/ 是否比 Task/ 更新
552
+ if (!options.ignoreSpecsUpdate && !options.prompt && !options.response) {
553
+ const { detectSpecChangesBeforeSplit, printChangeDetection } = await Promise.resolve().then(() => __importStar(require('../../core/change-detector')));
554
+ const changeResult = await detectSpecChangesBeforeSplit(iterationDir);
555
+ printChangeDetection(changeResult, '拆分前检测');
556
+ }
618
557
  // ── v6.49.13+: 模块驱动拆分 — CLI 按功能模块×端创建任务目录,AI 只填充内容 ──
619
558
  if (!options.prompt && !options.response) {
620
559
  const moduleDrivenResult = await tryModuleDrivenSplit(iteration, iterationDir, options);
@@ -748,16 +687,15 @@ async function iterationSplitCommand(options) {
748
687
  }
749
688
  }
750
689
  }
751
- // 粒度推荐(基于 STAFFING 人数)
752
- const staffing = readStaffing(iterationDir);
753
- const teamSize = staffing ? staffing.length : 0;
754
- const recommendedGranularity = options.granularity || recommendGranularity(teamSize);
755
- const granularityLabel = GRANULARITY_RULES[recommendedGranularity].label;
756
- const granularityHint = GRANULARITY_RULES[recommendedGranularity].desc;
757
690
  // 获取标准端名列表(用于 prompt 注入和 scope 规范化)
758
691
  const allPlatforms = await detectPlatforms(iterationDir);
692
+ // v6.76.0+: 扫描已有 Task 结构(增量拆分)
693
+ const existingTaskStructure = await scanExistingTaskStructure(iterationDir);
759
694
  // 构建完整 prompt(v6.69.3+: 传入标准端名列表,确保 AI 使用正确的端名)
760
- let splitPrompt = buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, staffing, teamSize, granularityLabel, granularityHint, allPlatforms);
695
+ // v6.76.0+: 传入过滤条件,限制拆分范围;传入已有 Task 结构,支持增量拆分
696
+ const modulesFilter = options.modules ? options.modules.split(',').map(m => m.trim()).filter(Boolean) : undefined;
697
+ const platformsFilter = options.platforms ? options.platforms.split(',').map(p => p.trim()).filter(Boolean) : undefined;
698
+ let splitPrompt = buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, allPlatforms, modulesFilter, platformsFilter, existingTaskStructure, options.devGuide);
761
699
  // 注入全局上下文(INDEX + TOC 目录,AI 自主读取)
762
700
  const { loadGlobalContext, formatGlobalContext } = await Promise.resolve().then(() => __importStar(require('../../core/prompt-builder')));
763
701
  const globalCtx = await loadGlobalContext(process.cwd(), 'split');
@@ -766,7 +704,6 @@ async function iterationSplitCommand(options) {
766
704
  }
767
705
  await (0, fs_extra_1.writeFile)((0, path_1.join)(promptsDir, `split-suggestion-${iteration}.md`), splitPrompt);
768
706
  logger_1.logger.info(` 🤖 AI 拆分建议 → .speccore/prompts/split-suggestion-${iteration}.md`);
769
- logger_1.logger.info(` 📏 推荐粒度: ${granularityLabel}${options.granularity ? ' (用户指定)' : ` (基于 ${teamSize} 人团队自动推荐)`}`);
770
707
  logger_1.logger.info(` 📜 上下文: CONSTITUTION + REQUIREMENT + ${specContents.length} 个 Spec 文档`);
771
708
  }
772
709
  else {
@@ -778,10 +715,19 @@ async function iterationSplitCommand(options) {
778
715
  return;
779
716
  }
780
717
  const content = await (0, fs_extra_1.readFile)(reqFile, 'utf-8');
781
- const sections = extractSections(content, options.sections);
782
- if (sections.length === 0) {
783
- spinner.fail('No sections found to split');
784
- return;
718
+ let sections = extractSections(content, options.sections);
719
+ // v6.76.0+: 按功能模块过滤
720
+ if (options.modules) {
721
+ const moduleFilters = options.modules.split(',').map(m => m.trim()).filter(Boolean);
722
+ const beforeCount = sections.length;
723
+ sections = sections.filter(s => moduleFilters.some(f => s.name.includes(f)));
724
+ if (sections.length < beforeCount) {
725
+ logger_1.logger.info(` 🎯 模块过滤: ${beforeCount} → ${sections.length}(只拆分: ${moduleFilters.join(', ')})`);
726
+ }
727
+ if (sections.length === 0) {
728
+ spinner.fail(`没有匹配 "${options.modules}" 的功能模块,请检查模块名称`);
729
+ return;
730
+ }
785
731
  }
786
732
  const platforms = await detectPlatforms(iterationDir, options.platforms);
787
733
  // ── 冲突检测: 检查是否已有 Task 目录 ──
@@ -870,7 +816,7 @@ async function iterationSplitCommand(options) {
870
816
  logger_1.logger.info(` ${taskId} → ${sections[i].name}`);
871
817
  if (contentPreview)
872
818
  logger_1.logger.info(` ${contentPreview}`);
873
- logger_1.logger.info(` 优先级: ${c.priority} | 工时: ${c.estimatedHours}h | 复杂度: ${c.complexity} | 👤 ${owner}`);
819
+ logger_1.logger.info(` 优先级: ${c.priority} | 工时: ${c.estimatedHours}h | 👤 ${owner}`);
874
820
  if (deps)
875
821
  logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
876
822
  logger_1.logger.info(` 平台: ${platforms.join(', ')}`);
@@ -1111,7 +1057,7 @@ async function createTaskFromSection(iterationDir, taskId, section, allPlatforms
1111
1057
  taskPlatforms = allPlatforms;
1112
1058
  }
1113
1059
  }
1114
- const complexity = section._complexity || { estimatedHours: 2, priority: 'medium', complexity: 'medium', apiCount: 0, dbCount: 0, pageCount: 0, wordCount: 0 };
1060
+ const complexity = section._complexity || { estimatedHours: 8, priority: 'medium' };
1115
1061
  const owner = section._owner || '未分配';
1116
1062
  const today = new Date().toISOString().split('T')[0];
1117
1063
  // 加载迭代级 analyze 产出(020-specs/),用于填充任务级文件
@@ -1494,7 +1440,7 @@ ${isBk ? apiList : pageList}
1494
1440
  const subtaskId = subtaskIdMap.get(platform);
1495
1441
  // 子任务目录名 = subtaskId(如 Task-001-booking-service)
1496
1442
  const subtaskDir = (0, path_1.join)(platformDir, subtaskId);
1497
- const subtaskHours = section._hoursByPlatform?.[platform] || Math.ceil(complexity.estimatedHours / taskPlatforms.length);
1443
+ const subtaskHours = complexity.estimatedHours || 8;
1498
1444
  // 判断是否后端(用于生成不同的文档内容)
1499
1445
  const isBk = platform === 'backend' || platform.startsWith('后台') || /-(service|api|server|backend)$/i.test(platform);
1500
1446
  await createSubtask(subtaskDir, subtaskId, platform, isBk ? '后端' : platform, isBk, subtaskHours);
@@ -1557,7 +1503,7 @@ ${isBk ? apiList : pageList}
1557
1503
  ? `020-specs/${sourceFile}`
1558
1504
  : `020-specs/${taskType === 'feature' ? 'features' : taskType === 'bugfix' ? 'bugs' : taskType === 'refactor' ? 'refactors' : 'research'}/${topic}.md`;
1559
1505
  // 任务类型中文标签
1560
- const typeLabels = { feature: '功能开发', bugfix: '缺陷修复', refactor: '重构优化', research: '技术调研' };
1506
+ const typeLabels = { feature: '功能开发', bugfix: '缺陷修复', refactor: '重构优化', research: '技术调研', security: '安全修复', performance: '性能优化' };
1561
1507
  const typeLabel = typeLabels[taskType] || taskType;
1562
1508
  // 收集同迭代的其它任务(用于关联关系)
1563
1509
  const relatedTasks = allSections
@@ -2433,9 +2379,38 @@ ${isH5 ? '移动端优先,适配 375/414/768' :
2433
2379
  /**
2434
2380
  * 构建完整的 AI 智能拆分 Prompt(含 SpecCore 理念 + 粒度规则 + 完整上下文)
2435
2381
  */
2436
- function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, staffing, teamSize, granularityLabel, granularityHint, standardPlatforms) {
2382
+ function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, standardPlatforms, modulesFilter, platformsFilter, existingTasks, devGuide) {
2437
2383
  let p = `# SpecCore AI 智能拆分\n\n`;
2438
- p += `> 迭代: ${iteration} | 粒度: ${granularityLabel} | 生成: ${new Date().toISOString().split('T')[0]}\n\n`;
2384
+ p += `> 迭代: ${iteration} | 生成: ${new Date().toISOString().split('T')[0]}\n\n`;
2385
+ // v6.76.0+: 拆分范围限制
2386
+ if (modulesFilter?.length || platformsFilter?.length) {
2387
+ p += `## 🎯 拆分范围限制(用户指定)\n\n`;
2388
+ if (modulesFilter?.length) {
2389
+ p += `- **只拆分以下功能模块**: ${modulesFilter.join('、')}\n`;
2390
+ p += `- 如果需求文档中有其他功能模块,**完全忽略**,不要拆分\n`;
2391
+ }
2392
+ if (platformsFilter?.length) {
2393
+ p += `- **只拆分涉及以下端的功能模块**: ${platformsFilter.join('、')}\n`;
2394
+ p += `- 如果某个功能模块不涉及这些端,**跳过不拆**\n`;
2395
+ p += `- 每个任务的 scope 只能包含这些端,不要引入其他端\n`;
2396
+ }
2397
+ p += `\n`;
2398
+ }
2399
+ // v6.76.0+: 增量拆分 — 已有 Task 结构
2400
+ if (existingTasks && existingTasks.size > 0) {
2401
+ p += `## 📂 已有 Task 结构(增量拆分模式)\n\n`;
2402
+ p += `本次拆分为**增量模式**:以下 Task 已存在,如果新拆分的功能单元与已有 Task 匹配,**复用该 Task ID,只追加新端**。\n\n`;
2403
+ for (const [taskName, platforms] of existingTasks.entries()) {
2404
+ p += `- **${taskName}**: 已有端 [${platforms.join(', ')}]\n`;
2405
+ }
2406
+ p += `\n`;
2407
+ p += `### 增量拆分规则\n`;
2408
+ p += `- 如果功能单元与已有 Task 的 functionalUnit 相同 → **复用该 Task ID**,scope 追加新端\n`;
2409
+ p += `- 如果功能单元是全新的 → 生成新的 Task ID(延续现有编号)\n`;
2410
+ p += `- 已有端不要重复拆分,只拆分**新增的端**\n`;
2411
+ p += `- 已有 Task 的 _shared/ 目录保持不变,新端目录追加到 10-backend/ 或 20-frontend/ 下\n`;
2412
+ p += `- API 契约(_shared/API_CONTRACT.yaml)需要补充新端涉及的接口\n\n`;
2413
+ }
2439
2414
  // 技术宪法
2440
2415
  if (constitutionContent) {
2441
2416
  p += `## 📜 技术宪法 (CONSTITUTION.md)\n\n${constitutionContent.slice(0, 3000)}\n\n---\n\n`;
@@ -2446,35 +2421,19 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
2446
2421
  for (const spec of specContents) {
2447
2422
  p += `## 📜 ${spec.name}\n\n${spec.content.slice(0, 3000)}\n\n---\n\n`;
2448
2423
  }
2449
- // 团队配置
2450
- if (staffing && staffing.length > 0) {
2451
- p += `## 👥 团队配置 (STAFFING.md)\n\n`;
2452
- p += `| 人员 | 擅长端 | 负荷 |\n| :--- | :--- | :--- |\n`;
2453
- for (const m of staffing) {
2454
- p += `| ${m.name} | ${m.platforms.join(', ')} | ${m.capacity}% |\n`;
2455
- }
2456
- p += `\n检测到 ${teamSize} 人团队,推荐粒度: ${granularityLabel}\n\n---\n\n`;
2457
- }
2458
- // 粒度说明(含硬约束)
2459
- p += `## 🎯 拆分粒度: ${granularityLabel}\n\n`;
2460
- p += `${granularityHint}\n\n`;
2461
- p += `### 当前粒度硬约束(必须严格遵守)\n`;
2462
- p += `> ⚠️ 工时约束按 **max(各端工时)** 计算,即单个开发人员的实际工作量,不是所有端的总和\n\n`;
2463
- if (granularityLabel.includes('粗')) {
2464
- p += `- 每人工时: 20-80h(1-2 周)\n- 接口上限: 15 个/任务\n- 数据表上限: 5 张/任务\n- 页面上限: 5 个/任务\n`;
2465
- }
2466
- else if (granularityLabel.includes('中')) {
2467
- p += `- 每人工时: 12-40h(3-5 天)\n- 接口上限: 8 个/任务\n- 数据表上限: 3 张/任务\n- 页面上限: 3 个/任务\n`;
2468
- }
2469
- else {
2470
- p += `- 每人工时: 4-24h(1-3 天)\n- 接口上限: 3 个/任务\n- 数据表上限: 2 张/任务\n- 页面上限: 1 个/任务\n`;
2424
+ // v6.69.3+: 注入标准端名列表
2425
+ if (standardPlatforms.length > 0) {
2426
+ p += `## 🖥️ 项目端列表(标准端名)\n\n`;
2427
+ p += `本项目已配置以下端,所有 scope 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
2428
+ p += `\`${standardPlatforms.join('`, `')}\`\n\n`;
2429
+ p += `- 后端端名示例: \`booking-service\`, \`room-service\`(不是 "后端"、"backend")\n`;
2430
+ p += `- 前端端名示例: \`admin-web\`, \`h5-mobile\`(不是 "web"、"admin"、"h5")\n`;
2431
+ p += `- **scope 数组必须使用上述标准端名**,否则会导致目录结构错误\n\n`;
2471
2432
  }
2472
- p += `\n**超出上限必须再拆,低于下限必须合并。**\n\n`;
2473
- p += `用户可通过 --granularity macro|module|atomic 调整全局粒度。\n\n`;
2474
2433
  // v6.69.3+: 注入标准端名列表
2475
2434
  if (standardPlatforms.length > 0) {
2476
2435
  p += `## 🖥️ 项目端列表(标准端名)\n\n`;
2477
- p += `本项目已配置以下端,所有 scope、hoursByPlatform 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
2436
+ p += `本项目已配置以下端,所有 scope 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
2478
2437
  p += `\`${standardPlatforms.join('`, `')}\`\n\n`;
2479
2438
  p += `- 后端端名示例: \`booking-service\`, \`room-service\`(不是 "后端"、"backend")\n`;
2480
2439
  p += `- 前端端名示例: \`admin-web\`, \`h5-mobile\`(不是 "web"、"admin"、"h5")\n`;
@@ -2556,13 +2515,9 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
2556
2515
  p += ` "functionalUnit": "所属功能单元/影响域(必填!见下方类型规则)",\n`;
2557
2516
  p += ` "name": "任务名称(中文)",\n`;
2558
2517
  p += ` "topic": "english-slug-for-directory",\n`;
2559
- p += ` "type": "feature|bugfix|refactor|research",\n`;
2518
+ p += ` "type": "feature|bugfix|refactor|research|security|performance",\n`;
2560
2519
  p += ` "reason": "为什么这样拆分",\n`;
2561
2520
  p += ` "scope": ["booking-service", "admin-web"],\n`;
2562
- p += ` "apis": ["POST /api/auth/login"],\n`;
2563
- p += ` "tables": ["users"],\n`;
2564
- p += ` "hoursByPlatform": { "booking-service": 8, "admin-web": 8 },\n`;
2565
- p += ` "estimatedHours": 16,\n`;
2566
2521
  p += ` "priority": "high|medium|low",\n`;
2567
2522
  p += ` "dependencies": [],\n`;
2568
2523
  p += ` "acceptanceCriteria": ["AC1: ..."],\n`;
@@ -2570,13 +2525,15 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
2570
2525
  p += ` "owner": "建议负责人",\n`;
2571
2526
  p += ` "sourceFile": "来源文档路径(如 bugs/login-timeout.md、features/user-auth.md)",\n`;
2572
2527
  p += ` "reqContent": "需求描述内容(Markdown 格式,写入 REQ.md)",\n`;
2573
- p += ` "techContent": "技术方案内容(Markdown 格式,写入 TECH.md)"\n`;
2528
+ p += ` "techContent": "技术方案内容(Markdown 格式,写入 TECH.md)"${devGuide ? `,\n "devGuideContent": "开发者实现指南(Markdown 格式,写入 DEV_GUIDE.md):实现步骤、关键代码示例、与存量功能集成、注意事项"` : ''}\n`;
2574
2529
  p += ` }\n]\n`;
2575
2530
  p += '```\n\n';
2576
2531
  p += `> **functionalUnit 必须填写**:根据任务类型语义不同:\n`;
2577
2532
  p += `> - feature → 功能模块名(如:用户管理、订单系统、支付模块)\n`;
2578
2533
  p += `> - bugfix → 受影响组件/流程(如:登录流程、支付回调、数据同步)\n`;
2579
2534
  p += `> - refactor → 重构目标范围(如:数据库层、API网关、状态管理)\n`;
2535
+ p += `> - security → 安全修复范围(如:SQL注入防护、XSS修复、鉴权加固)\n`;
2536
+ p += `> - performance → 性能优化目标(如:查询优化、缓存策略、并发提升)\n`;
2580
2537
  p += `> - research → 研究主题(如:WebSocket方案、缓存策略)\n`;
2581
2538
  p += `> 同一模块/领域的任务填相同的值,用于粒度校验和任务分组\n`;
2582
2539
  p += `> **topic** 必须是英文短横线格式(如 \`user-authentication\`、\`product-crud\`),用于生成任务目录名 Task-NNN-{topic}\n`;
@@ -2592,16 +2549,15 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
2592
2549
  p += `> - 从 020-specs/global/TECH.md 和对应端 TECH.md 中提取本任务相关的技术细节\n`;
2593
2550
  p += `> - 直接写入 00-specs/TECH.md,执行时 AI 据此直接开发\n`;
2594
2551
  p += `> **质量红线**:如果 reqContent/techContent 只有标题和占位符(如 "<!-- AI-FILL -->"),视为不合格,必须重新生成\n\n`;
2595
- p += `### ⚠️ 工时估算规则(重要)\n\n`;
2596
- p += `- **hoursByPlatform**: 按端分别估算工时,key 对应 scope 中的端名称\n`;
2597
- p += `- **estimatedHours**: 各端工时总和(仅用于展示,不参与粒度校验)\n`;
2598
- p += `- **粒度校验用 max(各端工时)**:衡量「一个开发人员实际干多少」,不是总和\n`;
2599
- p += `- 例:后端 8h + admin 8h = total 16h,但 per-person max = 8h,按 8h 判断粒度\n`;
2600
- p += `- 同一功能的前后端各端工作必须在一个原子任务里,不要按端拆分任务\n\n`;
2552
+ p += `### ⚠️ 拆分规则(重要)\n\n`;
2553
+ p += `- **一个功能单元默认 1 个任务**,最多拆成 3 个\n`;
2554
+ p += `- **按 scope 涉及的端创建子任务**:每个端 1 个子任务目录\n`;
2555
+ p += `- **不要按端拆分任务**:同一功能的前后端各端工作在一个任务里,按端拆分子任务\n`;
2556
+ p += `- **用户不满意时可手动拆分**:CLI 提供简单的默认拆分,复杂拆分由用户手动调整\n\n`;
2601
2557
  // 质量自检
2602
2558
  p += `## ✅ 质量自检(必须全部通过)\n\n`;
2603
2559
  p += `□ 每个任务都满足原子任务定义?\n`;
2604
- p += `□ 每个任务的 estimatedHours 在当前粒度范围内?(不满足 → 合并或再拆)\n`;
2560
+ p += `□ 每个任务的 scope 只包含必要的端?\n`;
2605
2561
  p += `□ 没有循环依赖?\n`;
2606
2562
  p += `□ 基础模块排在前面?\n`;
2607
2563
  p += `□ 同功能单元内的任务没被过度拆分?(每个功能单元 ≤ 3 个任务)\n`;
@@ -2683,35 +2639,24 @@ function autoAssign(section, platforms, staffing) {
2683
2639
  return best ? best.name : '未分配';
2684
2640
  }
2685
2641
  /**
2686
- * 分析章节复杂度,决定优先级和工时
2642
+ * 分析章节复杂度,决定优先级和工时(简化版)
2687
2643
  */
2688
2644
  function estimateSectionComplexity(section) {
2689
2645
  const content = section.content || '';
2690
2646
  const name = section.name || '';
2691
2647
  const full = `${name}\n${content}`;
2692
- // 统计复杂度指标
2693
- const apiCount = (full.match(/\/api\/|API|接口|endpoint|POST|GET|PUT|DELETE/gi) || []).length;
2694
- const dbCount = (full.match(/数据库|表|DDL|schema|model|entity|索引|字段/gi) || []).length;
2695
- const pageCount = (full.match(/页面|表单|列表|详情|弹窗|modal|dialog/gi) || []).length;
2648
+ // 粗略估算工时(仅用于展示)
2696
2649
  const wordCount = full.length;
2697
- // 判断复杂度
2698
- let complexity = 'medium';
2699
- let score = apiCount * 3 + dbCount * 2 + pageCount;
2700
- if (score <= 3 && wordCount < 200)
2701
- complexity = 'low';
2702
- else if (score >= 10 || wordCount > 800)
2703
- complexity = 'high';
2704
- // 工时预估
2705
- const estimatedHours = complexity === 'high' ? 16 : complexity === 'medium' ? 8 : 4;
2650
+ const estimatedHours = wordCount > 800 ? 16 : wordCount > 300 ? 8 : 4;
2706
2651
  // 优先级
2707
2652
  let priority = 'medium';
2708
- if (dbCount >= 3 || apiCount >= 5 || full.includes('核心') || full.includes('基础')) {
2653
+ if (full.includes('核心') || full.includes('基础') || full.includes('安全')) {
2709
2654
  priority = 'high';
2710
2655
  }
2711
- else if (apiCount === 0 && dbCount === 0 && pageCount <= 1) {
2656
+ else if (wordCount < 200) {
2712
2657
  priority = 'low';
2713
2658
  }
2714
- return { apiCount, dbCount, pageCount, wordCount, complexity, estimatedHours, priority };
2659
+ return { estimatedHours, priority };
2715
2660
  }
2716
2661
  /**
2717
2662
  * 语义依赖检测: 比字符串匹配更准确的任务间关系
@@ -2779,6 +2724,39 @@ async function detectExistingTasks(iterDir) {
2779
2724
  await scanRecursive(targetDir);
2780
2725
  return tasks;
2781
2726
  }
2727
+ /**
2728
+ * v6.76.0+: 扫描已有 Task 的端结构
2729
+ * 返回每个 Task 已有的端目录(10-backend/{端}, 20-frontend/{端})
2730
+ */
2731
+ async function scanExistingTaskStructure(iterDir) {
2732
+ const result = new Map();
2733
+ const tasks = await detectExistingTasks(iterDir);
2734
+ for (const taskName of tasks) {
2735
+ const taskDir = (0, path_1.join)(iterDir, '030-tasks', taskName);
2736
+ if (!(await (0, fs_extra_1.pathExists)(taskDir)))
2737
+ continue;
2738
+ const platforms = [];
2739
+ // 扫描 10-backend/ 和 20-frontend/ 子目录
2740
+ for (const category of ['10-backend', '20-frontend']) {
2741
+ const catDir = (0, path_1.join)(taskDir, category);
2742
+ if (!(await (0, fs_extra_1.pathExists)(catDir)))
2743
+ continue;
2744
+ try {
2745
+ const entries = await (0, fs_extra_1.readdir)(catDir, { withFileTypes: true });
2746
+ for (const e of entries) {
2747
+ if (e.isDirectory() && !e.name.startsWith('.')) {
2748
+ platforms.push(`${category}/${e.name}`);
2749
+ }
2750
+ }
2751
+ }
2752
+ catch { }
2753
+ }
2754
+ if (platforms.length > 0) {
2755
+ result.set(taskName, platforms);
2756
+ }
2757
+ }
2758
+ return result;
2759
+ }
2782
2760
  /**
2783
2761
  * 生成任务总览报告 → 000-overview/TASK_SUMMARY.md
2784
2762
  * 包含:任务名、功能单元、人工工时、AI工时、优先级、依赖、风险
@@ -3175,13 +3153,7 @@ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
3175
3153
  section._scopePlatforms = modPlatforms;
3176
3154
  section._complexity = {
3177
3155
  estimatedHours: 8,
3178
- hoursByPlatform: {},
3179
3156
  priority: 'medium',
3180
- complexity: 'medium',
3181
- apiCount: 0,
3182
- dbCount: 0,
3183
- pageCount: 0,
3184
- wordCount: 0,
3185
3157
  };
3186
3158
  section._owner = '未分配';
3187
3159
  section._taskId = taskId;