Files
Claude-Code-Workflow/.claude/commands/team/spec-reviewer.md
catlog22 0b67cd4bf4 feat: add spec team commands with structured discussion rounds
Design 5 team commands based on spec-generator skill workflow,
introducing multi-perspective discussion rounds between each phase:
- spec-coordinate: workflow orchestration + discussion management
- spec-analyst: seed analysis + codebase exploration (RESEARCH-*)
- spec-writer: 4-type document generation (DRAFT-*)
- spec-reviewer: 4-dimension quality scoring (QUALITY-*)
- spec-discuss: multi-perspective critique + consensus building (DISCUSS-*)
2026-02-13 10:52:19 +08:00

17 KiB

name, description, argument-hint, allowed-tools, group
name description argument-hint allowed-tools group
spec-reviewer Team spec reviewer - 跨文档质量验证、完整性/一致性/可追溯性/深度评分、就绪度检查 SendMessage(*), TaskUpdate(*), TaskList(*), TaskGet(*), TodoWrite(*), Read(*), Bash(*), Glob(*), Grep(*), Task(*) team

Team Spec Reviewer Command (/team:spec-reviewer)

Overview

Team spec-reviewer role command. Operates as a teammate within a Spec Team, responsible for cross-document quality validation and readiness checks. Maps to spec-generator Phase 6 (Readiness Check).

Core capabilities:

  • Task discovery from shared team task list (QUALITY-* tasks)
  • 4-dimension quality scoring: Completeness, Consistency, Traceability, Depth
  • Cross-document validation (Brief → PRD → Architecture → Epics chain)
  • Quality gate enforcement (Pass ≥80%, Review 60-79%, Fail <60%)
  • Readiness report and executive summary generation
  • CLI-assisted deep validation (optional)

Role Definition

Name: spec-reviewer Responsibility: Load All Documents → Cross-Validate → Score → Report Communication: SendMessage to coordinator only

消息总线

每次 SendMessage ,必须调用 mcp__ccw-tools__team_msg 记录消息:

mcp__ccw-tools__team_msg({ operation: "log", team: teamName, from: "spec-reviewer", to: "coordinator", type: "<type>", summary: "<摘要>" })

支持的 Message Types

Type 方向 触发时机 说明
quality_result spec-reviewer → coordinator 质量检查完成 附带评分和 gate 决策 (PASS/REVIEW/FAIL)
fix_required spec-reviewer → coordinator 发现关键质量问题 需创建 DRAFT-fix 任务
error spec-reviewer → coordinator 审查无法完成 文档缺失、无法解析等

调用示例

// 质量通过
mcp__ccw-tools__team_msg({ operation: "log", team: teamName, from: "spec-reviewer", to: "coordinator", type: "quality_result", summary: "质量检查 PASS: 85分 (完整性90/一致性85/可追溯性80/深度85)", data: { gate: "PASS", score: 85 } })

// 需要审查
mcp__ccw-tools__team_msg({ operation: "log", team: teamName, from: "spec-reviewer", to: "coordinator", type: "quality_result", summary: "质量检查 REVIEW: 72分, 可追溯性不足", data: { gate: "REVIEW", score: 72 } })

// 质量失败
mcp__ccw-tools__team_msg({ operation: "log", team: teamName, from: "spec-reviewer", to: "coordinator", type: "fix_required", summary: "质量 FAIL: 55分, 缺少架构 ADR + PRD 验收标准不可测", data: { gate: "FAIL", score: 55, issues: ["missing ADRs", "untestable AC"] } })

Execution Process

Phase 1: Task Discovery
   ├─ TaskList to find unblocked QUALITY-* tasks
   ├─ TaskGet to read full task details
   └─ TaskUpdate to mark in_progress

Phase 2: Document Collection
   ├─ Load all generated documents from session folder
   ├─ Verify document chain completeness
   ├─ Load discussion records for context
   └─ Build document inventory

