@zhuan-ai/zhuanspec 2.11.3 → 2.11.6

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.
@@ -44,16 +44,15 @@ const WAVE_HEADER_PATTERN = /^###\s+Wave\s+(\d+)/im;
44
44
  export function checkTestCaseInquiryLogged(tasksContent) {
45
45
  const result = {
46
46
  ruleId: 'test-case-inquiry-logged',
47
- ruleName: 'Test case inquiry logged',
47
+ ruleName: '测试用例询问记录',
48
48
  passed: false,
49
49
  errors: [],
50
50
  warnings: [],
51
51
  };
52
52
  const sectionMatch = tasksContent.match(TEST_CASE_SOURCE_LOG_PATTERN);
53
53
  if (!sectionMatch) {
54
- result.errors.push('Missing "## Test Case Source Log" section — the AI must ask the user about test cases ' +
55
- 'at the start of the proposal phase and record the result here before writing any spec/task content. ' +
56
- 'Add a section with at least: "用户回答: 提供/跳过" and "TDD 模式: 启用/禁用".');
54
+ result.errors.push('缺少 "## Test Case Source Log" 章节 — AI 必须在提案阶段开始时询问用户测试用例并记录结果。' +
55
+ '至少包含:"用户回答: 提供/跳过" 和 "TDD 模式: 启用/禁用"。');
57
56
  return result;
58
57
  }
59
58
  const sectionStartIndex = sectionMatch.index + sectionMatch[0].length;
@@ -64,11 +63,11 @@ export function checkTestCaseInquiryLogged(tasksContent) {
64
63
  const sectionContent = tasksContent.slice(sectionStartIndex, sectionEndIndex);
65
64
  const contentWithoutComments = sectionContent.replace(HTML_COMMENT_PATTERN, '').trim();
66
65
  if (!contentWithoutComments) {
67
- result.errors.push('"## Test Case Source Log" is empty — record the inquiry outcome (user answer + TDD mode status)');
66
+ result.errors.push('"## Test Case Source Log" 为空 — 请记录询问结果(用户回答 + TDD 模式状态)');
68
67
  return result;
69
68
  }
70
69
  if (UNRESOLVED_MARKERS.test(contentWithoutComments)) {
71
- result.errors.push('"## Test Case Source Log" contains unresolved markers (TODO / TBD / PENDING)');
70
+ result.errors.push('"## Test Case Source Log" 包含未解决的标记(TODO / TBD / PENDING)');
72
71
  return result;
73
72
  }
74
73
  const hasDeclaredStatus = TDD_MODE_DECLARED_PATTERNS.some(p => p.test(contentWithoutComments));
@@ -77,7 +76,7 @@ export function checkTestCaseInquiryLogged(tasksContent) {
77
76
  result.passed = true;
78
77
  }
79
78
  else {
80
- result.errors.push('"## Test Case Source Log" is too brief — include at least the user\'s answer and TDD mode status');
79
+ result.errors.push('"## Test Case Source Log" 内容过简 — 至少包含用户回答和 TDD 模式状态');
81
80
  }
82
81
  return result;
83
82
  }
@@ -98,7 +97,7 @@ export function checkTestCaseInquiryLogged(tasksContent) {
98
97
  export function checkPreClarification(tasksContent) {
99
98
  const result = {
100
99
  ruleId: 'pre-clarification-completed',
101
- ruleName: 'Pre-clarification completed',
100
+ ruleName: '前置澄清记录',
102
101
  passed: false,
103
102
  errors: [],
104
103
  warnings: [],
@@ -106,7 +105,7 @@ export function checkPreClarification(tasksContent) {
106
105
  // Check if section exists
107
106
  const sectionMatch = tasksContent.match(PRE_CLARIFICATION_SECTION_PATTERN);
108
107
  if (!sectionMatch) {
109
- result.errors.push('Pre-Clarification Log section is missing');
108
+ result.errors.push('Pre-Clarification Log 章节缺失');
110
109
  return result;
111
110
  }
112
111
  // Extract section content (from header to next ## or end)
@@ -120,12 +119,12 @@ export function checkPreClarification(tasksContent) {
120
119
  const contentWithoutComments = sectionContent.replace(HTML_COMMENT_PATTERN, '').trim();
121
120
  // Check if section is empty
122
121
  if (!contentWithoutComments) {
123
- result.errors.push('Pre-Clarification Log section is empty or contains only HTML comments');
122
+ result.errors.push('Pre-Clarification Log 章节为空或仅包含 HTML 注释');
124
123
  return result;
125
124
  }
126
125
  // Check for unresolved markers
127
126
  if (UNRESOLVED_MARKERS.test(contentWithoutComments)) {
128
- result.errors.push('Pre-Clarification Log contains unresolved markers (TODO, TBD, PENDING, or ?)');
127
+ result.errors.push('Pre-Clarification Log 包含未解决的标记(TODO、TBD、PENDING 或 ?)');
129
128
  return result;
130
129
  }
131
130
  // Check for valid completion markers or actual content
@@ -135,7 +134,7 @@ export function checkPreClarification(tasksContent) {
135
134
  result.passed = true;
136
135
  }
137
136
  else {
138
- result.errors.push('Pre-Clarification Log does not contain valid completion markers or substantive Q&A content');
137
+ result.errors.push('Pre-Clarification Log 未包含有效的完成标记或实质性问答内容');
139
138
  }
140
139
  return result;
141
140
  }
@@ -152,7 +151,7 @@ export function checkPreClarification(tasksContent) {
152
151
  export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames) {
153
152
  const result = {
154
153
  ruleId: 'skill-tags-valid',
155
- ruleName: 'Skill tags valid',
154
+ ruleName: 'Skill 标签校验',
156
155
  passed: false,
157
156
  errors: [],
158
157
  warnings: [],
@@ -160,7 +159,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
160
159
  // a. Check Skill Mapping section exists with table data
161
160
  const sectionMatch = tasksContent.match(SKILL_MAPPING_SECTION_PATTERN);
162
161
  if (!sectionMatch) {
163
- result.errors.push('Skill Mapping section not found');
162
+ result.errors.push('Skill Mapping 章节未找到');
164
163
  }
165
164
  else {
166
165
  // Extract section content (from header to next ## or end)
@@ -183,7 +182,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
183
182
  });
184
183
  // Need at least 2 rows (header + 1 data row)
185
184
  if (!dataRows || dataRows.length < 2) {
186
- result.errors.push('Skill Mapping table has no data rows');
185
+ result.errors.push('Skill Mapping 表格无数据行');
187
186
  }
188
187
  }
189
188
  // If no tasks, consider it valid
@@ -196,7 +195,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
196
195
  for (const task of parsedTasks) {
197
196
  // b. Check if task has skill tag
198
197
  if (task.skills.length === 0) {
199
- result.errors.push(`Task ${task.id}: missing @skill tag`);
198
+ result.errors.push(`任务 ${task.id}: 缺少 @skill 标签`);
200
199
  continue;
201
200
  }
202
201
  // Validate each skill
@@ -214,7 +213,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
214
213
  descLower.includes('verify') ||
215
214
  task.description.length > 30; // Substantial description is acceptable
216
215
  if (!hasReason) {
217
- result.warnings.push(`Task ${task.id}: @skill:none should have a reason in description`);
216
+ result.warnings.push(`任务 ${task.id}: @skill:none 应在描述中说明原因`);
218
217
  }
219
218
  continue;
220
219
  }
@@ -223,7 +222,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
223
222
  const availableSkills = knownSkillNames.length <= 10
224
223
  ? knownSkillNames.join(', ')
225
224
  : `${knownSkillNames.slice(0, 10).join(', ')}... (${knownSkillNames.length} total)`;
226
- result.errors.push(`Task ${task.id}: @skill:${skill} is not a known skill. Available skills: ${availableSkills}`);
225
+ result.errors.push(`任务 ${task.id}: @skill:${skill} 不是已知的 skill。可用 skills: ${availableSkills}`);
227
226
  }
228
227
  }
229
228
  }
@@ -242,7 +241,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
242
241
  export function checkTaskOrderingByWave(tasksContent, parsedTasks) {
243
242
  const result = {
244
243
  ruleId: 'task-ordering-by-wave',
245
- ruleName: 'Task ordering by wave',
244
+ ruleName: '任务 Wave 排序校验',
246
245
  passed: false,
247
246
  errors: [],
248
247
  warnings: [],
@@ -263,13 +262,13 @@ export function checkTaskOrderingByWave(tasksContent, parsedTasks) {
263
262
  });
264
263
  }
265
264
  if (waveHeaders.length === 0) {
266
- result.errors.push('No Wave headers found in tasks.md. Expected format: "### Wave N"');
265
+ result.errors.push('tasks.md 中未找到 Wave 头部。期望格式: "### Wave N"');
267
266
  return result;
268
267
  }
269
268
  // Generate execution plan to get actual waves
270
269
  const executionPlan = generateExecutionPlan(parsedTasks);
271
270
  if (executionPlan.hasCycle) {
272
- result.errors.push('Task dependency graph has cycles, cannot validate wave ordering');
271
+ result.errors.push('任务依赖图存在循环,无法校验 Wave 排序');
273
272
  return result;
274
273
  }
275
274
  // Build a map of task ID to actual wave number
@@ -302,7 +301,7 @@ export function checkTaskOrderingByWave(tasksContent, parsedTasks) {
302
301
  let lastWave = 0;
303
302
  for (const task of taskPositions) {
304
303
  if (task.actualWave < lastWave) {
305
- result.errors.push(`Task ${task.id} (wave ${task.actualWave}) appears after tasks of wave ${lastWave}`);
304
+ result.errors.push(`任务 ${task.id}(Wave ${task.actualWave})出现在 Wave ${lastWave} 的任务之后`);
306
305
  }
307
306
  lastWave = Math.max(lastWave, task.actualWave);
308
307
  }
@@ -311,18 +310,18 @@ export function checkTaskOrderingByWave(tasksContent, parsedTasks) {
311
310
  const declaredWaveNumbers = new Set(waveHeaders.map(h => h.wave));
312
311
  for (const declared of declaredWaveNumbers) {
313
312
  if (!actualWaveNumbers.has(declared)) {
314
- result.warnings.push(`Wave header "### Wave ${declared}" does not match any computed wave`);
313
+ result.warnings.push(`Wave 头部 "### Wave ${declared}" 与计算得到的 Wave 不匹配`);
315
314
  }
316
315
  }
317
316
  for (const actual of actualWaveNumbers) {
318
317
  if (!declaredWaveNumbers.has(actual)) {
319
- result.errors.push(`Missing Wave header for computed wave ${actual}`);
318
+ result.errors.push(`缺少计算得到的 Wave ${actual} 的头部`);
320
319
  }
321
320
  }
322
321
  // Check wave header ordering
323
322
  for (let i = 1; i < waveHeaders.length; i++) {
324
323
  if (waveHeaders[i].wave <= waveHeaders[i - 1].wave) {
325
- result.errors.push(`Wave headers are not in ascending order: Wave ${waveHeaders[i - 1].wave} followed by Wave ${waveHeaders[i].wave}`);
324
+ result.errors.push(`Wave 头部未按升序排列: Wave ${waveHeaders[i - 1].wave} 后面是 Wave ${waveHeaders[i].wave}`);
326
325
  }
327
326
  }
328
327
  result.passed = result.errors.length === 0;
@@ -341,7 +340,7 @@ export function checkTaskOrderingByWave(tasksContent, parsedTasks) {
341
340
  export function checkSubagentDirectives(parsedTasks) {
342
341
  const result = {
343
342
  ruleId: 'subagent-directives-valid',
344
- ruleName: 'Subagent directives valid',
343
+ ruleName: '子代理指令校验',
345
344
  passed: false,
346
345
  errors: [],
347
346
  warnings: [],
@@ -354,7 +353,7 @@ export function checkSubagentDirectives(parsedTasks) {
354
353
  // Generate execution plan
355
354
  const executionPlan = generateExecutionPlan(parsedTasks);
356
355
  if (executionPlan.hasCycle) {
357
- result.errors.push('Task dependency graph has cycles, cannot validate subagent directives');
356
+ result.errors.push('任务依赖图存在循环,无法校验子代理指令');
358
357
  return result;
359
358
  }
360
359
  if (executionPlan.waves.length === 0) {
@@ -370,24 +369,24 @@ export function checkSubagentDirectives(parsedTasks) {
370
369
  // Check subagent_count attribute exists with correct value
371
370
  const subagentCountPattern = new RegExp(`<wave[^>]*number="${wave.wave}"[^>]*subagent_count="${taskCount}"`);
372
371
  if (!subagentCountPattern.test(xml)) {
373
- result.errors.push(`Wave ${wave.wave}: missing or incorrect subagent_count attribute (expected ${taskCount})`);
372
+ result.errors.push(`Wave ${wave.wave}: 缺少或错误的 subagent_count 属性(期望 ${taskCount})`);
374
373
  }
375
374
  // Check execution_mode attribute exists with correct value
376
375
  const executionModePattern = new RegExp(`<wave[^>]*number="${wave.wave}"[^>]*execution_mode="${expectedMode}"`);
377
376
  if (!executionModePattern.test(xml)) {
378
- result.errors.push(`Wave ${wave.wave}: missing or incorrect execution_mode attribute (expected "${expectedMode}")`);
377
+ result.errors.push(`Wave ${wave.wave}: 缺少或错误的 execution_mode 属性(期望 "${expectedMode}")`);
379
378
  }
380
379
  }
381
380
  // Validate scheduling_summary section exists
382
381
  if (!xml.includes('<scheduling_summary>')) {
383
- result.errors.push('Missing <scheduling_summary> section in orchestration_strategy');
382
+ result.errors.push('编排策略中缺少 <scheduling_summary> 节点');
384
383
  }
385
384
  else {
386
385
  // Validate each wave has a scheduling summary entry
387
386
  for (const wave of executionPlan.waves) {
388
387
  const waveSummaryPattern = new RegExp(`Wave ${wave.wave} \u2192`);
389
388
  if (!waveSummaryPattern.test(xml)) {
390
- result.errors.push(`Wave ${wave.wave}: missing scheduling summary entry in <scheduling_summary>`);
389
+ result.errors.push(`Wave ${wave.wave}: <scheduling_summary> 中缺少调度摘要条目`);
391
390
  }
392
391
  }
393
392
  }
@@ -402,7 +401,7 @@ export function checkSubagentDirectives(parsedTasks) {
402
401
  export function checkReviewGateDefined(tasksContent) {
403
402
  const result = {
404
403
  ruleId: 'review-gate-defined',
405
- ruleName: 'Review gate defined',
404
+ ruleName: 'Review 门禁定义',
406
405
  passed: false,
407
406
  errors: [],
408
407
  warnings: [],
@@ -411,7 +410,7 @@ export function checkReviewGateDefined(tasksContent) {
411
410
  /review\s*gate/i.test(tasksContent) ||
412
411
  /双轨审查/.test(tasksContent);
413
412
  if (!hasReviewGate) {
414
- result.errors.push('tasks.md must include a zhuanspec review gate before completion');
413
+ result.errors.push('tasks.md 必须包含 zhuanspec review 门禁');
415
414
  return result;
416
415
  }
417
416
  result.passed = true;
@@ -428,7 +427,7 @@ export function checkReviewGateDefined(tasksContent) {
428
427
  export function checkProposalFormatValid(proposalContent) {
429
428
  const result = {
430
429
  ruleId: 'proposal-format-valid',
431
- ruleName: 'Proposal format valid',
430
+ ruleName: '提案格式校验',
432
431
  passed: false,
433
432
  errors: [],
434
433
  warnings: [],
@@ -571,7 +570,7 @@ function extractScenarioDimensions(name, description) {
571
570
  function checkSpecCodeConsistent(specsDir, repoRoot, phase) {
572
571
  const result = {
573
572
  ruleId: 'spec-code-consistent',
574
- ruleName: 'Spec-Code consistency',
573
+ ruleName: 'Spec-代码一致性',
575
574
  passed: false,
576
575
  errors: [],
577
576
  warnings: [],
@@ -579,7 +578,7 @@ function checkSpecCodeConsistent(specsDir, repoRoot, phase) {
579
578
  // Skip this check in Propose phase
580
579
  if (phase === 'propose') {
581
580
  result.passed = true;
582
- result.warnings.push('SKIPPED (Propose phase) - no code changes to validate');
581
+ result.warnings.push('跳过(Propose 阶段)— 还没有代码变更可校验');
583
582
  return result;
584
583
  }
585
584
  // If specsDir doesn't exist, skip this check
@@ -756,7 +755,7 @@ function checkSpecCodeConsistent(specsDir, repoRoot, phase) {
756
755
  export function checkFileConflicts(parsedTasks) {
757
756
  const result = {
758
757
  ruleId: 'file-conflict-detection',
759
- ruleName: 'File conflict detection',
758
+ ruleName: '文件冲突检测',
760
759
  passed: true,
761
760
  errors: [],
762
761
  warnings: [],
@@ -784,8 +783,8 @@ export function checkFileConflicts(parsedTasks) {
784
783
  for (const [file, taskIds] of fileToTasks) {
785
784
  if (taskIds.length >= 2) {
786
785
  result.passed = false;
787
- result.errors.push(`File conflict in Wave ${wave}: "${file}" is modified by tasks [${taskIds.join(', ')}]. ` +
788
- `Add @depends to specify execution order or tasks will be serialized automatically.`);
786
+ result.errors.push(`Wave ${wave} 文件冲突: "${file}" 被任务 [${taskIds.join(', ')}] 同时修改。` +
787
+ `请添加 @depends 指定执行顺序,否则任务将自动串行化。`);
789
788
  }
790
789
  }
791
790
  }
@@ -805,7 +804,7 @@ export function checkFileConflicts(parsedTasks) {
805
804
  export function checkTestCaseCoverage(tasksContent, hasTestCase = false) {
806
805
  const result = {
807
806
  ruleId: 'test-case-coverage',
808
- ruleName: 'Test case coverage',
807
+ ruleName: '测试用例覆盖',
809
808
  passed: false,
810
809
  errors: [],
811
810
  warnings: [],
@@ -813,13 +812,13 @@ export function checkTestCaseCoverage(tasksContent, hasTestCase = false) {
813
812
  // If no test case mode, skip with warning
814
813
  if (!hasTestCase) {
815
814
  result.passed = true;
816
- result.warnings.push('SKIPPED (no test case mode) - test-case-coverage check only applies when test cases are provided');
815
+ result.warnings.push('跳过(无测试用例模式)— test-case-coverage 检查仅在提供测试用例时生效');
817
816
  return result;
818
817
  }
819
818
  // Check for Test Case Coverage section (required when hasTestCase=true)
820
819
  const testCaseSectionPattern = /^##\s+Test\s+Case\s+Coverage/im;
821
820
  if (!testCaseSectionPattern.test(tasksContent)) {
822
- result.errors.push('Missing Test Case Coverage section - REQUIRED when test cases are provided (TDD mode or user-provided cases)');
821
+ result.errors.push('缺少 Test Case Coverage 章节 — 提供测试用例时必须填写(TDD 模式或用户提供)');
823
822
  return result;
824
823
  }
825
824
  // Check for @test-case tags in tasks
@@ -829,7 +828,7 @@ export function checkTestCaseCoverage(tasksContent, hasTestCase = false) {
829
828
  result.passed = true;
830
829
  }
831
830
  else {
832
- result.errors.push('No @test-case tags found - REQUIRED when test cases are provided. Link tasks to test cases using @test-case:TC-XXX');
831
+ result.errors.push('未找到 @test-case 标签 — 提供测试用例时必须使用 @test-case:TC-XXX 关联任务与测试用例');
833
832
  }
834
833
  // Validate Test Case Coverage table format (four columns required)
835
834
  // Expected headers: Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景
@@ -841,7 +840,7 @@ export function checkTestCaseCoverage(tasksContent, hasTestCase = false) {
841
840
  if (headerLine) {
842
841
  const columns = headerLine.split('|').filter(col => col.trim() !== '');
843
842
  if (columns.length < 4) {
844
- result.warnings.push(`Test Case Coverage table should have 4 columns (Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景), found ${columns.length}`);
843
+ result.warnings.push(`Test Case Coverage 表格应有 4 列(Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景),当前 ${columns.length} 列`);
845
844
  }
846
845
  else {
847
846
  // Check column names contain expected keywords
@@ -851,13 +850,13 @@ export function checkTestCaseCoverage(tasksContent, hasTestCase = false) {
851
850
  const hasPathCol = headerText.includes('文件路径') || headerText.includes('path') || headerText.includes('路径');
852
851
  const hasScenarioCol = headerText.includes('覆盖场景') || headerText.includes('场景') || headerText.includes('scenario');
853
852
  if (!hasTaskCol || !hasTestCaseIdCol || !hasPathCol || !hasScenarioCol) {
854
- result.warnings.push('Test Case Coverage table headers should follow format: Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景');
853
+ result.warnings.push('Test Case Coverage 表格列头格式不匹配,应为: Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景');
855
854
  }
856
855
  }
857
856
  }
858
857
  }
859
858
  else {
860
- result.warnings.push('Test Case Coverage table format not detected - ensure table has 4 columns with proper headers');
859
+ result.warnings.push('Test Case Coverage 表格格式未检测到 — 请确保表格有 4 列且列头正确');
861
860
  }
862
861
  return result;
863
862
  }
@@ -866,30 +865,64 @@ export function checkTestCaseCoverage(tasksContent, hasTestCase = false) {
866
865
  *
867
866
  * When a tech-spec document exists, validates that the technical spec
868
867
  * adequately covers the proposal's requirements.
869
- * This is a basic framework - full logic will be implemented in Task 7's Skill.
870
868
  *
871
- * Tech spec document lookup order:
872
- * 1. techDesign/tech-spec.md (standard path)
873
- * 2. tech-spec.md in the proposal root directory
874
- * 3. 用户提供本地技术方案路径
869
+ * Tech spec document lookup order (priority):
870
+ * 1. 用户通过 --tech-spec 指定的外部路径 (externalTechSpecPath)
871
+ * 2. techDesign/ 目录下的任意 .md 文件(文件名不限定为 tech-spec.md)
872
+ * 3. 提案根目录下的任意技术方案相关 .md 文件
875
873
  *
876
874
  * Checks:
877
- * - tech-spec.md file exists and is non-empty
878
- * - tech-spec.md contains substantive content (not just placeholder)
875
+ * - tech-spec file exists and is non-empty
876
+ * - tech-spec contains substantive content (not just placeholder)
877
+ * - structural elements (DB tables, APIs, classes, etc.) are covered in proposal files
879
878
  */
880
- export function checkTechSpecCoverage(changeDir) {
879
+ export function checkTechSpecCoverage(changeDir, externalTechSpecPath) {
881
880
  const result = {
882
881
  ruleId: 'tech-spec-coverage',
883
- ruleName: 'Tech Spec Coverage',
882
+ ruleName: '技术方案覆盖率',
884
883
  passed: false,
885
884
  errors: [],
886
885
  warnings: [],
887
886
  };
888
- // Look for tech-spec.md from multiple sources
889
- const candidatePaths = [
890
- join(changeDir, 'techDesign', 'tech-spec.md'), // Standard path
891
- join(changeDir, 'tech-spec.md'), // Proposal root directory
892
- ];
887
+ // Look for tech-spec from multiple sources (priority order):
888
+ // 1. User-specified external path via --tech-spec
889
+ // 2. Any .md file under techDesign/ directory (filename NOT restricted to tech-spec.md)
890
+ // 3. Any tech-spec-like .md file in proposal root directory
891
+ const candidatePaths = [];
892
+ if (externalTechSpecPath) {
893
+ // Resolve external path: could be absolute or relative to cwd
894
+ const resolved = path.isAbsolute(externalTechSpecPath)
895
+ ? externalTechSpecPath
896
+ : join(process.cwd(), externalTechSpecPath);
897
+ candidatePaths.push(resolved);
898
+ }
899
+ // Scan techDesign/ directory for any .md files (not just tech-spec.md)
900
+ const techDesignDir = join(changeDir, 'techDesign');
901
+ if (existsSync(techDesignDir)) {
902
+ try {
903
+ const entries = readdirSync(techDesignDir);
904
+ for (const entry of entries) {
905
+ if (entry.endsWith('.md')) {
906
+ candidatePaths.push(join(techDesignDir, entry));
907
+ }
908
+ }
909
+ }
910
+ catch {
911
+ // Ignore read errors
912
+ }
913
+ }
914
+ // Also check proposal root for any tech-spec-like files
915
+ try {
916
+ const rootEntries = readdirSync(changeDir);
917
+ for (const entry of rootEntries) {
918
+ if (entry.endsWith('.md') && entry !== 'design.md' && entry !== 'tasks.md' && entry !== 'proposal.md') {
919
+ candidatePaths.push(join(changeDir, entry));
920
+ }
921
+ }
922
+ }
923
+ catch {
924
+ // Ignore read errors
925
+ }
893
926
  let techSpecPath = null;
894
927
  for (const candidate of candidatePaths) {
895
928
  if (existsSync(candidate)) {
@@ -898,9 +931,9 @@ export function checkTechSpecCoverage(changeDir) {
898
931
  }
899
932
  }
900
933
  if (!techSpecPath) {
901
- // No tech-spec.md found anywhere — skip check gracefully
934
+ // No tech-spec found anywhere — skip check gracefully
902
935
  result.passed = true;
903
- result.warnings.push('No tech-spec.md found (checked techDesign/tech-spec.md and proposal root) — skipping coverage check');
936
+ result.warnings.push('未找到技术方案文档(已检查外部路径、techDesign/tech-spec.md 和提案根目录)— 跳过覆盖率检查');
904
937
  return result;
905
938
  }
906
939
  let content;
@@ -908,13 +941,13 @@ export function checkTechSpecCoverage(changeDir) {
908
941
  content = readFileSync(techSpecPath, 'utf-8');
909
942
  }
910
943
  catch {
911
- result.errors.push(`Failed to read ${path.relative(changeDir, techSpecPath)}`);
944
+ result.errors.push(`无法读取 ${path.relative(changeDir, techSpecPath)}`);
912
945
  return result;
913
946
  }
914
947
  // Check if content is just the placeholder template
915
948
  const stripped = content.replace(/^#.*$/gm, '').replace(/^>.*$/gm, '').replace(/^(created|source):.*$/gm, '').trim();
916
949
  if (stripped.length < 50) {
917
- result.warnings.push('techDesign/tech-spec.md appears to be a placeholder — populate with actual technical spec content');
950
+ result.warnings.push('techDesign/tech-spec.md 似乎是占位模板 — 请填充实际技术方案内容');
918
951
  result.passed = true;
919
952
  return result;
920
953
  }
@@ -923,7 +956,7 @@ export function checkTechSpecCoverage(changeDir) {
923
956
  const elements = extractTechSpecElements(content);
924
957
  const totalElements = elements.length;
925
958
  if (totalElements === 0) {
926
- result.warnings.push('No structured elements (tables, APIs, classes, configs) detected in tech-spec.md — skipping coverage check');
959
+ result.warnings.push('tech-spec.md 中未检测到结构化元素(表、API、类、配置)— 跳过覆盖率检查');
927
960
  result.passed = true;
928
961
  return result;
929
962
  }
@@ -1160,7 +1193,7 @@ const TASK_ID_PATTERN = /^\d+\.\d+$/;
1160
1193
  export function checkTaskIdFormat(tasksContent) {
1161
1194
  const result = {
1162
1195
  ruleId: 'task-id-format',
1163
- ruleName: 'Task ID format',
1196
+ ruleName: '任务 ID 格式',
1164
1197
  passed: false,
1165
1198
  errors: [],
1166
1199
  warnings: [],
@@ -1174,11 +1207,83 @@ export function checkTaskIdFormat(tasksContent) {
1174
1207
  }
1175
1208
  }
1176
1209
  if (invalidIds.length > 0) {
1177
- result.errors = invalidIds.map(id => `Invalid task ID "${id}": must be N.M format (e.g., 1.1, 2.3)`);
1210
+ result.errors = invalidIds.map(id => `无效任务 ID "${id}": 必须为 N.M 格式(如 1.1, 2.3)`);
1178
1211
  }
1179
1212
  result.passed = invalidIds.length === 0;
1180
1213
  return result;
1181
1214
  }
1215
+ /**
1216
+ * Rule: task-checkbox-format
1217
+ *
1218
+ * Validates that all tasks under Wave headers use standard GitHub Flavored Markdown
1219
+ * checkbox format: `- [ ] N.M description` or `- [x] N.M description`.
1220
+ * Rejects legacy formats like `#### Task N.M:` headers or `✅` emoji as completion marker.
1221
+ * This ensures automated progress tracking (parseTasks, record-progress, review gate) works correctly.
1222
+ */
1223
+ export function checkTaskCheckboxFormat(tasksContent) {
1224
+ const result = {
1225
+ ruleId: 'task-checkbox-format',
1226
+ ruleName: '任务复选框格式',
1227
+ passed: false,
1228
+ errors: [],
1229
+ warnings: [],
1230
+ };
1231
+ // 1. Check for legacy #### Task headers (should not be used)
1232
+ const legacyTaskHeaders = tasksContent.match(/^####\s*(?:Task\s+\d+\.\d+|T\d+)\s*:.+$/gm) || [];
1233
+ if (legacyTaskHeaders.length > 0) {
1234
+ for (const header of legacyTaskHeaders) {
1235
+ result.errors.push(`检测到旧格式任务: "${header.trim()}"。` +
1236
+ `必须使用复选框格式: "- [ ] N.M 描述" 或 "- [x] N.M 描述"`);
1237
+ }
1238
+ }
1239
+ // 2. Find all lines under Wave headers that look like tasks but don't use checkbox format
1240
+ const lines = tasksContent.split('\n');
1241
+ let inWaveSection = false;
1242
+ for (const line of lines) {
1243
+ // Detect Wave header
1244
+ if (/^###\s+Wave\s+\d+/i.test(line)) {
1245
+ inWaveSection = true;
1246
+ continue;
1247
+ }
1248
+ // Detect next non-Wave section header (## or ### not Wave)
1249
+ if (/^#{2,3}\s+(?!Wave\s+\d+)/i.test(line)) {
1250
+ inWaveSection = false;
1251
+ continue;
1252
+ }
1253
+ if (!inWaveSection)
1254
+ continue;
1255
+ // Skip empty lines, comments, and non-list lines
1256
+ const trimmed = line.trim();
1257
+ if (!trimmed || trimmed.startsWith('<!--') || !trimmed.startsWith('-'))
1258
+ continue;
1259
+ // Line starts with - but is NOT a valid checkbox format
1260
+ if (/^-\s+(?!\[[ xX]\])\S/.test(trimmed)) {
1261
+ // Only flag lines that look like task items (have N.M id or substantial content)
1262
+ if (/^-\s+\d+\.\d+\s/.test(trimmed) || /^-\s+[A-Z]/.test(trimmed)) {
1263
+ result.errors.push(`任务行缺少复选框: "${trimmed.substring(0, 80)}"。` +
1264
+ `必须使用复选框格式: "- [ ] N.M 描述" 或 "- [x] N.M 描述"`);
1265
+ }
1266
+ }
1267
+ }
1268
+ // 3. Verify checkbox tasks exist (at least one task under Wave headers)
1269
+ const checkboxTasks = tasksContent.match(/^-\s+\[[ xX]\]\s+\d+\.\d+\s+.+$/gm) || [];
1270
+ if (checkboxTasks.length === 0 && legacyTaskHeaders.length === 0) {
1271
+ // No checkbox tasks found - this is an ERROR because all tasks MUST use checkbox format
1272
+ result.errors.push('Wave 头部下未找到复选框格式的任务。' +
1273
+ '所有任务必须使用 GitHub Flavored Markdown 复选框格式: "- [ ] N.M 描述"(待办)或 "- [x] N.M 描述"(已完成)。' +
1274
+ '禁止使用 #### 头部、纯文本或 emoji 标记。');
1275
+ }
1276
+ // 4. Check that completed tasks use [x] not ✅ emoji on checkbox lines
1277
+ const checkboxWithEmoji = tasksContent.match(/^-\s+\[[ ]\]\s+.+\u2705\s*$/gm) || [];
1278
+ if (checkboxWithEmoji.length > 0) {
1279
+ for (const line of checkboxWithEmoji) {
1280
+ result.warnings.push(`任务使用了 ✅ emoji 但复选框未勾选: "${line.trim().substring(0, 80)}"。` +
1281
+ `请使用 "- [x]" 标记完成,而非 ✅ emoji`);
1282
+ }
1283
+ }
1284
+ result.passed = result.errors.length === 0;
1285
+ return result;
1286
+ }
1182
1287
  /**
1183
1288
  * Main entry: Run all strict validation rules
1184
1289
  */
@@ -1194,17 +1299,30 @@ export async function runStrictValidation(tasksContent, parsedTasks, knownSkillN
1194
1299
  checkFileConflicts(parsedTasks),
1195
1300
  checkTestCaseCoverage(tasksContent, hasTestCase),
1196
1301
  checkTaskIdFormat(tasksContent),
1302
+ checkTaskCheckboxFormat(tasksContent),
1197
1303
  ];
1198
1304
  // Add new rules if parameters are provided
1199
1305
  if (options?.proposalContent) {
1200
1306
  checks.push(checkProposalFormatValid(options.proposalContent));
1201
1307
  }
1202
1308
  // Add tech-spec-coverage rule if any tech spec document exists
1309
+ // Trigger when: techDesign/ directory has any .md files OR external --tech-spec path provided
1203
1310
  if (options?.changeDir) {
1204
- const hasTechSpec = existsSync(join(options.changeDir, 'techDesign', 'tech-spec.md')) ||
1205
- existsSync(join(options.changeDir, 'tech-spec.md'));
1311
+ let hasTechSpec = !!options.techSpecPath;
1312
+ if (!hasTechSpec) {
1313
+ const techDesignDir = join(options.changeDir, 'techDesign');
1314
+ if (existsSync(techDesignDir)) {
1315
+ try {
1316
+ const entries = readdirSync(techDesignDir);
1317
+ hasTechSpec = entries.some((e) => e.endsWith('.md'));
1318
+ }
1319
+ catch {
1320
+ // Ignore
1321
+ }
1322
+ }
1323
+ }
1206
1324
  if (hasTechSpec) {
1207
- checks.push(checkTechSpecCoverage(options.changeDir));
1325
+ checks.push(checkTechSpecCoverage(options.changeDir, options.techSpecPath));
1208
1326
  }
1209
1327
  }
1210
1328
  // Detect current phase for spec-code consistency check
@@ -324,12 +324,33 @@ export class Validator {
324
324
  // No metadata file - assume spec-driven (no test cases)
325
325
  }
326
326
  }
327
+ // Detect external tech-spec path from @ref tags in tasks.md
328
+ // Format: @ref:/path/to/tech-spec.md#section or @ref:techDesign/tech-spec.md#section
329
+ // Note: the tech spec filename is NOT limited to 'tech-spec.md' — any .md file
330
+ // referenced via @ref that is NOT under the standard techDesign/ directory
331
+ // is treated as a potential external tech spec document.
332
+ let techSpecPath;
333
+ const refMatches = tasksContent.match(/@ref:([^\s#]+)/g);
334
+ if (refMatches) {
335
+ for (const refTag of refMatches) {
336
+ const refPath = refTag.replace('@ref:', '');
337
+ // Skip standard techDesign/ paths — those are already handled by default lookup
338
+ if (refPath.startsWith('techDesign/'))
339
+ continue;
340
+ // Any external .md file is a potential tech spec (filename is not restricted)
341
+ if (refPath.endsWith('.md')) {
342
+ techSpecPath = refPath;
343
+ break;
344
+ }
345
+ }
346
+ }
327
347
  const strictResult = await runStrictValidation(tasksContent, parsedTasks, knownSkillNames, {
328
348
  proposalContent,
329
349
  specsDir,
330
350
  repoRoot,
331
351
  changeDir,
332
352
  hasTestCase,
353
+ techSpecPath,
333
354
  });
334
355
  strictChecks = strictResult.checks;
335
356
  // Merge strict errors into main issues
@@ -2,8 +2,8 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
4
4
  const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
5
- const TASK_HEADER_PATTERN = /^####\s*(?:Task\s+\d+\.\d+|T\d+)\s*:/;
6
- const COMPLETED_HEADER_PATTERN = /^####\s*(?:Task\s+\d+\.\d+|T\d+)\s*:.*\u2705\s*$/;
5
+ const TASK_HEADER_PATTERN = /^####\s*(?:Task\s+)?\d+\.\d+[\s:]|^####\s*T\d+\s*:/;
6
+ const COMPLETED_HEADER_PATTERN = /^####\s*(?:Task\s+)?\d+\.\d+[\s:].*\u2705|^####\s*T\d+\s*:.*\u2705\s*$/;
7
7
  export function countTasksFromContent(content) {
8
8
  const lines = content.split('\n');
9
9
  let total = 0;