mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
- Implemented CLI fallback using `ccw team` for various team command operations in `execute.md`, `plan.md`, `review.md`, `spec-analyst.md`, `spec-coordinate.md`, `spec-discuss.md`, `spec-reviewer.md`, `spec-writer.md`, and `test.md`. - Updated command generation templates to include CLI fallback examples. - Enhanced validation checks to ensure CLI fallback sections are present. - Added quality standards for CLI fallback in team command design. - Introduced a new `GlobalGraphExpander` class for expanding search results with cross-directory relationships. - Added tests for `GlobalGraphExpander` to verify functionality and score decay factors.
493 lines
17 KiB
Markdown
493 lines
17 KiB
Markdown
---
|
||
name: spec-reviewer
|
||
description: Team spec reviewer - 跨文档质量验证、完整性/一致性/可追溯性/深度评分、就绪度检查
|
||
argument-hint: ""
|
||
allowed-tools: SendMessage(*), TaskUpdate(*), TaskList(*), TaskGet(*), TodoWrite(*), Read(*), Bash(*), Glob(*), Grep(*), Task(*)
|
||
group: 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` 记录消息:
|
||
|
||
```javascript
|
||
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 | 审查无法完成 | 文档缺失、无法解析等 |
|
||
|
||
### 调用示例
|
||
|
||
```javascript
|
||
// 质量通过
|
||
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"] } })
|
||
```
|
||
|
||
### CLI 回退
|
||
|
||
当 `mcp__ccw-tools__team_msg` MCP 不可用时,使用 `ccw team` CLI 作为等效回退:
|
||
|
||
```javascript
|
||
// 回退: 将 MCP 调用替换为 Bash CLI(参数一一对应)
|
||
Bash(`ccw team log --team "${teamName}" --from "spec-reviewer" --to "coordinator" --type "quality_result" --summary "质量检查 PASS: 85分" --data '{"gate":"PASS","score":85}' --json`)
|
||
```
|
||
|
||
**参数映射**: `team_msg(params)` → `ccw team log --team <team> --from spec-reviewer --to coordinator --type <type> --summary "<text>" [--data '<json>'] [--json]`
|
||
|
||
## 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
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```javascript
|
||
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
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```javascript
|
||
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 |
|