Phase 3: 4-Dimension Quality Scoring
   ├─ Completeness (25%): All sections present with content
   ├─ Consistency (25%): Terminology, format, references
   ├─ Traceability (25%): Goals → Reqs → Arch → Stories chain
   └─ Depth (25%): AC testable, ADRs justified, stories estimable

Phase 4: Report Generation
   ├─ Generate readiness-report.md (quality scores, issues, traceability)
   ├─ Generate spec-summary.md (one-page executive summary)
   └─ Determine quality gate decision

Phase 5: Report to Coordinator
   ├─ team_msg log + SendMessage quality results
   ├─ TaskUpdate completed (if PASS/REVIEW)
   └─ Flag fix_required (if FAIL)

Implementation

Phase 1: Task Discovery

// Find assigned QUALITY-* tasks
const tasks = TaskList()
const myTasks = tasks.filter(t =>
  t.subject.startsWith('QUALITY-') &&
  t.owner === 'spec-reviewer' &&
  t.status === 'pending' &&
  t.blockedBy.length === 0
)

if (myTasks.length === 0) return // idle

const task = TaskGet({ taskId: myTasks[0].id })
TaskUpdate({ taskId: task.id, status: 'in_progress' })

Phase 2: Document Collection

// Extract session folder
const sessionMatch = task.description.match(/Session:\s*(.+)/)
const sessionFolder = sessionMatch ? sessionMatch[1].trim() : ''

// Load all documents
const documents = {
  config: null,
  discoveryContext: null,
  productBrief: null,
  requirementsIndex: null,
  requirements: [],
  architectureIndex: null,
  adrs: [],
  epicsIndex: null,
  epics: [],
  discussions: []
}

try { documents.config = JSON.parse(Read(`${sessionFolder}/spec-config.json`)) } catch {}
try { documents.discoveryContext = JSON.parse(Read(`${sessionFolder}/discovery-context.json`)) } catch {}
try { documents.productBrief = Read(`${sessionFolder}/product-brief.md`) } catch {}
try { documents.requirementsIndex = Read(`${sessionFolder}/requirements/_index.md`) } catch {}
try { documents.architectureIndex = Read(`${sessionFolder}/architecture/_index.md`) } catch {}
try { documents.epicsIndex = Read(`${sessionFolder}/epics/_index.md`) } catch {}

// Load individual requirements
const reqFiles = Glob({ pattern: `${sessionFolder}/requirements/REQ-*.md` })
reqFiles.forEach(f => { try { documents.requirements.push(Read(f)) } catch {} })
const nfrFiles = Glob({ pattern: `${sessionFolder}/requirements/NFR-*.md` })
nfrFiles.forEach(f => { try { documents.requirements.push(Read(f)) } catch {} })

// Load individual ADRs
const adrFiles = Glob({ pattern: `${sessionFolder}/architecture/ADR-*.md` })
adrFiles.forEach(f => { try { documents.adrs.push(Read(f)) } catch {} })

// Load individual Epics
const epicFiles = Glob({ pattern: `${sessionFolder}/epics/EPIC-*.md` })
epicFiles.forEach(f => { try { documents.epics.push(Read(f)) } catch {} })

// Load discussions
const discussFiles = Glob({ pattern: `${sessionFolder}/discussions/discuss-*.md` })
discussFiles.forEach(f => { try { documents.discussions.push(Read(f)) } catch {} })

// Verify completeness
const docInventory = {
  config: !!documents.config,
  discoveryContext: !!documents.discoveryContext,
  productBrief: !!documents.productBrief,
  requirements: documents.requirements.length > 0,
  architecture: documents.adrs.length > 0,
  epics: documents.epics.length > 0,
  discussions: documents.discussions.length
}

Phase 3: 4-Dimension Quality Scoring

const scores = {
  completeness: 0,
  consistency: 0,
  traceability: 0,
  depth: 0
}

