Skills move to skills/<name>/SKILL.md and the repo gains both manifests, so it is now a marketplace hosting exactly one plugin whose root is the repo root (source: "."). Claude Code installs it from git; no symlinks involved. Why: install.sh wired the library into every consuming repo as an ABSOLUTE symlink to $CHECKOUT_DIR. The checkout moved three times, most recently when local-dev@1966d5a dropped the embedded .skills/ copy, and nothing migrated the links — 243 dangling links across 12 repos, hidden from git by the **/.claude/skills/ ignore rule. Worse, ~/dev spans two path roots (/Users/mathias on the iCloud-shared Macs, /home/mathias on koala), so an absolute link stored there is broken on one side by construction. Re-running the installer cannot fix that; only removing the mechanism can. install.sh keeps the harnesses with no plugin system and drops from 42 links per harness to one directory link, via the convergent ~/.agents/skills convention that Pi and OpenCode both read: ~/.agents/skills -> $CHECKOUT_DIR/skills ~/.config/crush/skills -> ~/.agents/skills ~/.config/opencode/skills -> ~/.agents/skills wire_claude_repo is deleted outright rather than gated behind a flag — on a mixed-root fleet it cannot be made correct. Antigravity and Vibe targets go too; both unused. CHECKOUT_DIR keeps its host-local default deliberately: pointing it into ~/dev would put a git working tree inside iCloud with two concurrent writers. Taskfile drops five near-identical wiring loops (they had already drifted) and delegates to install.sh. `task check` gains frontmatter validation, a stray-root-entry gate, and manifest/tag version parity — the loose secret-hygiene.md sat unloadable for months because nothing checked layout. Verified: claude plugin validate . passes clean; a probe skill loaded as skills:zzprobe through --plugin-dir; task check green at 42 skills. Refs: #23 Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
8.4 KiB
name, description
| name | description |
|---|---|
| atdd | Implement user stories using the ATDD workflow — Red (failing acceptance test) → Green (minimal implementation) → Refactor. One story at a time. |
Acceptance Test-Driven Development (ATDD)
Overview
ATDD extends TDD to the acceptance level. You write tests that capture the behavior described in a user story's acceptance criteria — then implement just enough to make them pass.
Input required: User stories with clear acceptance criteria. Load user-stories skill first.
Core Methodology: Red-Green-Refactor at Acceptance Level
RED → Write failing acceptance test (one story at a time)
GREEN → Write minimal code to make it pass
REFACTOR → Clean up code (load clean-code skill)
COMMIT → Commit with reference to the story
Key principle: Never mix phases. RED is only writing tests. GREEN is only making tests pass. REFACTOR is only cleaning up.
Stop and confirm before advancing from RED to GREEN, and from GREEN to REFACTOR.
Working on One Story at a Time
Pick the highest-priority story from the backlog. Do not start a second story until the first is done (committed, clean, all tests green).
Phase 1: RED — Write Failing Acceptance Test
Translate the story's acceptance criteria into executable tests.
For each acceptance criterion:
Given [context]
When [action]
Then [observable outcome]
Becomes a test case.
Go ATDD Example
Story: "Users can register with email and password"
Acceptance criteria:
- Given valid email and password, registration succeeds and returns the new user
- Given an email already in use, registration returns ErrDuplicateEmail
- Given an invalid email format, registration returns ErrInvalidEmail
- Given a password shorter than 8 chars, registration returns ErrWeakPassword
Acceptance tests (write these BEFORE implementation):
func TestUserRegistration_Acceptance(t *testing.T) {
tests := []struct {
name string
email string
pass string
wantErr error
}{
{
name: "valid registration creates user",
email: "alice@example.com",
pass: "securePass1!",
wantErr: nil,
},
{
name: "duplicate email returns ErrDuplicateEmail",
email: "existing@example.com",
pass: "securePass1!",
wantErr: ErrDuplicateEmail,
},
{
name: "invalid email format returns ErrInvalidEmail",
email: "not-an-email",
pass: "securePass1!",
wantErr: ErrInvalidEmail,
},
{
name: "short password returns ErrWeakPassword",
email: "alice@example.com",
pass: "short",
wantErr: ErrWeakPassword,
},
}
store := NewInMemoryUserStore()
// Pre-seed duplicate email
store.Seed(User{Email: "existing@example.com"})
svc := NewUserService(store)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := svc.Register(context.Background(), tt.email, tt.pass)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}
Run and verify they fail:
go test -run TestUserRegistration_Acceptance ./...
# FAIL: UserService.Register undefined
The test must fail because the implementation doesn't exist yet. If it passes, you're testing existing behavior. Check your test.
STOP here. Confirm before moving to GREEN.
Phase 2: GREEN — Minimal Implementation
Implement the absolute minimum code necessary to make the acceptance tests pass. Ugly code is acceptable at this stage. The goal is green, not clean.
// Minimal implementation — prioritize passing tests over elegance
func (s *UserService) Register(ctx context.Context, email, password string) (User, error) {
if !strings.Contains(email, "@") {
return User{}, ErrInvalidEmail
}
if len(password) < 8 {
return User{}, ErrWeakPassword
}
if s.store.Exists(ctx, email) {
return User{}, ErrDuplicateEmail
}
u := User{ID: newID(), Email: email}
if err := s.store.Save(ctx, u); err != nil {
return User{}, fmt.Errorf("save user: %w", err)
}
return u, nil
}
Verify GREEN:
go test -run TestUserRegistration_Acceptance ./...
# ok
go test ./... # All other tests still pass
go test -race ./...
STOP here. Confirm before moving to REFACTOR.
Phase 3: REFACTOR — Clean Up
With tests green, clean the code. Do not change behavior.
During REFACTOR, load and follow the clean-code skill.
Apply:
- Extract functions for complex logic
- Better naming
- Remove duplication
- Apply SOLID principles
- For design decisions, load
solidskill
After every change: go test ./... must stay green.
Do NOT refactor the acceptance tests themselves unless they have a bug. The tests are your specification — changing them changes what you've committed to.
STOP here. Confirm before committing.
Phase 4: COMMIT
Commit the completed story. Reference the story in the commit message.
git commit -m "feat: user registration with email validation
Implements acceptance criteria for US-001 (User Registration).
- Valid email/password creates a user
- Duplicate email returns ErrDuplicateEmail
- Invalid email format returns ErrInvalidEmail
- Short password returns ErrWeakPassword"
For Test Design Questions
When writing acceptance tests, load the test-design skill to ensure:
- Tests are understandable (Farley: Understandable)
- Tests don't couple to implementation details (Farley: Maintainable)
- Tests are repeatable across environments (Farley: Repeatable)
For Code Review Before Commit
Load the code-review skill before committing to check:
- Error handling follows
fmt.Errorf("op: %w", err)pattern - Context propagation is correct
- No race conditions in concurrent code
- Exported API is minimal
For Refactoring Guidance
Load the refactoring skill for systematic techniques to apply during REFACTOR phase.
For Clean Code Principles
Load the clean-code skill during REFACTOR phase to apply:
- Naming conventions
- Function size and responsibility
- SOLID principles
Go-Specific ATDD Notes
Acceptance Test Location
Place acceptance tests in _test.go files with the _acceptance_test.go suffix or //go:build acceptance build tag to separate from fast unit tests:
//go:build acceptance
package service_test
go test ./... # Unit tests only (fast)
go test -tags=acceptance ./... # Include acceptance tests
In-Memory Implementations for Acceptance Tests
Use in-memory implementations of repositories for acceptance tests:
type InMemoryUserStore struct {
mu sync.Mutex
users map[string]User
}
func (s *InMemoryUserStore) Save(ctx context.Context, u User) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.users[u.Email]; exists {
return ErrDuplicateEmail
}
s.users[u.Email] = u
return nil
}
func (s *InMemoryUserStore) Exists(ctx context.Context, email string) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, exists := s.users[email]
return exists
}
This keeps acceptance tests fast and deterministic.
Given-When-Then in Go
The t.Run structure maps naturally to Given-When-Then:
t.Run("given valid credentials, when logging in, then returns token", func(t *testing.T) {
// Given: valid user exists
store := InMemoryUserStore{...}
// When: login is called
token, err := svc.Login(ctx, email, pass)
// Then: token is returned
require.NoError(t, err)
assert.NotEmpty(t, token)
})
Verification Checklist
Before committing:
- All acceptance tests pass
- All existing tests still pass
- Race detector clean:
go test -race ./... - REFACTOR phase complete — code is clean
- Commit message references the user story
- No behavior added beyond the acceptance criteria
Cross-References
- Requires:
user-storiesskill output (stories with acceptance criteria) - During REFACTOR: load
clean-codeskill - For test quality: load
test-designskill - For code review: load
code-reviewskill - For refactoring guidance: load
refactoringskill - Foundation: load
tddskill for core TDD methodology