Files
james.bland 3f0786f1e7 refactor: agents and skills move to a client-neutral source that renders per client
`.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.
2026-09-19 17:27:26 -04:00

9.2 KiB

name, description
name description
tdd-workflow 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

// 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

// 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

// 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

// === 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:

# 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.

# 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!)

# pragma: no cover - Reason: Debug utility only used in development
def debug_print(data):
    print(json.dumps(data, indent=2))
/* 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

// ❌ 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:

# 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"