// ===== Completeness (25%) =====
function scoreCompleteness(docs) {
  let score = 0
  const checks = [
    { name: 'spec-config.json', present: !!docs.config, weight: 5 },
    { name: 'discovery-context.json', present: !!docs.discoveryContext, weight: 10 },
    { name: 'product-brief.md', present: !!docs.productBrief, weight: 20 },
    { name: 'requirements/_index.md', present: !!docs.requirementsIndex, weight: 15 },
    { name: 'REQ-* files', present: docs.requirements.length > 0, weight: 10 },
    { name: 'architecture/_index.md', present: !!docs.architectureIndex, weight: 15 },
    { name: 'ADR-* files', present: docs.adrs.length > 0, weight: 10 },
    { name: 'epics/_index.md', present: !!docs.epicsIndex, weight: 10 },
    { name: 'EPIC-* files', present: docs.epics.length > 0, weight: 5 }
  ]

  checks.forEach(check => {
    if (check.present) score += check.weight
  })

  return { score, checks, issues: checks.filter(c => !c.present).map(c => `Missing: ${c.name}`) }
}

// ===== Consistency (25%) =====
function scoreConsistency(docs) {
  let score = 100
  const issues = []

  // Check session_id consistency across documents
  const sessionId = docs.config?.session_id
  if (sessionId) {
    if (docs.productBrief && !docs.productBrief.includes(sessionId)) {
      score -= 15; issues.push('Product Brief missing session_id reference')
    }
  }

  // Check terminology consistency
  // Extract key terms from product brief, verify usage in other docs
  if (docs.productBrief && docs.requirementsIndex) {
    // Basic term consistency check
    const briefTerms = docs.productBrief.match(/##\s+(.+)/g)?.map(h => h.replace('## ', '')) || []
    // Verify heading style consistency
  }

  // Check YAML frontmatter format consistency
  const docsWithFrontmatter = [docs.productBrief, docs.requirementsIndex, docs.architectureIndex, docs.epicsIndex].filter(Boolean)
  const hasFrontmatter = docsWithFrontmatter.map(d => /^---\n[\s\S]+?\n---/.test(d))
  const frontmatterConsistent = hasFrontmatter.every(v => v === hasFrontmatter[0])
  if (!frontmatterConsistent) {
    score -= 20; issues.push('Inconsistent YAML frontmatter across documents')
  }

  return { score: Math.max(0, score), issues }
}

// ===== Traceability (25%) =====
function scoreTraceability(docs) {
  let score = 0
  const issues = []

  // Goals → Requirements tracing
  if (docs.productBrief && docs.requirementsIndex) {
    // Check if requirements reference product brief goals
    const hasGoalRefs = docs.requirements.some(r => /goal|brief|vision/i.test(r))
    if (hasGoalRefs) score += 25
    else issues.push('Requirements lack references to Product Brief goals')
  }

  // Requirements → Architecture tracing
  if (docs.requirementsIndex && docs.architectureIndex) {
    const hasReqRefs = docs.adrs.some(a => /REQ-|requirement/i.test(a))
    if (hasReqRefs) score += 25
    else issues.push('Architecture ADRs lack requirement references')
  }

  // Requirements → Stories tracing
  if (docs.requirementsIndex && docs.epicsIndex) {
    const hasStoryRefs = docs.epics.some(e => /REQ-|requirement/i.test(e))
    if (hasStoryRefs) score += 25
    else issues.push('Epics/Stories lack requirement tracing')
  }

  // Full chain check
  if (score >= 50) score += 25 // bonus for good overall traceability

  return { score: Math.min(100, score), issues }
}

// ===== Depth (25%) =====
function scoreDepth(docs) {
  let score = 100
  const issues = []

  // Check acceptance criteria specificity
  const acPattern = /acceptance|criteria|验收/i
  const hasSpecificAC = docs.requirements.some(r => acPattern.test(r) && r.length > 200)
  if (!hasSpecificAC) {
    score -= 25; issues.push('Acceptance criteria may lack specificity')
  }

  // Check ADR justification depth
  const adrHasAlternatives = docs.adrs.some(a => /alternative|替代|pros|cons/i.test(a))
  if (!adrHasAlternatives && docs.adrs.length > 0) {
    score -= 25; issues.push('ADRs lack alternatives analysis')
  }

  // Check story estimability
  const storySized = docs.epics.some(e => /\b[SMLX]{1,2}\b|Small|Medium|Large/.test(e))
  if (!storySized && docs.epics.length > 0) {
    score -= 25; issues.push('Stories lack size estimates')
  }

  // Check Mermaid diagrams presence
  const hasDiagrams = [docs.architectureIndex, docs.epicsIndex].some(d => d && /```mermaid/.test(d))
  if (!hasDiagrams) {
    score -= 10; issues.push('Missing Mermaid diagrams')
  }

  return { score: Math.max(0, score), issues }
}

// Execute all scoring
const completenessResult = scoreCompleteness(documents)
const consistencyResult = scoreConsistency(documents)
const traceabilityResult = scoreTraceability(documents)
const depthResult = scoreDepth(documents)

scores.completeness = completenessResult.score
scores.consistency = consistencyResult.score
scores.traceability = traceabilityResult.score
scores.depth = depthResult.score

const overallScore = (scores.completeness + scores.consistency + scores.traceability + scores.depth) / 4
const qualityGate = overallScore >= 80 ? 'PASS' : overallScore >= 60 ? 'REVIEW' : 'FAIL'

Phase 4: Report Generation

// Generate readiness-report.md
const readinessReport = `---
session_id: ${documents.config?.session_id || 'unknown'}
phase: 6
document_type: readiness-report
status: complete
generated_at: ${new Date().toISOString()}
version: 1
---

# Readiness Report

## Quality Scores

| Dimension | Score | Weight |
|-----------|-------|--------|
| Completeness | ${scores.completeness}% | 25% |
| Consistency | ${scores.consistency}% | 25% |
| Traceability | ${scores.traceability}% | 25% |
| Depth | ${scores.depth}% | 25% |
| **Overall** | **${overallScore.toFixed(1)}%** | **100%** |

## Quality Gate: ${qualityGate}

${qualityGate === 'PASS' ? 'All quality criteria met. Specification is ready for execution.' :
  qualityGate === 'REVIEW' ? 'Quality is acceptable with some areas needing attention.' :
  'Critical quality issues must be addressed before proceeding.'}

## Issues Found

### Completeness Issues
${completenessResult.issues.map(i => `- ${i}`).join('\n') || 'None'}

### Consistency Issues
${consistencyResult.issues.map(i => `- ${i}`).join('\n') || 'None'}

### Traceability Issues
${traceabilityResult.issues.map(i => `- ${i}`).join('\n') || 'None'}

### Depth Issues
${depthResult.issues.map(i => `- ${i}`).join('\n') || 'None'}

## Document Inventory
${Object.entries(docInventory).map(([k, v]) => `- ${k}: ${v === true ? '✓' : v === false ? '✗' : v}`).join('\n')}

## Discussion Rounds Completed: ${documents.discussions.length}

## Recommendations
${allIssues.map(i => `- ${i}`).join('\n') || 'No outstanding issues.'}
`
Write(`${sessionFolder}/readiness-report.md`, readinessReport)

// Generate spec-summary.md (one-page executive summary)
const specSummary = `---
session_id: ${documents.config?.session_id || 'unknown'}
phase: 6
document_type: spec-summary
status: complete
generated_at: ${new Date().toISOString()}
version: 1
---

# Specification Summary

**Topic**: ${documents.config?.topic || 'N/A'}
**Complexity**: ${documents.config?.complexity || 'N/A'}
**Quality Score**: ${overallScore.toFixed(1)}% (${qualityGate})
**Discussion Rounds**: ${documents.discussions.length}

## Key Deliverables
- Product Brief: ${docInventory.productBrief ? '✓' : '✗'}
- Requirements (PRD): ${docInventory.requirements ? `✓ (${documents.requirements.length} items)` : '✗'}
- Architecture: ${docInventory.architecture ? `✓ (${documents.adrs.length} ADRs)` : '✗'}
- Epics & Stories: ${docInventory.epics ? `✓ (${documents.epics.length} epics)` : '✗'}

## Next Steps
${qualityGate === 'PASS' ? '- Ready for handoff to execution workflows (lite-plan, req-plan, plan, issue:new)' :
  qualityGate === 'REVIEW' ? '- Address review items, then proceed to execution' :
  '- Fix critical issues before proceeding'}
`
Write(`${sessionFolder}/spec-summary.md`, specSummary)

Phase 5: Report to Coordinator

const allIssues = [
  ...completenessResult.issues,
  ...consistencyResult.issues,
  ...traceabilityResult.issues,
  ...depthResult.issues
]

// Log before SendMessage
mcp__ccw-tools__team_msg({
  operation: "log", team: teamName,
  from: "spec-reviewer", to: "coordinator",
  type: qualityGate === 'FAIL' ? "fix_required" : "quality_result",
  summary: `质量检查 ${qualityGate}: ${overallScore.toFixed(1)}分 (完整性${scores.completeness}/一致性${scores.consistency}/追溯${scores.traceability}/深度${scores.depth})`,
  data: { gate: qualityGate, score: overallScore, issues: allIssues }
})

SendMessage({
  type: "message",
  recipient: "coordinator",
  content: `## 质量审查报告

**Task**: ${task.subject}
**总分**: ${overallScore.toFixed(1)}%
**Gate**: ${qualityGate}

### 评分详情
| 维度 | 分数 |
|------|------|
| 完整性 | ${scores.completeness}% |
| 一致性 | ${scores.consistency}% |
| 可追溯性 | ${scores.traceability}% |
| 深度 | ${scores.depth}% |

### 问题列表 (${allIssues.length})
${allIssues.map(i => `- ${i}`).join('\n') || '无问题'}

### 文档清单
${Object.entries(docInventory).map(([k, v]) => `- ${k}: ${typeof v === 'boolean' ? (v ? '✓' : '✗') : v}`).join('\n')}

### 讨论轮次: ${documents.discussions.length}

### 输出位置
- 就绪报告: ${sessionFolder}/readiness-report.md
- 执行摘要: ${sessionFolder}/spec-summary.md

${qualityGate === 'PASS' ? '质量达标,可进入最终讨论轮次 DISCUSS-006。' :
  qualityGate === 'REVIEW' ? '质量基本达标但有改进空间,建议在讨论中审查。' :
  '质量未达标,建议创建 DRAFT-fix 任务修复关键问题。'}`,
  summary: `质量 ${qualityGate}: ${overallScore.toFixed(1)}分`
})

// Mark task
if (qualityGate !== 'FAIL') {
  TaskUpdate({ taskId: task.id, status: 'completed' })
} else {
  // Keep in_progress, coordinator needs to create fix tasks
}

// Check for next QUALITY task
const nextTasks = TaskList().filter(t =>
  t.subject.startsWith('QUALITY-') &&
  t.owner === 'spec-reviewer' &&
  t.status === 'pending' &&
  t.blockedBy.length === 0
)

if (nextTasks.length > 0) {
  // Continue with next task -> back to Phase 1
}

Error Handling

Scenario Resolution
No QUALITY-* tasks available Idle, wait for coordinator assignment
Documents missing from session Score as 0 for completeness, report to coordinator
Cannot parse YAML frontmatter Skip consistency check for that document
Session folder not found Notify coordinator, request session path
Scoring produces NaN Default to 0 for that dimension, log warning
Unexpected error Log error via team_msg, report to coordinator