`.claude/agents/` was the source of truth, which made every role Claude-Code shaped. Adding a second client meant rewriting each role in that client's syntax and maintaining both copies — the drift this scaffold exists to prevent, one layer up. Roles and skills now live under `.agents/` and render into each registered client. `.claude/agents/`, `.claude/skills/` and `.codex/agents/` are generated; `scripts/sync-agent-integrations.py --check` fails on drift and belongs in CI. The role metadata is portable rather than vendor-named: `reasoning_tier` (deep/balanced/fast/vision), `capabilities`, `mutation`, `invocation`, and an optional `preload_skills`. A client manifest maps those to native syntax and must declare what it cannot express — `codex.yaml` declares `tier_policy: unsupported` and its adapters say so in the file, rather than the tier silently evaporating and leaving the repository to believe it was enforced. The port is behaviour-preserving where it should be and a fix where it should not. Every instruction body is byte-identical — the whole diff to `.claude/agents/` is 18 added lines and zero deletions. What changed is frontmatter that was missing: - four agents (`code-reviewer`, `tdd-guardian`, `dependency-audit`, `pr-creator`) declared no `tools:` and therefore inherited the ENTIRE tool pool, so three review-only agents could edit and write the code they were reviewing. All eight now declare capabilities explicitly. - the six read-only roles gain a non-editing permission mode, so the constraint is enforced by the client rather than by the prompt asking nicely. - `mutation` is now explicit, which records the two roles that genuinely need to write: `pr-creator` (external-write — it pushes a branch and opens a PR) and `dependency-audit` (workspace-write — package managers rewrite lockfiles). `pr-creator` keeps `shell` because opening a PR needs it, but it is now the only agent here with a write mutation and a declared reason for it, instead of one of four with unlimited access by omission.
350 lines
9.2 KiB
Markdown
350 lines
9.2 KiB
Markdown
---
|
|
name: tdd-workflow
|
|
description: Test-Driven Development workflow with RED-GREEN-REFACTOR cycle, coverage verification, and quality gates. Use when planning or implementing features with TDD.
|
|
---
|
|
|
|
# TDD Workflow Skill
|
|
|
|
## The TDD Cycle
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ │
|
|
│ ┌─────┐ ┌───────┐ ┌──────────┐ │
|
|
│ │ RED │ ──▶ │ GREEN │ ──▶ │ REFACTOR │ ──┐ │
|
|
│ └─────┘ └───────┘ └──────────┘ │ │
|
|
│ ▲ │ │
|
|
│ └──────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Phase 1: RED (Write Failing Test)
|
|
|
|
### Rules
|
|
- Write ONE test that describes desired behavior
|
|
- Test must fail for the RIGHT reason
|
|
- NO production code exists yet
|
|
|
|
### Checklist
|
|
- [ ] Test describes behavior, not implementation
|
|
- [ ] Test name clearly states expected outcome
|
|
- [ ] Test uses factory functions for data
|
|
- [ ] Test fails with meaningful error message
|
|
- [ ] Error indicates missing behavior, not syntax/import errors
|
|
|
|
### Example
|
|
```typescript
|
|
// STEP 1: Write the failing test
|
|
describe('PaymentProcessor', () => {
|
|
it('should reject payments with negative amounts', () => {
|
|
const payment = getMockPayment({ amount: -100 });
|
|
|
|
const result = processPayment(payment);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Amount must be positive');
|
|
});
|
|
});
|
|
|
|
// At this point:
|
|
// - processPayment doesn't exist
|
|
// - Running test gives: "ReferenceError: processPayment is not defined"
|
|
// - This is the RIGHT failure - the function doesn't exist yet
|
|
```
|
|
|
|
## Phase 2: GREEN (Write Minimum Code)
|
|
|
|
### Rules
|
|
- Write ONLY enough code to pass the test
|
|
- No additional functionality
|
|
- No "while you're there" improvements
|
|
- Ugly code is acceptable (we'll refactor)
|
|
|
|
### Checklist
|
|
- [ ] Implementation makes test pass
|
|
- [ ] No code beyond what test demands
|
|
- [ ] All existing tests still pass
|
|
- [ ] Coverage increased for new code
|
|
|
|
### Example
|
|
```typescript
|
|
// STEP 2: Write minimum code to pass
|
|
export function processPayment(payment: Payment): PaymentResult {
|
|
if (payment.amount < 0) {
|
|
return { success: false, error: 'Amount must be positive' };
|
|
}
|
|
return { success: true };
|
|
}
|
|
|
|
// This passes the test - that's enough for now
|
|
// Don't add validation for zero, max amounts, etc.
|
|
// Those need their own tests first!
|
|
```
|
|
|
|
## Phase 3: REFACTOR (Improve Code Quality)
|
|
|
|
### Rules
|
|
- COMMIT before refactoring
|
|
- Only refactor if it adds value
|
|
- All tests must stay green
|
|
- No new functionality
|
|
|
|
### When to Refactor
|
|
- Duplicate knowledge (not just similar code)
|
|
- Unclear names
|
|
- Complex conditionals
|
|
- Magic numbers/strings
|
|
|
|
### When NOT to Refactor
|
|
- Code is already clean
|
|
- Similarity is structural, not semantic
|
|
- Would add unnecessary abstraction
|
|
|
|
### Example
|
|
```typescript
|
|
// STEP 3: Assess and refactor
|
|
// BEFORE REFACTORING: git commit -m "feat: add negative amount validation"
|
|
|
|
// Assessment:
|
|
// - Magic number 0 could be a constant
|
|
// - Error message could be a constant
|
|
// - But... it's simple enough. Skip refactoring.
|
|
|
|
// If we had multiple validations:
|
|
const VALIDATION_ERRORS = {
|
|
NEGATIVE_AMOUNT: 'Amount must be positive',
|
|
ZERO_AMOUNT: 'Amount must be greater than zero',
|
|
EXCEEDS_LIMIT: 'Amount exceeds maximum limit',
|
|
} as const;
|
|
|
|
const MAX_PAYMENT_AMOUNT = 10000;
|
|
|
|
export function processPayment(payment: Payment): PaymentResult {
|
|
if (payment.amount < 0) {
|
|
return { success: false, error: VALIDATION_ERRORS.NEGATIVE_AMOUNT };
|
|
}
|
|
if (payment.amount === 0) {
|
|
return { success: false, error: VALIDATION_ERRORS.ZERO_AMOUNT };
|
|
}
|
|
if (payment.amount > MAX_PAYMENT_AMOUNT) {
|
|
return { success: false, error: VALIDATION_ERRORS.EXCEEDS_LIMIT };
|
|
}
|
|
return { success: true };
|
|
}
|
|
```
|
|
|
|
## Complete TDD Session Example
|
|
|
|
```typescript
|
|
// === CYCLE 1: Negative amounts ===
|
|
|
|
// RED: Write failing test
|
|
it('should reject negative amounts', () => {
|
|
const payment = getMockPayment({ amount: -100 });
|
|
const result = processPayment(payment);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
// RUN: ❌ FAIL - processPayment is not defined
|
|
|
|
// GREEN: Minimal implementation
|
|
function processPayment(payment: Payment): PaymentResult {
|
|
if (payment.amount < 0) return { success: false };
|
|
return { success: true };
|
|
}
|
|
// RUN: ✅ PASS
|
|
|
|
// REFACTOR: Assess - simple enough, skip
|
|
// COMMIT: "feat: reject negative payment amounts"
|
|
|
|
// === CYCLE 2: Zero amounts ===
|
|
|
|
// RED: Write failing test
|
|
it('should reject zero amounts', () => {
|
|
const payment = getMockPayment({ amount: 0 });
|
|
const result = processPayment(payment);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
// RUN: ❌ FAIL - expected false, got true
|
|
|
|
// GREEN: Add zero check
|
|
function processPayment(payment: Payment): PaymentResult {
|
|
if (payment.amount <= 0) return { success: false };
|
|
return { success: true };
|
|
}
|
|
// RUN: ✅ PASS
|
|
|
|
// REFACTOR: Assess - still simple, skip
|
|
// COMMIT: "feat: reject zero payment amounts"
|
|
|
|
// === CYCLE 3: Maximum amount ===
|
|
|
|
// RED: Write failing test
|
|
it('should reject amounts over 10000', () => {
|
|
const payment = getMockPayment({ amount: 10001 });
|
|
const result = processPayment(payment);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
// RUN: ❌ FAIL - expected false, got true
|
|
|
|
// GREEN: Add max check
|
|
function processPayment(payment: Payment): PaymentResult {
|
|
if (payment.amount <= 0) return { success: false };
|
|
if (payment.amount > 10000) return { success: false };
|
|
return { success: true };
|
|
}
|
|
// RUN: ✅ PASS
|
|
|
|
// REFACTOR: Now we have magic number 10000
|
|
// COMMIT first: "feat: reject payments over maximum"
|
|
// Then refactor:
|
|
const MAX_PAYMENT_AMOUNT = 10000;
|
|
|
|
function processPayment(payment: Payment): PaymentResult {
|
|
if (payment.amount <= 0) return { success: false };
|
|
if (payment.amount > MAX_PAYMENT_AMOUNT) return { success: false };
|
|
return { success: true };
|
|
}
|
|
// RUN: ✅ PASS
|
|
// COMMIT: "refactor: extract max payment constant"
|
|
```
|
|
|
|
## Quality Gates
|
|
|
|
Before committing, verify:
|
|
|
|
```bash
|
|
# 1. All tests pass
|
|
npm test # or pytest, cargo test
|
|
|
|
# 2. Coverage meets threshold
|
|
npm test -- --coverage
|
|
# Verify: 80%+ overall
|
|
|
|
# 3. Type checking passes
|
|
npm run typecheck # or mypy, cargo check
|
|
|
|
# 4. Linting passes
|
|
npm run lint # or ruff, cargo clippy
|
|
|
|
# 5. No secrets in code
|
|
git diff --staged | grep -i "password\|secret\|api.key"
|
|
```
|
|
|
|
## Coverage Verification
|
|
|
|
**CRITICAL: Never trust claimed coverage - always verify.**
|
|
|
|
```bash
|
|
# Python
|
|
pytest --cov=src --cov-report=term-missing --cov-fail-under=80
|
|
|
|
# TypeScript
|
|
npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80}}'
|
|
|
|
# Rust
|
|
cargo tarpaulin --fail-under 80
|
|
```
|
|
|
|
### Interpreting Coverage
|
|
|
|
| Metric | Meaning | Target |
|
|
|--------|---------|--------|
|
|
| Lines | % of lines executed | 80%+ |
|
|
| Branches | % of if/else paths taken | 80%+ |
|
|
| Functions | % of functions called | 80%+ |
|
|
| Statements | % of statements executed | 80%+ |
|
|
|
|
### Coverage Exceptions (Document!)
|
|
|
|
```python
|
|
# pragma: no cover - Reason: Debug utility only used in development
|
|
def debug_print(data):
|
|
print(json.dumps(data, indent=2))
|
|
```
|
|
|
|
```typescript
|
|
/* istanbul ignore next -- @preserve Reason: Error boundary for React */
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error(error);
|
|
}
|
|
```
|
|
|
|
## Anti-Patterns
|
|
|
|
### Writing Test After Code
|
|
```
|
|
❌ WRONG:
|
|
1. Write all production code
|
|
2. Write tests to cover it
|
|
3. "I have 80% coverage!"
|
|
|
|
✅ CORRECT:
|
|
1. Write failing test
|
|
2. Write code to pass
|
|
3. Repeat until feature complete
|
|
```
|
|
|
|
### Testing Implementation
|
|
```typescript
|
|
// ❌ BAD: Tests implementation detail
|
|
it('should call validatePayment', () => {
|
|
const spy = jest.spyOn(service, 'validatePayment');
|
|
processPayment(payment);
|
|
expect(spy).toHaveBeenCalled();
|
|
});
|
|
|
|
// ✅ GOOD: Tests behavior
|
|
it('should reject invalid payments', () => {
|
|
const payment = getMockPayment({ amount: -1 });
|
|
const result = processPayment(payment);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
```
|
|
|
|
### Writing Too Many Tests at Once
|
|
```
|
|
❌ WRONG:
|
|
1. Write 10 tests
|
|
2. Implement everything
|
|
3. All tests pass
|
|
|
|
✅ CORRECT:
|
|
1. Write 1 test
|
|
2. Make it pass
|
|
3. Refactor if needed
|
|
4. Repeat
|
|
```
|
|
|
|
### Skipping Refactor Assessment
|
|
```
|
|
❌ WRONG:
|
|
1. Test passes
|
|
2. Immediately write next test
|
|
3. Code becomes messy
|
|
|
|
✅ CORRECT:
|
|
1. Test passes
|
|
2. Ask: "Should I refactor?"
|
|
3. If yes: commit, then refactor
|
|
4. If no: continue
|
|
```
|
|
|
|
## Commit Messages
|
|
|
|
Follow this pattern:
|
|
|
|
```bash
|
|
# After GREEN (feature added)
|
|
git commit -m "feat: add payment amount validation"
|
|
|
|
# After REFACTOR
|
|
git commit -m "refactor: extract payment validation constants"
|
|
|
|
# Test-only changes
|
|
git commit -m "test: add edge cases for payment validation"
|
|
|
|
# Bug fixes (driven by failing test)
|
|
git commit -m "fix: handle null payment amount"
|
|
```
|