refactor(plugin)!: ship as a Claude Code plugin, one dir link per harness
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
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
---
|
||||
name: atdd
|
||||
description: 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):**
|
||||
```go
|
||||
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:**
|
||||
```bash
|
||||
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.
|
||||
|
||||
```go
|
||||
// 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:**
|
||||
```bash
|
||||
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 `solid` skill
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
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
|
||||
//go:build acceptance
|
||||
|
||||
package service_test
|
||||
```
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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-stories` skill output (stories with acceptance criteria)
|
||||
- During REFACTOR: load `clean-code` skill
|
||||
- For test quality: load `test-design` skill
|
||||
- For code review: load `code-review` skill
|
||||
- For refactoring guidance: load `refactoring` skill
|
||||
- Foundation: load `tdd` skill for core TDD methodology
|
||||
@@ -0,0 +1,282 @@
|
||||
---
|
||||
name: clean-code
|
||||
description: Write code that humans can read, understand, and safely change. Apply SOLID, GRASP, and Clean Code principles during the REFACTOR phase of TDD.
|
||||
---
|
||||
|
||||
# Clean Code
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
Write code for humans first, computers second. Every piece of code you write will be read many more times than it is written. Optimize for clarity and simplicity above all else.
|
||||
|
||||
Code has three consumers:
|
||||
1. **Users** — get their needs met
|
||||
2. **Customers** — make or save money
|
||||
3. **Developers** — must maintain it
|
||||
|
||||
Developers read code 10x more than they write it. Design for them.
|
||||
|
||||
## When to Apply This Skill
|
||||
|
||||
- During the **REFACTOR phase** of TDD (after the test is green)
|
||||
- When reviewing code (load `code-review` skill too)
|
||||
- When designing new types or packages
|
||||
|
||||
**Do not apply during the GREEN phase.** Ugly code that passes is correct. Clean it after.
|
||||
|
||||
## Naming
|
||||
|
||||
Naming is the most important thing in code. In priority order:
|
||||
|
||||
1. **Consistency** — Same concept = same name everywhere. One name per concept.
|
||||
2. **Understandability** — Domain language, not technical jargon.
|
||||
3. **Specificity** — Precise, not vague. Avoid `data`, `info`, `manager`, `handler`, `processor`, `utils`.
|
||||
4. **Brevity** — Short but not cryptic.
|
||||
5. **Searchability** — Unique, greppable names.
|
||||
6. **Pronounceability** — You should be able to say it in conversation.
|
||||
|
||||
```go
|
||||
// Bad: inconsistent, vague, cryptic
|
||||
func getUsrById(id string) {}
|
||||
func fetchCustomerByID(id string) {}
|
||||
func retrieveClientById(id string) {}
|
||||
|
||||
// Good: consistent, specific
|
||||
func GetUser(ctx context.Context, id UserID) (User, error) {}
|
||||
func GetOrder(ctx context.Context, id OrderID) (Order, error) {}
|
||||
```
|
||||
|
||||
## Functions/Methods
|
||||
|
||||
- Do one thing, do it well, do it only
|
||||
- Operate at a single level of abstraction
|
||||
- Prefer fewer parameters — zero is ideal, one or two common, three should be rare
|
||||
- Avoid side effects — if a function has a side effect, name it accordingly
|
||||
- Prefer pure functions where possible
|
||||
|
||||
```go
|
||||
// Bad: does too much, mixed abstraction levels
|
||||
func processOrder(o *Order, db *sql.DB, mailer Mailer) error {
|
||||
if o.Items == nil || len(o.Items) == 0 {
|
||||
return errors.New("empty order")
|
||||
}
|
||||
total := 0.0
|
||||
for _, item := range o.Items {
|
||||
total += item.Price * float64(item.Qty)
|
||||
}
|
||||
if _, err := db.Exec("INSERT INTO orders ...", o.ID, total); err != nil {
|
||||
return fmt.Errorf("save order: %w", err)
|
||||
}
|
||||
if err := mailer.Send(o.Customer.Email, "confirmed"); err != nil {
|
||||
return fmt.Errorf("send confirmation: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Good: single level of abstraction, composed from focused functions
|
||||
func ProcessOrder(ctx context.Context, o *Order, repo OrderRepository, mailer Mailer) error {
|
||||
if err := validateOrder(o); err != nil {
|
||||
return fmt.Errorf("validate: %w", err)
|
||||
}
|
||||
total := calculateTotal(o.Items)
|
||||
if err := repo.Save(ctx, o, total); err != nil {
|
||||
return fmt.Errorf("save: %w", err)
|
||||
}
|
||||
if err := mailer.SendConfirmation(ctx, o.Customer.Email); err != nil {
|
||||
return fmt.Errorf("notify: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling (Go-Specific)
|
||||
|
||||
In Go, error returns are a first-class feature, not a smell. Follow these rules:
|
||||
|
||||
```go
|
||||
// Always wrap with context
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
|
||||
// Never: naked return without context
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Never: log and return (pick one)
|
||||
if err != nil {
|
||||
log.Error("failed", "err", err)
|
||||
return err // caller gets logged error AND returns it — double-counted
|
||||
}
|
||||
|
||||
// Never: swallow errors silently
|
||||
_ = doSomething()
|
||||
```
|
||||
|
||||
Error wrapping builds a call-chain narrative: `"http handler: parse request: validate email: invalid format"`. This is valuable, not verbose.
|
||||
|
||||
## Comments
|
||||
|
||||
**The best comment is code that doesn't need one.** Before writing a comment, ask whether you could make the code itself clearer.
|
||||
|
||||
When comments are necessary, explain **why**, not what. The code shows what happens; comments explain decisions and intent.
|
||||
|
||||
```go
|
||||
// Bad: explains what (redundant)
|
||||
// increment counter
|
||||
i++
|
||||
|
||||
// Good: explains why
|
||||
// Offset by 1 because the upstream API uses 1-based page numbering
|
||||
page := requestedPage + 1
|
||||
```
|
||||
|
||||
**Go adaptation:** `godoc` comments on exported identifiers ARE documentation, not a failure. The "comments are failure" rule applies to inline comments explaining bad code. Exported types and functions must have godoc comments.
|
||||
|
||||
```go
|
||||
// UserService handles user lifecycle operations.
|
||||
// It is safe for concurrent use.
|
||||
type UserService struct { ... }
|
||||
|
||||
// GetByEmail returns the user with the given email address.
|
||||
// Returns ErrNotFound if no user exists with that email.
|
||||
func (s *UserService) GetByEmail(ctx context.Context, email Email) (User, error) { ... }
|
||||
```
|
||||
|
||||
## SOLID Principles (Brief)
|
||||
|
||||
For full SOLID guidance with Go examples, load the `solid` skill.
|
||||
|
||||
| Principle | Quick test |
|
||||
|-----------|------------|
|
||||
| SRP | "Does this type have ONE reason to change?" |
|
||||
| OCP | "Can I add behavior by implementing an interface, not editing this code?" |
|
||||
| LSP | "Can callers substitute any implementation without knowing the difference?" |
|
||||
| ISP | "Is this interface the smallest it can be?" (`io.Reader` is a model) |
|
||||
| DIP | "Do I pass interfaces as parameters, not concrete types?" |
|
||||
|
||||
## GRASP Principles
|
||||
|
||||
**Information Expert:** Assign responsibility to the type that has the data needed to fulfil it.
|
||||
|
||||
**Low Coupling:** Minimize dependencies between packages. When one changes, few others should too.
|
||||
|
||||
**High Cohesion:** Keep related functionality together. Everything in a package should relate to its central purpose.
|
||||
|
||||
**Controller:** One type handles system events for a use case.
|
||||
|
||||
**Tell, Don't Ask:** Tell objects what to do rather than asking for their data and acting on it.
|
||||
|
||||
## Go-Specific Clean Code Rules
|
||||
|
||||
### Prefer interfaces over concrete types in function signatures
|
||||
|
||||
```go
|
||||
// Bad: tied to a concrete implementation
|
||||
func NewHandler(db *postgres.DB) *Handler { ... }
|
||||
|
||||
// Good: depends on the behavior needed
|
||||
func NewHandler(store UserStore) *Handler { ... }
|
||||
```
|
||||
|
||||
### Small, focused interfaces
|
||||
|
||||
```go
|
||||
// The io.Reader/io.Writer pattern is the Go ideal
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// If you need both, compose at the call site
|
||||
type ReadWriter interface {
|
||||
Reader
|
||||
Writer
|
||||
}
|
||||
```
|
||||
|
||||
### Embedding over inheritance
|
||||
|
||||
```go
|
||||
// Use embedding for composition, not to inherit behavior you'll override
|
||||
type LoggedHandler struct {
|
||||
Handler
|
||||
logger *slog.Logger
|
||||
}
|
||||
```
|
||||
|
||||
### Constructor injection for dependencies
|
||||
|
||||
```go
|
||||
type Service struct {
|
||||
repo Repository
|
||||
mailer Mailer
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewService(repo Repository, mailer Mailer, logger *slog.Logger) *Service {
|
||||
return &Service{repo: repo, mailer: mailer, logger: logger}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid package-level state
|
||||
|
||||
Package-level variables are global state. They make tests unreliable and code hard to reason about.
|
||||
|
||||
```go
|
||||
// Bad
|
||||
var db *sql.DB
|
||||
|
||||
// Good: pass as dependency
|
||||
type Handler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
```
|
||||
|
||||
## Code Smell Detection (Quick Reference)
|
||||
|
||||
During the REFACTOR phase, look for these smells and address them:
|
||||
|
||||
| Smell | Go Signal | Fix |
|
||||
|-------|-----------|-----|
|
||||
| Long function | > ~30 lines (Go can go longer, but be honest) | Extract function |
|
||||
| Too many parameters | > 3-4 parameters | Use a config struct or option pattern |
|
||||
| Divergent change | One type changes for multiple unrelated reasons | Split into focused types |
|
||||
| Feature envy | Method uses another type's data more than its own | Move method to that type |
|
||||
| Magic numbers | Bare literals | Named constants or typed config |
|
||||
| Deep nesting | > 3 levels of indentation | Early returns, extract helpers |
|
||||
|
||||
For comprehensive smell detection, load the `code-review` skill.
|
||||
|
||||
## Structural Principles
|
||||
|
||||
**Separation of concerns:** Business logic should know nothing of HTTP, SQL, or filesystems.
|
||||
|
||||
**Modularity:** Each package understandable in isolation.
|
||||
|
||||
**Command-Query Separation:** Functions either do something (command) or return something (query), not both.
|
||||
|
||||
**Principle of Least Surprise:** Code should behave the way readers expect.
|
||||
|
||||
**Boy Scout Rule:** Leave the code better than you found it.
|
||||
|
||||
## Pre-Refactor Checklist
|
||||
|
||||
Before cleaning up code:
|
||||
|
||||
- [ ] All tests pass
|
||||
- [ ] Race detector clean: `go test -race ./...`
|
||||
- [ ] I understand what the code does
|
||||
- [ ] I have a clear target design in mind
|
||||
|
||||
During refactoring:
|
||||
- [ ] Tests stay green after each change
|
||||
- [ ] Each change is small and focused
|
||||
- [ ] Commit after each meaningful improvement
|
||||
|
||||
## References
|
||||
|
||||
- `references/clean-code.md` — detailed naming rules, object calisthenics
|
||||
- `references/code-smells.md` — comprehensive smell catalog with examples
|
||||
- Load `solid` skill for SOLID principles with Go examples
|
||||
- Load `refactoring` skill for systematic refactoring techniques
|
||||
@@ -0,0 +1,376 @@
|
||||
# Clean Code Practices
|
||||
|
||||
## What is Clean Code?
|
||||
|
||||
Code that is:
|
||||
- **Easy to understand** - reveals intent clearly
|
||||
- **Easy to change** - modifications are localized
|
||||
- **Easy to test** - dependencies are injectable
|
||||
- **Simple** - no unnecessary complexity
|
||||
|
||||
## The Human-Centered Approach
|
||||
|
||||
Code has THREE consumers:
|
||||
1. **Users** - get their needs met
|
||||
2. **Customers** - make or save money
|
||||
3. **Developers** - must maintain it
|
||||
|
||||
Design for all three, but remember: **developers read code 10x more than they write it.**
|
||||
|
||||
## Naming Principles
|
||||
|
||||
### 1. Consistency & Uniqueness (HIGHEST PRIORITY)
|
||||
Same concept = same name everywhere. One name per concept.
|
||||
|
||||
```typescript
|
||||
// BAD: Inconsistent names for same concept
|
||||
getUserById(id)
|
||||
fetchCustomerById(id)
|
||||
retrieveClientById(id)
|
||||
|
||||
// GOOD: Consistent
|
||||
getUser(id)
|
||||
getOrder(id)
|
||||
getProduct(id)
|
||||
```
|
||||
|
||||
### 2. Understandability
|
||||
Use domain language, not technical jargon.
|
||||
|
||||
```typescript
|
||||
// BAD: Technical
|
||||
const arr = users.filter(u => u.isActive);
|
||||
|
||||
// GOOD: Domain language
|
||||
const activeCustomers = users.filter(user => user.isActive);
|
||||
```
|
||||
|
||||
### 3. Specificity
|
||||
Avoid vague names: `data`, `info`, `manager`, `handler`, `processor`, `utils`
|
||||
|
||||
```typescript
|
||||
// BAD: Vague
|
||||
class DataManager { }
|
||||
function processInfo(data) { }
|
||||
|
||||
// GOOD: Specific
|
||||
class OrderRepository { }
|
||||
function validatePayment(payment) { }
|
||||
```
|
||||
|
||||
### 4. Brevity (but not at cost of clarity)
|
||||
Short names are good only if meaning is preserved.
|
||||
|
||||
```typescript
|
||||
// BAD: Too cryptic
|
||||
const usrLst = getUsrs();
|
||||
|
||||
// BAD: Unnecessarily long
|
||||
const listOfAllActiveUsersInTheSystem = getActiveUsers();
|
||||
|
||||
// GOOD: Brief but clear
|
||||
const activeUsers = getActiveUsers();
|
||||
```
|
||||
|
||||
### 5. Searchability
|
||||
Names should be unique enough to grep/search.
|
||||
|
||||
```typescript
|
||||
// BAD: Common word, hard to search
|
||||
const data = fetch();
|
||||
|
||||
// GOOD: Unique, searchable
|
||||
const orderSummary = fetchOrderSummary();
|
||||
```
|
||||
|
||||
### 6. Pronounceability
|
||||
You should be able to say it in conversation.
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
const genymdhms = generateYearMonthDayHourMinuteSecond();
|
||||
|
||||
// GOOD
|
||||
const timestamp = generateTimestamp();
|
||||
```
|
||||
|
||||
### 7. Austerity
|
||||
Avoid unnecessary filler words.
|
||||
|
||||
```typescript
|
||||
// BAD: Redundant
|
||||
const userData = user; // 'Data' adds nothing
|
||||
class UserClass { } // 'Class' adds nothing
|
||||
|
||||
// GOOD
|
||||
const user = user;
|
||||
class User { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Object Calisthenics (9 Rules)
|
||||
|
||||
Exercises to improve OO design. Follow strictly during practice, relax slightly in production.
|
||||
|
||||
### 1. One Level of Indentation per Method
|
||||
|
||||
```typescript
|
||||
// BAD: Multiple levels
|
||||
function process(orders: Order[]) {
|
||||
for (const order of orders) {
|
||||
if (order.isValid()) {
|
||||
for (const item of order.items) {
|
||||
if (item.inStock) {
|
||||
// process...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Extract methods
|
||||
function process(orders: Order[]) {
|
||||
orders.filter(o => o.isValid()).forEach(processOrder);
|
||||
}
|
||||
|
||||
function processOrder(order: Order) {
|
||||
order.items.filter(i => i.inStock).forEach(processItem);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Don't Use the ELSE Keyword
|
||||
|
||||
Use early returns, guard clauses, or polymorphism.
|
||||
|
||||
```typescript
|
||||
// BAD: else
|
||||
function getDiscount(user: User): number {
|
||||
if (user.isPremium) {
|
||||
return 20;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Early return
|
||||
function getDiscount(user: User): number {
|
||||
if (user.isPremium) return 20;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Wrap All Primitives and Strings
|
||||
|
||||
Primitives should be wrapped in domain objects when they have meaning.
|
||||
|
||||
```typescript
|
||||
// BAD: Primitive obsession
|
||||
function createUser(email: string, age: number) { }
|
||||
|
||||
// GOOD: Value objects
|
||||
class Email {
|
||||
constructor(private value: string) {
|
||||
if (!this.isValid(value)) throw new InvalidEmail();
|
||||
}
|
||||
private isValid(email: string): boolean { ... }
|
||||
}
|
||||
|
||||
class Age {
|
||||
constructor(private value: number) {
|
||||
if (value < 0 || value > 150) throw new InvalidAge();
|
||||
}
|
||||
}
|
||||
|
||||
function createUser(email: Email, age: Age) { }
|
||||
```
|
||||
|
||||
### 4. First-Class Collections
|
||||
|
||||
Any class with a collection should have no other instance variables.
|
||||
|
||||
```typescript
|
||||
// BAD: Collection mixed with other state
|
||||
class Order {
|
||||
items: OrderItem[] = [];
|
||||
customerId: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// GOOD: Collection is its own class
|
||||
class OrderItems {
|
||||
constructor(private items: OrderItem[] = []) {}
|
||||
|
||||
add(item: OrderItem): void { ... }
|
||||
total(): Money { ... }
|
||||
isEmpty(): boolean { ... }
|
||||
}
|
||||
|
||||
class Order {
|
||||
constructor(
|
||||
private items: OrderItems,
|
||||
private customerId: CustomerId
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. One Dot per Line (Law of Demeter)
|
||||
|
||||
Don't chain through object graphs.
|
||||
|
||||
```typescript
|
||||
// BAD: Train wreck
|
||||
const city = order.customer.address.city;
|
||||
|
||||
// GOOD: Tell, don't ask
|
||||
const city = order.getShippingCity();
|
||||
```
|
||||
|
||||
### 6. Don't Abbreviate
|
||||
|
||||
If a name is too long to type, the class is doing too much.
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
const custRepo = new CustRepo();
|
||||
const ord = new Ord();
|
||||
|
||||
// GOOD
|
||||
const customerRepository = new CustomerRepository();
|
||||
const order = new Order();
|
||||
```
|
||||
|
||||
### 7. Keep All Entities Small
|
||||
|
||||
- Classes: < 50 lines
|
||||
- Methods: < 10 lines
|
||||
- Files: < 100 lines
|
||||
|
||||
If larger, it's probably doing too much. Split it.
|
||||
|
||||
### 8. No Classes with More Than Two Instance Variables
|
||||
|
||||
Forces small, focused classes.
|
||||
|
||||
```typescript
|
||||
// BAD: Too many variables
|
||||
class Order {
|
||||
id: string;
|
||||
customerId: string;
|
||||
items: Item[];
|
||||
total: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// GOOD: Composed of smaller objects
|
||||
class Order {
|
||||
constructor(
|
||||
private id: OrderId,
|
||||
private details: OrderDetails
|
||||
) {}
|
||||
}
|
||||
|
||||
class OrderDetails {
|
||||
constructor(
|
||||
private customer: Customer,
|
||||
private lineItems: LineItems
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. No Getters/Setters/Properties
|
||||
|
||||
Objects should have behavior, not just data. Tell objects what to do.
|
||||
|
||||
```typescript
|
||||
// BAD: Data bag with getters
|
||||
class Account {
|
||||
getBalance(): number { return this.balance; }
|
||||
setBalance(value: number) { this.balance = value; }
|
||||
}
|
||||
|
||||
// Caller does the work
|
||||
if (account.getBalance() >= amount) {
|
||||
account.setBalance(account.getBalance() - amount);
|
||||
}
|
||||
|
||||
// GOOD: Behavior-rich object
|
||||
class Account {
|
||||
withdraw(amount: Money): WithdrawResult {
|
||||
if (!this.canWithdraw(amount)) {
|
||||
return WithdrawResult.insufficientFunds();
|
||||
}
|
||||
this.balance = this.balance.subtract(amount);
|
||||
return WithdrawResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
// Caller tells, object decides
|
||||
const result = account.withdraw(amount);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comments
|
||||
|
||||
### When to Write Comments
|
||||
|
||||
**Only write comments to explain WHY, not WHAT or HOW.**
|
||||
|
||||
Code explains what and how. Comments explain business reasons, non-obvious decisions, or warnings.
|
||||
|
||||
```typescript
|
||||
// BAD: Explains what (redundant)
|
||||
// Add 1 to counter
|
||||
counter++;
|
||||
|
||||
// GOOD: Explains why
|
||||
// Compensate for 0-based indexing in legacy API
|
||||
counter++;
|
||||
```
|
||||
|
||||
### Prefer Self-Documenting Code
|
||||
|
||||
Instead of commenting, rename to make intent clear.
|
||||
|
||||
```typescript
|
||||
// BAD: Comment needed
|
||||
// Check if user can access premium features
|
||||
if (user.subscriptionLevel >= 2 && !user.isBanned) { }
|
||||
|
||||
// GOOD: Self-documenting
|
||||
if (user.canAccessPremiumFeatures()) { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Formatting
|
||||
|
||||
### Vertical Spacing
|
||||
- Related code together
|
||||
- Blank lines between concepts
|
||||
- Most important/public at top
|
||||
|
||||
### Horizontal Spacing
|
||||
- Consistent indentation
|
||||
- Space around operators
|
||||
- Max line length ~80-120 characters
|
||||
|
||||
### Storytelling
|
||||
Code should read top-to-bottom like a story. High-level at top, details below.
|
||||
|
||||
```typescript
|
||||
class OrderProcessor {
|
||||
// Public API first
|
||||
process(order: Order): ProcessResult {
|
||||
this.validate(order);
|
||||
this.calculateTotals(order);
|
||||
return this.save(order);
|
||||
}
|
||||
|
||||
// Supporting methods below, in order of appearance
|
||||
private validate(order: Order): void { ... }
|
||||
private calculateTotals(order: Order): void { ... }
|
||||
private save(order: Order): ProcessResult { ... }
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,334 @@
|
||||
# Code Smells & Anti-Patterns
|
||||
|
||||
## What Are Code Smells?
|
||||
|
||||
Indicators that something MAY be wrong. Not bugs, but design problems that make code hard to understand, change, or test.
|
||||
|
||||
## The Five Categories
|
||||
|
||||
### 1. Bloaters
|
||||
Code that has grown too large.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Long Method** | > 10 lines | Extract Method |
|
||||
| **Large Class** | > 50 lines, multiple responsibilities | Extract Class |
|
||||
| **Long Parameter List** | > 3 parameters | Introduce Parameter Object |
|
||||
| **Data Clumps** | Same group of variables appear together | Extract Class |
|
||||
| **Primitive Obsession** | Primitives instead of small objects | Wrap in Value Object |
|
||||
|
||||
### 2. Object-Orientation Abusers
|
||||
Misuse of OO principles.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Switch Statements** | Type checking, large switch/if-else | Replace with Polymorphism |
|
||||
| **Parallel Inheritance** | Adding subclass requires adding another | Merge Hierarchies |
|
||||
| **Refused Bequest** | Subclass doesn't use parent methods | Replace Inheritance with Delegation |
|
||||
| **Alternative Classes** | Different interfaces, same concept | Rename, Extract Superclass |
|
||||
|
||||
### 3. Change Preventers
|
||||
Code that makes changes difficult.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Divergent Change** | One class changed for many reasons | Extract Class (SRP) |
|
||||
| **Shotgun Surgery** | One change touches many classes | Move Method/Field together |
|
||||
| **Parallel Inheritance** | (see above) | Merge Hierarchies |
|
||||
|
||||
### 4. Dispensables
|
||||
Code that can be removed.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Comments** | Explaining bad code | Rename, Extract Method |
|
||||
| **Duplicate Code** | Copy-paste | Extract Method, Pull Up Method |
|
||||
| **Dead Code** | Unreachable code | Delete |
|
||||
| **Speculative Generality** | "Just in case" code | Delete (YAGNI) |
|
||||
| **Lazy Class** | Class that does almost nothing | Inline Class |
|
||||
|
||||
### 5. Couplers
|
||||
Excessive coupling between classes.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Feature Envy** | Method uses another class's data extensively | Move Method |
|
||||
| **Inappropriate Intimacy** | Classes know too much about each other | Move Method, Extract Class |
|
||||
| **Message Chains** | `a.getB().getC().getD()` | Hide Delegate |
|
||||
| **Middle Man** | Class only delegates | Inline Class |
|
||||
|
||||
---
|
||||
|
||||
## The Seven Most Common Code Smells
|
||||
|
||||
### 1. Long Method
|
||||
|
||||
**Symptom:** Method > 10 lines, doing multiple things.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
function processOrder(order: Order) {
|
||||
// Validate
|
||||
if (!order.items.length) throw new Error('Empty');
|
||||
if (!order.customer) throw new Error('No customer');
|
||||
|
||||
// Calculate
|
||||
let total = 0;
|
||||
for (const item of order.items) {
|
||||
total += item.price * item.quantity;
|
||||
if (item.discount) {
|
||||
total -= item.discount;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply tax
|
||||
const taxRate = getTaxRate(order.customer.state);
|
||||
total = total * (1 + taxRate);
|
||||
|
||||
// Save
|
||||
db.orders.insert({ ...order, total });
|
||||
|
||||
// Notify
|
||||
emailService.send(order.customer.email, 'Order confirmed');
|
||||
}
|
||||
|
||||
// REFACTORED
|
||||
function processOrder(order: Order) {
|
||||
validateOrder(order);
|
||||
const total = calculateTotal(order);
|
||||
saveOrder(order, total);
|
||||
notifyCustomer(order);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Large Class
|
||||
|
||||
**Symptom:** Class with many responsibilities, > 50 lines.
|
||||
|
||||
```typescript
|
||||
// SMELL: God class
|
||||
class User {
|
||||
// User data
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
// Authentication
|
||||
login() { }
|
||||
logout() { }
|
||||
resetPassword() { }
|
||||
|
||||
// Preferences
|
||||
setTheme() { }
|
||||
setLanguage() { }
|
||||
|
||||
// Notifications
|
||||
sendEmail() { }
|
||||
sendSMS() { }
|
||||
|
||||
// Billing
|
||||
charge() { }
|
||||
refund() { }
|
||||
}
|
||||
|
||||
// REFACTORED: Separate classes
|
||||
class User { name: string; email: string; }
|
||||
class AuthService { login(); logout(); resetPassword(); }
|
||||
class UserPreferences { setTheme(); setLanguage(); }
|
||||
class NotificationService { sendEmail(); sendSMS(); }
|
||||
class BillingService { charge(); refund(); }
|
||||
```
|
||||
|
||||
### 3. Feature Envy
|
||||
|
||||
**Symptom:** Method uses another class's data more than its own.
|
||||
|
||||
```typescript
|
||||
// SMELL: Order envies Customer
|
||||
class Order {
|
||||
calculateShipping(customer: Customer): number {
|
||||
if (customer.country === 'US') {
|
||||
if (customer.state === 'CA') return 10;
|
||||
return 15;
|
||||
}
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
|
||||
// REFACTORED: Move to Customer
|
||||
class Customer {
|
||||
getShippingCost(): number {
|
||||
if (this.country === 'US') {
|
||||
if (this.state === 'CA') return 10;
|
||||
return 15;
|
||||
}
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
calculateShipping(): number {
|
||||
return this.customer.getShippingCost();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Primitive Obsession
|
||||
|
||||
**Symptom:** Using primitives for domain concepts.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
function createUser(email: string, age: number, zipCode: string) {
|
||||
// No validation, easy to pass wrong values
|
||||
if (!email.includes('@')) throw new Error();
|
||||
if (age < 0) throw new Error();
|
||||
}
|
||||
|
||||
// REFACTORED: Value objects
|
||||
class Email {
|
||||
constructor(private value: string) {
|
||||
if (!value.includes('@')) throw new InvalidEmail();
|
||||
}
|
||||
}
|
||||
|
||||
class Age {
|
||||
constructor(private value: number) {
|
||||
if (value < 0 || value > 150) throw new InvalidAge();
|
||||
}
|
||||
}
|
||||
|
||||
function createUser(email: Email, age: Age, address: Address) {
|
||||
// Type system prevents invalid data
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Switch Statements
|
||||
|
||||
**Symptom:** Switching on type, repeated across codebase.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
function getArea(shape: Shape): number {
|
||||
switch (shape.type) {
|
||||
case 'circle': return Math.PI * shape.radius ** 2;
|
||||
case 'rectangle': return shape.width * shape.height;
|
||||
case 'triangle': return 0.5 * shape.base * shape.height;
|
||||
}
|
||||
}
|
||||
|
||||
function getPerimeter(shape: Shape): number {
|
||||
switch (shape.type) { // Same switch again!
|
||||
case 'circle': return 2 * Math.PI * shape.radius;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// REFACTORED: Polymorphism
|
||||
interface Shape {
|
||||
getArea(): number;
|
||||
getPerimeter(): number;
|
||||
}
|
||||
|
||||
class Circle implements Shape {
|
||||
constructor(private radius: number) {}
|
||||
getArea(): number { return Math.PI * this.radius ** 2; }
|
||||
getPerimeter(): number { return 2 * Math.PI * this.radius; }
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Inappropriate Intimacy
|
||||
|
||||
**Symptom:** Classes know too much about each other's internals.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
class Order {
|
||||
process() {
|
||||
const inventory = new Inventory();
|
||||
// Reaching into inventory's internals
|
||||
for (const item of this.items) {
|
||||
const stock = inventory.stockLevels[item.sku];
|
||||
if (stock.quantity < item.quantity) {
|
||||
throw new Error('Out of stock');
|
||||
}
|
||||
inventory.stockLevels[item.sku].quantity -= item.quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// REFACTORED: Tell, don't ask
|
||||
class Inventory {
|
||||
reserve(items: OrderItem[]): ReserveResult {
|
||||
// Inventory manages its own state
|
||||
for (const item of items) {
|
||||
if (!this.canReserve(item)) {
|
||||
return ReserveResult.outOfStock(item);
|
||||
}
|
||||
}
|
||||
this.deductStock(items);
|
||||
return ReserveResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
process(inventory: Inventory) {
|
||||
const result = inventory.reserve(this.items);
|
||||
if (!result.isSuccess()) {
|
||||
throw new OutOfStockError(result.failedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Speculative Generality
|
||||
|
||||
**Symptom:** "Just in case" abstractions that aren't used.
|
||||
|
||||
```typescript
|
||||
// SMELL: Over-engineered for hypothetical needs
|
||||
interface PaymentProcessor {
|
||||
process(): void;
|
||||
rollback(): void;
|
||||
audit(): void;
|
||||
generateReport(): void;
|
||||
scheduleRecurring(): void;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor {
|
||||
process() { /* actual code */ }
|
||||
rollback() { throw new Error('Not implemented'); }
|
||||
audit() { throw new Error('Not implemented'); }
|
||||
generateReport() { throw new Error('Not implemented'); }
|
||||
scheduleRecurring() { throw new Error('Not implemented'); }
|
||||
}
|
||||
|
||||
// REFACTORED: YAGNI
|
||||
interface PaymentProcessor {
|
||||
process(): void;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor {
|
||||
process() { /* actual code */ }
|
||||
}
|
||||
// Add other methods when actually needed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prevention Strategies
|
||||
|
||||
1. **Follow Object Calisthenics** - Rules prevent most smells
|
||||
2. **Practice TDD** - Tests reveal design problems early
|
||||
3. **Review in pairs** - Fresh eyes catch smells
|
||||
4. **Refactor continuously** - Don't let smells accumulate
|
||||
5. **Apply SOLID** - Prevents structural smells
|
||||
6. **Use static analysis** - Tools catch common issues
|
||||
|
||||
---
|
||||
|
||||
## When You Find a Smell
|
||||
|
||||
1. **Confirm it's a problem** - Not all smells need fixing
|
||||
2. **Ensure test coverage** - Before refactoring
|
||||
3. **Refactor in small steps** - Keep tests passing
|
||||
4. **Commit frequently** - Easy to revert if needed
|
||||
@@ -0,0 +1,284 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Systematic code smell detection and Go-specific review checklist. Load before any pre-merge review.
|
||||
---
|
||||
|
||||
# Code Review
|
||||
|
||||
## Overview
|
||||
|
||||
Code review is smell detection and principle verification, not style policing. The goal is to identify patterns that make code hard to understand, change, test, or safely operate.
|
||||
|
||||
**This skill is read-only analysis.** When you find issues, document them with specific file:line references and prioritize by architectural impact. For fixes, load the `refactoring` or `clean-code` skill.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Pre-merge review of any pull request
|
||||
- Before committing a significant change
|
||||
- When asked to "review this code"
|
||||
- After the GREEN phase of TDD (use with `clean-code` skill)
|
||||
|
||||
## Review Process
|
||||
|
||||
### Phase 0: Iron Laws (block on any violation)
|
||||
|
||||
Before reading the diff, scan for these. Any violation is a blocking finding regardless of how clean the rest of the change looks.
|
||||
|
||||
1. **No security vulnerabilities** — command injection, SQL injection, credential exposure, path traversal, unchecked input at system boundaries
|
||||
2. **No silently swallowed errors** — `err != nil` without wrapping, logging, or surfacing is always wrong; `_ = f()` on a fallible call is wrong
|
||||
3. **No missing validation at system boundaries** — user input, external API responses, and file reads must be validated before use
|
||||
|
||||
If any iron law is violated, flag it as **Blocking** before any other phase. The other phases still run, but the iron-law findings come first in the report. See Phase 3 for detailed patterns and code examples covering iron laws 2 and 3.
|
||||
|
||||
### Phase 1: Context
|
||||
|
||||
Before examining code:
|
||||
1. Understand what the change is trying to accomplish
|
||||
2. Identify the affected packages and their purpose
|
||||
3. Check test coverage — is the behavior tested?
|
||||
4. Read the commit message and PR description
|
||||
|
||||
### Phase 2: Architectural Analysis (Highest Priority)
|
||||
|
||||
Check for structural problems first. These are expensive to fix later.
|
||||
|
||||
**Dependency violations:**
|
||||
- Does domain logic import infrastructure packages?
|
||||
- Are there circular imports between packages?
|
||||
- Does a type do things that multiple different stakeholders would request changes to?
|
||||
|
||||
**Abstraction problems:**
|
||||
- Is there a large switch/if-else on a type string that should be an interface?
|
||||
- Are there God types that know too much about too many things?
|
||||
- Is there Shotgun Surgery — one logical change requires editing many files?
|
||||
|
||||
### Phase 3: Go-Specific Checklist
|
||||
|
||||
#### Error Handling
|
||||
|
||||
```go
|
||||
// Required: wrap with context
|
||||
return fmt.Errorf("parse invoice: %w", err)
|
||||
|
||||
// Forbidden: naked return
|
||||
return err
|
||||
|
||||
// Forbidden: log and return (pick one)
|
||||
log.Error("failed", "err", err)
|
||||
return err
|
||||
|
||||
// Forbidden: silently ignore
|
||||
_ = doSomething()
|
||||
|
||||
// Suspicious: sentinel errors without documentation
|
||||
var ErrFoo = errors.New("foo") // Is this exported? Is it documented?
|
||||
```
|
||||
|
||||
#### Goroutine Safety
|
||||
|
||||
- Is shared state protected by a mutex or accessed only via channels?
|
||||
- Are goroutines guaranteed to terminate (no goroutine leaks)?
|
||||
- Is `sync.WaitGroup` used correctly (Add before go, Done via defer)?
|
||||
- Is there a `go test -race` passing?
|
||||
|
||||
```go
|
||||
// Check: is this safe for concurrent use?
|
||||
type Cache struct {
|
||||
mu sync.RWMutex // Good: explicit lock
|
||||
items map[string]Item
|
||||
}
|
||||
|
||||
// Red flag: map access without lock in concurrent context
|
||||
type BadCache struct {
|
||||
items map[string]Item // Unsafe if accessed from multiple goroutines
|
||||
}
|
||||
```
|
||||
|
||||
#### Exported API Minimalism
|
||||
|
||||
- Only export what callers outside the package actually need
|
||||
- Internal implementation details should be unexported
|
||||
- Does the exported API make sense as a standalone contract?
|
||||
|
||||
```go
|
||||
// Good: minimal exported surface
|
||||
type UserService struct { /* unexported fields */ }
|
||||
func NewUserService(...) *UserService { ... }
|
||||
func (s *UserService) GetUser(ctx context.Context, id UserID) (User, error) { ... }
|
||||
|
||||
// Bad: over-exported internals
|
||||
type UserService struct {
|
||||
Store UserStore // Exported field — callers can replace it, breaks encapsulation
|
||||
Logger *slog.Logger // Exported — callers can mutate it
|
||||
}
|
||||
```
|
||||
|
||||
#### Dependency Safety
|
||||
|
||||
Before adding any new dependency:
|
||||
- Has `govulncheck` been run? (`govulncheck ./...`)
|
||||
- Is there a justification in the commit message for non-stdlib additions?
|
||||
- Pre-approved without justification: `testify`, `slog`, `templ`, `sqlc`
|
||||
- Everything else needs a reason
|
||||
|
||||
#### Generic/interface{} Usage
|
||||
|
||||
```go
|
||||
// Bad: any/interface{} when a concrete type or generic works
|
||||
func Process(data interface{}) error { ... }
|
||||
|
||||
// Good: typed
|
||||
func Process[T Processor](data T) error { ... }
|
||||
|
||||
// Good: concrete type when only one type is expected
|
||||
func ProcessOrder(o *Order) error { ... }
|
||||
```
|
||||
|
||||
#### Context Propagation
|
||||
|
||||
- Every function that does I/O (DB, HTTP, file) should accept `ctx context.Context` as the first parameter
|
||||
- Context should be passed down, not stored in structs
|
||||
- `context.Background()` should only appear at the top-level (main, test setup)
|
||||
|
||||
```go
|
||||
// Good
|
||||
func (s *Store) GetUser(ctx context.Context, id UserID) (User, error) { ... }
|
||||
|
||||
// Bad: no context — can't cancel, can't trace, can't timeout
|
||||
func (s *Store) GetUser(id UserID) (User, error) { ... }
|
||||
|
||||
// Bad: context stored in struct
|
||||
type Handler struct {
|
||||
ctx context.Context // Don't do this
|
||||
}
|
||||
```
|
||||
|
||||
#### Logging with slog
|
||||
|
||||
```go
|
||||
// Good: structured, leveled
|
||||
slog.InfoContext(ctx, "user created", "user_id", u.ID, "email", u.Email)
|
||||
|
||||
// Bad: unstructured
|
||||
log.Printf("user created: %s", u.Email)
|
||||
|
||||
// Bad: logging errors that are also returned
|
||||
slog.Error("failed to save", "err", err)
|
||||
return fmt.Errorf("save user: %w", err) // caller is double-counting
|
||||
```
|
||||
|
||||
### Phase 4: Code Smell Detection
|
||||
|
||||
Apply the catalog from `clean-code/references/code-smells.md`. Key smells to prioritize:
|
||||
|
||||
**Architectural impact (fix immediately):**
|
||||
- Global Data — package-level variables modified at runtime
|
||||
- Shotgun Surgery — one change touches many unrelated files
|
||||
- Feature Envy — a method uses another package's internals more than its own
|
||||
- God Object — a type that does everything
|
||||
|
||||
**Design impact (fix before merge if possible):**
|
||||
- Long Method — functions > ~50 lines in Go (Go can be longer than OOP, but be honest)
|
||||
- Long Parameter List — > 4-5 parameters; consider a config struct
|
||||
- Duplicate Code — same logic copy-pasted (wait for Rule of Three)
|
||||
- Message Chain — `a.B().C().D()` — violates Law of Demeter
|
||||
- Primitive Obsession — using `string` for email, user ID, currency
|
||||
|
||||
**Readability (note in review, fix if cheap):**
|
||||
- Magic Numbers — bare literals without named constants
|
||||
- Uncommunicative Names — `data`, `info`, `x`, `tmp`
|
||||
- Fallacious Comments — comments that don't match the code
|
||||
|
||||
### Phase 5: Test Quality
|
||||
|
||||
- Are new behaviors covered by tests?
|
||||
- Are tests table-driven where appropriate?
|
||||
- Do tests test behavior, not implementation?
|
||||
- Are there mock-only tests that test nothing real?
|
||||
|
||||
For comprehensive test review, load the `test-design` skill.
|
||||
|
||||
## Severity Classification
|
||||
|
||||
| Severity | Description | Action |
|
||||
|----------|-------------|--------|
|
||||
| **Blocking** | Security issue, data corruption risk, broken behavior | Must fix before merge |
|
||||
| **High** | Architectural violation, goroutine safety, missing context | Fix before merge |
|
||||
| **Medium** | Design smell, missing tests for changed behavior | Fix in this PR or create issue |
|
||||
| **Low** | Naming, style, minor duplication | Note in review, optional |
|
||||
|
||||
## Output Format
|
||||
|
||||
When documenting findings:
|
||||
|
||||
```markdown
|
||||
## Code Review: [package/file]
|
||||
|
||||
### Blocking
|
||||
- `internal/store/user.go:47`: Error swallowed with `_ =` — callers can't know if Save failed
|
||||
|
||||
### High
|
||||
- `internal/handler/order.go:12`: Domain logic in HTTP handler — moves tax calculation out of service layer
|
||||
- `cmd/server/main.go:89`: Missing context propagation — `GetUser` call has no timeout
|
||||
|
||||
### Medium
|
||||
- `internal/service/invoice.go:34`: Long method (87 lines) — extract `calculateLineItems` and `applyTaxRules`
|
||||
|
||||
### Low
|
||||
- `internal/domain/user.go:5`: Field `Data string` — name reveals nothing about what data
|
||||
```
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
The brain MCP holds prior review decisions across sessions. Use it to avoid re-litigating the same tradeoffs.
|
||||
|
||||
**At review start:**
|
||||
- Run `brain_query` with the package or feature name + "review" to surface prior review findings, recurring smells, or architectural decisions for this area. This prevents flagging the same smell that was already accepted as a tradeoff.
|
||||
|
||||
**After review completes:**
|
||||
- For findings the author accepts as a tradeoff (with rationale), run `brain_write` to record the decision. Future reviews will not re-flag it.
|
||||
|
||||
**Never:**
|
||||
- Embed brain queries in the review output. They are context for you, not findings for the author.
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||
|
||||
**At end of each phase:**
|
||||
- `session_log` with `{skill: "code-review", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**Phases for this skill:** phase-0-iron-laws, phase-1-context, phase-2-architectural-analysis, phase-3-go-checklist, phase-4-code-smell-detection, phase-5-test-quality
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached.
|
||||
- `fail` — the phase's intended outcome was NOT reached.
|
||||
- `skip` — phase was skipped intentionally.
|
||||
|
||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
||||
|
||||
## Common Go Anti-Patterns
|
||||
|
||||
| Anti-Pattern | What to Look For |
|
||||
|-------------|-----------------|
|
||||
| Goroutine leak | `go func()` without a done channel or WaitGroup |
|
||||
| Error ignored | `_ = f()` or missing `if err != nil` after a fallible call |
|
||||
| Nil map write | `m[k] = v` without `m = make(map[...]{})` initialization |
|
||||
| Race on map | Concurrent map read/write without mutex |
|
||||
| ctx in struct | `type T struct { ctx context.Context }` |
|
||||
| Naked panic | `panic("something went wrong")` in non-initialization code |
|
||||
| fmt.Println in library | Use `slog` or accept a logger; never print to stdout from lib code |
|
||||
| init() abuse | `init()` with side effects that make testing hard |
|
||||
|
||||
## References
|
||||
|
||||
- `clean-code/references/code-smells.md` — comprehensive smell catalog
|
||||
- Load `refactoring` skill when recommending fixes
|
||||
- Load `test-design` skill for reviewing test quality
|
||||
- Load `solid` skill for SOLID principle violations
|
||||
|
||||
## Mode 2 Routing Note
|
||||
|
||||
This skill is invoked identically in Mode 1 (cloud Claude) and Mode 2 (client-local with supervisor routing). The routing layer (Plan 6) does not exist yet; treat this skill as Mode 1 only for now. Findings format and severity classification are unchanged across modes.
|
||||
@@ -0,0 +1,346 @@
|
||||
---
|
||||
name: cognitive-load
|
||||
description: Measure how much mental effort a codebase demands from developers. Use when identifying hotspots, before architectural decisions, or when code feels hard to understand.
|
||||
---
|
||||
|
||||
# Cognitive Load Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
Cognitive load is the mental effort required to understand, modify, and reason about code. High cognitive load leads to bugs, slow development, and burnout.
|
||||
|
||||
This skill provides a framework for measuring and reducing cognitive load using the **Cognitive Load Index (CLI)**, scored 0–1000.
|
||||
|
||||
## CLI Score Rating Scale
|
||||
|
||||
| CLI Score | Rating | Action |
|
||||
|-----------|--------|--------|
|
||||
| 0–100 | Excellent | Model codebase |
|
||||
| 101–250 | Good | Minor improvements |
|
||||
| 251–400 | Moderate | Address hotspots |
|
||||
| 401–600 | Concerning | Plan refactoring |
|
||||
| 601–800 | Poor | Significant refactoring needed |
|
||||
| 801–999 | Severe | Consider rewrite of affected areas |
|
||||
|
||||
## The 8 Dimensions
|
||||
|
||||
### D1: Structural Complexity
|
||||
|
||||
Cyclomatic complexity — the number of independent paths through code. Each `if`, `for`, `switch`, `case`, `&&`, `||` adds one.
|
||||
|
||||
**Thresholds for Go:**
|
||||
- Simple function: < 5 (no issue)
|
||||
- Moderate: 5–10 (watch closely)
|
||||
- Complex: 10–15 (consider refactoring)
|
||||
- Critical: > 15 (must refactor)
|
||||
|
||||
**Detection:**
|
||||
```bash
|
||||
# Install: go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
|
||||
gocyclo -over 10 ./...
|
||||
```
|
||||
|
||||
**Go hotspots:** Request handlers that mix auth, validation, business logic, and response formatting in one function.
|
||||
|
||||
### D2: Nesting Depth
|
||||
|
||||
Maximum nesting depth — how many levels deep a reader must track.
|
||||
|
||||
**Thresholds for Go:**
|
||||
- Ideal: 1–2 levels
|
||||
- Acceptable: 3 levels
|
||||
- Warning: 4 levels
|
||||
- Critical: 5+ levels
|
||||
|
||||
**Fix:** Early returns and guard clauses eliminate deep nesting:
|
||||
```go
|
||||
// Bad: 4 levels deep
|
||||
func process(r *http.Request) error {
|
||||
if r != nil {
|
||||
if r.Body != nil {
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err == nil {
|
||||
// actual work at level 4
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Good: guard clauses, 1 level
|
||||
func process(r *http.Request) error {
|
||||
if r == nil {
|
||||
return errors.New("nil request")
|
||||
}
|
||||
if r.Body == nil {
|
||||
return errors.New("no request body")
|
||||
}
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
// actual work at level 1
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### D3: Volume/Size
|
||||
|
||||
Lines of code per file and function. Size alone isn't the issue — hidden responsibilities are.
|
||||
|
||||
**Thresholds for Go:**
|
||||
- Function: > 50 lines is a signal (Go can go longer, but be honest about why)
|
||||
- File: > 400 lines often means too many responsibilities
|
||||
- Package: > 2000 lines total may need splitting
|
||||
|
||||
**Go note:** Table-driven tests inflate LOC legitimately. Distinguish test LOC from production LOC when assessing.
|
||||
|
||||
### D4: Naming Quality
|
||||
|
||||
Identifiers that don't reveal intent force readers to build a mental model from context.
|
||||
|
||||
**Poor naming signals:**
|
||||
- Single-letter variables outside loop indices: `x`, `d`, `tmp`
|
||||
- Generic suffixes: `Manager`, `Handler`, `Processor`, `Data`, `Info`, `Util`
|
||||
- Abbreviations: `usrLst`, `custRepo`, `cfg2`
|
||||
- Inconsistency: `getUserById`, `FetchCustomerByID`, `retrieveClientById` for the same concept
|
||||
|
||||
**Good naming in Go:**
|
||||
```go
|
||||
// Bad
|
||||
func (m *Manager) proc(d interface{}) error { ... }
|
||||
|
||||
// Good
|
||||
func (s *OrderService) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) { ... }
|
||||
```
|
||||
|
||||
### D5: Coupling
|
||||
|
||||
How many packages/types a given unit depends on. High coupling means a change in one place ripples widely.
|
||||
|
||||
**Detection:**
|
||||
```bash
|
||||
# Count imports per file
|
||||
grep -c "^import" *.go
|
||||
# or examine import blocks manually
|
||||
```
|
||||
|
||||
**Go-specific coupling smells:**
|
||||
- A domain type that imports from `database/sql`, `net/http`, and `smtp` simultaneously
|
||||
- A package that imports more than 10 other internal packages
|
||||
- Circular imports (Go compiler rejects these, but near-circular is still a smell)
|
||||
|
||||
### D6: Cohesion
|
||||
|
||||
Whether things that belong together are together. Low cohesion means related code is scattered.
|
||||
|
||||
**Signs of low cohesion:**
|
||||
- A function that calls many unrelated packages
|
||||
- A package where functions don't share data or purpose
|
||||
- Utility packages that grow without bound (`util`, `common`, `helpers`)
|
||||
|
||||
### D7: Duplication
|
||||
|
||||
Repeated logic that must be updated in multiple places when requirements change.
|
||||
|
||||
**Detection:**
|
||||
```bash
|
||||
# Simple: find identical error messages that suggest copy-paste
|
||||
grep -r "error:" --include="*.go" | sort | uniq -d
|
||||
```
|
||||
|
||||
**Go note:** Apply the Rule of Three — don't extract the first or second duplication. The third is the signal.
|
||||
|
||||
### D8: Navigability
|
||||
|
||||
How easily can a reader find relevant code and understand entry points?
|
||||
|
||||
**Signs of poor navigability:**
|
||||
- No clear entry point (no `main.go`, no `New*` constructors)
|
||||
- Package names that don't reflect content
|
||||
- Files that mix many unrelated types
|
||||
- Deep directory nesting with unclear boundaries
|
||||
|
||||
## Go-Specific Cognitive Load Hotspots
|
||||
|
||||
### Deeply Nested Error Handling
|
||||
|
||||
Go's explicit error handling is a feature, but it can create visual noise:
|
||||
|
||||
```go
|
||||
// High cognitive load: multiple nested error paths
|
||||
func (s *Service) Process(ctx context.Context, id string) error {
|
||||
u, err := s.store.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
o, err := s.store.GetOrgByUserID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return ErrEntityNotFound
|
||||
}
|
||||
return fmt.Errorf("get org: %w", err)
|
||||
}
|
||||
// ... more nesting
|
||||
}
|
||||
return fmt.Errorf("get user: %w", err)
|
||||
}
|
||||
// ... happy path
|
||||
}
|
||||
|
||||
// Lower cognitive load: extract to focused functions
|
||||
func (s *Service) Process(ctx context.Context, id string) error {
|
||||
entity, err := s.resolveEntity(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve entity: %w", err)
|
||||
}
|
||||
return s.processEntity(ctx, entity)
|
||||
}
|
||||
```
|
||||
|
||||
### Goroutine Soup
|
||||
|
||||
Goroutines without clear lifecycle management are high cognitive load:
|
||||
|
||||
```go
|
||||
// High load: who owns these goroutines? When do they stop?
|
||||
func startWorkers() {
|
||||
go processQueue()
|
||||
go cleanupExpired()
|
||||
go sendNotifications()
|
||||
}
|
||||
|
||||
// Lower load: explicit lifecycle with context and WaitGroup
|
||||
func startWorkers(ctx context.Context) *WorkerPool {
|
||||
pool := &WorkerPool{}
|
||||
pool.wg.Add(3)
|
||||
go pool.processQueue(ctx)
|
||||
go pool.cleanupExpired(ctx)
|
||||
go pool.sendNotifications(ctx)
|
||||
return pool
|
||||
}
|
||||
|
||||
func (p *WorkerPool) Shutdown() {
|
||||
p.cancel()
|
||||
p.wg.Wait()
|
||||
}
|
||||
```
|
||||
|
||||
### Over-Generalization with Generics
|
||||
|
||||
Generics add expressive power but also cognitive load. Use them sparingly:
|
||||
|
||||
```go
|
||||
// High load: generic just because you can
|
||||
func Transform[T any, U any](items []T, fn func(T) U) []U { ... }
|
||||
|
||||
// Lower load: concrete types when you have only one use case
|
||||
func transformOrders(orders []Order) []OrderSummary { ... }
|
||||
```
|
||||
|
||||
Extract generics only when you genuinely have multiple type applications.
|
||||
|
||||
### Init Functions
|
||||
|
||||
`init()` functions run invisibly and create hidden state:
|
||||
|
||||
```go
|
||||
// High load: what order do these run? What state do they set?
|
||||
func init() {
|
||||
db = mustConnectDB()
|
||||
cache = newCache()
|
||||
}
|
||||
|
||||
// Lower load: explicit initialization in main or constructor
|
||||
func main() {
|
||||
db := mustConnectDB(cfg.DatabaseURL)
|
||||
cache := newCache(cfg.CacheSize)
|
||||
svc := NewService(db, cache)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Long Interfaces
|
||||
|
||||
An interface with 10 methods forces implementors to understand all 10 methods before reasoning about any:
|
||||
|
||||
```go
|
||||
// High cognitive load: 10 methods before you can implement
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, u User) error
|
||||
GetByID(ctx context.Context, id UserID) (User, error)
|
||||
GetByEmail(ctx context.Context, email string) (User, error)
|
||||
Update(ctx context.Context, u User) error
|
||||
Delete(ctx context.Context, id UserID) error
|
||||
List(ctx context.Context, filter Filter) ([]User, error)
|
||||
Count(ctx context.Context, filter Filter) (int64, error)
|
||||
Exists(ctx context.Context, id UserID) (bool, error)
|
||||
Lock(ctx context.Context, id UserID) error
|
||||
Unlock(ctx context.Context, id UserID) error
|
||||
}
|
||||
|
||||
// Lower load: separate by use case
|
||||
type UserReader interface {
|
||||
GetByID(ctx context.Context, id UserID) (User, error)
|
||||
GetByEmail(ctx context.Context, email string) (User, error)
|
||||
}
|
||||
|
||||
type UserWriter interface {
|
||||
Create(ctx context.Context, u User) error
|
||||
Update(ctx context.Context, u User) error
|
||||
Delete(ctx context.Context, id UserID) error
|
||||
}
|
||||
```
|
||||
|
||||
## When to Measure
|
||||
|
||||
- Before starting refactoring work: establish a baseline
|
||||
- When a package/file is frequently the source of bugs
|
||||
- When team members consistently report confusion about a subsystem
|
||||
- Before a major feature addition that will touch complex code
|
||||
|
||||
## Hotspot Identification Process
|
||||
|
||||
1. **Size sweep:** Find files > 400 LOC and functions > 50 LOC
|
||||
```bash
|
||||
find . -name "*.go" ! -name "*_test.go" | xargs wc -l | sort -rn | head -20
|
||||
```
|
||||
|
||||
2. **Complexity sweep:** Find high cyclomatic complexity
|
||||
```bash
|
||||
gocyclo -over 10 ./...
|
||||
```
|
||||
|
||||
3. **Naming sweep:** Find packages with vague names
|
||||
```bash
|
||||
ls internal/ # Are there `util`, `common`, `helpers`?
|
||||
```
|
||||
|
||||
4. **Coupling sweep:** Find files with many imports
|
||||
```bash
|
||||
grep -l "^import" **/*.go | xargs grep -c '"' | sort -t: -k2 -rn | head -10
|
||||
```
|
||||
|
||||
5. **Duplication sweep:**
|
||||
```bash
|
||||
# Look for copied error strings, identical function signatures
|
||||
grep -rn "TODO\|FIXME\|HACK" --include="*.go" # Technical debt markers
|
||||
```
|
||||
|
||||
## Reduction Strategies
|
||||
|
||||
| High CLI Dimension | Primary Fix |
|
||||
|-------------------|-------------|
|
||||
| D1: Structural Complexity | Extract function, replace conditional with polymorphism |
|
||||
| D2: Nesting Depth | Guard clauses, early returns |
|
||||
| D3: Volume/Size | Extract type, split package |
|
||||
| D4: Naming | Rename (most impactful, cheapest refactoring) |
|
||||
| D5: Coupling | Introduce interface, apply DIP |
|
||||
| D6: Cohesion | Extract type, reorganize package |
|
||||
| D7: Duplication | Extract shared function (after Rule of Three) |
|
||||
| D8: Navigability | Reorganize package structure, add package docs |
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Load `refactoring` skill for systematic techniques to reduce identified hotspots
|
||||
- Load `solid` skill for structural fixes (coupling, cohesion)
|
||||
- Load `clean-code` skill for naming improvements
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: debug
|
||||
description: Systematic hypothesis-first debugging. Generate 3-5 ranked hypotheses with verification steps before suggesting any fix. Use when encountering a bug, test failure, or unexpected behavior.
|
||||
---
|
||||
|
||||
# Debug
|
||||
|
||||
## Overview
|
||||
|
||||
Debugging is hypothesis generation, not fix generation. The first instinct — "try this and see if it works" — wastes time and teaches nothing. A disciplined debug session produces a small set of ranked, falsifiable hypotheses, each with a concrete verification command. The fix comes after one hypothesis is confirmed, not before.
|
||||
|
||||
**Core principle:** A hypothesis you cannot verify in one command is not a hypothesis — it is a guess.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Any test failure whose cause is not immediately obvious
|
||||
- Any production error or unexpected behavior
|
||||
- Any bug report from a user or stakeholder
|
||||
- Before reaching for a debugger or adding print statements: form hypotheses first
|
||||
|
||||
**Do not use for:**
|
||||
- Compile errors with clear messages (just fix the typo)
|
||||
- Already-diagnosed bugs where you know the cause (go straight to TDD with a regression test)
|
||||
|
||||
## Iron Laws
|
||||
|
||||
1. **Never suggest "try X and see what happens."** Every hypothesis must have a specific expected outcome if correct.
|
||||
2. **Generate 3-5 hypotheses, ordered by likelihood (most likely first).** Fewer than 3 means you stopped thinking; more than 5 means you are not prioritizing.
|
||||
3. **Diagnose only — do not fix in this skill.** The fix happens in a separate TDD cycle (load `tdd` skill) once a hypothesis is confirmed.
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Read the failure
|
||||
|
||||
Before forming hypotheses:
|
||||
- Read the full error message and stack trace, not just the headline
|
||||
- Read the file where the failure originated, around the failing line
|
||||
- If the failure is from a test, read the test and the code under test
|
||||
- Identify the **failure mode** — what actually went wrong (e.g. "nil pointer dereference in goroutine spawned by handler") not just what the error says ("runtime error")
|
||||
|
||||
### Step 2: Generate hypotheses
|
||||
|
||||
For each hypothesis, capture three things:
|
||||
|
||||
- **Mechanism:** what specific code path or state would produce this exact failure
|
||||
- **Verification:** the single command or file inspection that confirms or denies it
|
||||
- **Expected outcome if correct:** the specific output you would see
|
||||
|
||||
Order by likelihood. The most likely cause is hypothesis 1.
|
||||
|
||||
### Step 3: Output
|
||||
|
||||
Use this format:
|
||||
|
||||
```
|
||||
HYPOTHESIS 1 (likelihood: high): <mechanism in one sentence>
|
||||
VERIFY: <exact command or file:line to inspect>
|
||||
EXPECTED IF CORRECT: <specific output, value, or condition>
|
||||
|
||||
HYPOTHESIS 2 (likelihood: medium): <mechanism>
|
||||
VERIFY: <exact command>
|
||||
EXPECTED IF CORRECT: <specific output>
|
||||
|
||||
[... up to 5 ...]
|
||||
|
||||
RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
||||
```
|
||||
|
||||
End with the recommendation, not a fix.
|
||||
|
||||
## Worked Example
|
||||
|
||||
**Failure:**
|
||||
```
|
||||
--- FAIL: TestInvoiceParser_HandlesEmptyPDF (0.00s)
|
||||
parser_test.go:47: panic: runtime error: index out of range [0] with length 0
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
HYPOTHESIS 1 (likelihood: high): parser indexes into pages[0] before checking len(pages) > 0; empty PDFs produce a zero-page document
|
||||
VERIFY: rg -n 'pages\[0\]' internal/parser/
|
||||
EXPECTED IF CORRECT: at least one site reads pages[0] without a preceding length check
|
||||
|
||||
HYPOTHESIS 2 (likelihood: medium): the test fixture is a zero-byte file rather than a valid empty PDF; pdf library returns nil pages slice instead of empty
|
||||
VERIFY: ls -la testdata/empty.pdf && file testdata/empty.pdf
|
||||
EXPECTED IF CORRECT: file size 0 bytes or "data" rather than "PDF document"
|
||||
|
||||
HYPOTHESIS 3 (likelihood: low): a recently changed dependency reordered the page-extraction API; pages[0] now refers to metadata, not content
|
||||
VERIFY: git log --oneline -10 -- go.sum | grep -i pdf
|
||||
EXPECTED IF CORRECT: a pdf-library bump in the last few commits
|
||||
|
||||
RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why It Fails |
|
||||
|---|---|
|
||||
| "Maybe try restarting the service" | Not a mechanism. Not falsifiable. Teaches nothing if it works. |
|
||||
| "Could be a race condition" | Mechanism without specifics. Which two operations race? On what state? |
|
||||
| "Let me add some print statements and see" | Skips hypothesis generation. Generates noise, not understanding. |
|
||||
| Single hypothesis presented as fact | If you are sure, write the regression test, do not run a debug skill. |
|
||||
| Mixing hypotheses with fix suggestions | The skill is diagnose-only. The fix is a TDD task on its hypothesis. |
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
The brain holds prior debug sessions across the project. Use it to skip rediscovering known failure modes.
|
||||
|
||||
**At debug start:**
|
||||
- Run `brain_query` with the error message snippet + the package name. Past sessions may have logged identical or similar failures with their resolved hypotheses.
|
||||
|
||||
**After a hypothesis is confirmed:**
|
||||
- Run `brain_write` with the failure signature → confirmed mechanism. Future debug sessions on the same area get the answer immediately.
|
||||
|
||||
**Never:**
|
||||
- Run `brain_write` for an unconfirmed hypothesis. Speculation in the brain pollutes future queries.
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||
|
||||
**At end of each phase:**
|
||||
- `session_log` with `{skill: "debug", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**Phases for this skill:** read-failure, generate-hypotheses, output
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached.
|
||||
- `fail` — the phase's intended outcome was NOT reached.
|
||||
- `skip` — phase was skipped intentionally.
|
||||
|
||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
||||
|
||||
## Mode 2 Routing Note
|
||||
|
||||
This skill produces high-volume mechanical output (hypothesis enumeration) and is a candidate for Mode 2 routing to a local model in the future. Until Plan 6 ships the routing pod, treat as Mode 1 only. The hypothesis format and discipline are identical regardless of which model generates them.
|
||||
|
||||
## Cross-References
|
||||
|
||||
- After a hypothesis is confirmed, load `tdd` skill — write a failing regression test that proves the bug exists, then fix it.
|
||||
- For test failures specifically caused by mock-vs-real divergence, also load `tdd/references/testing-anti-patterns.md`.
|
||||
- Load `code-review` skill if the diagnosis surfaces a structural issue (god object, shotgun surgery) rather than a pointwise bug.
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: discovery-framing
|
||||
description: Run the discovery / framing phase of product innovation — define and quantify a focused customer problem BEFORE any solution exists. Four movements: understand the problem space, learn about customers, identify and size problems, decide pursue/table/kill. Produces a problem brief per prioritized opportunity. Use when an idea is still vague, when someone is jumping to solutions before the problem is validated, when scoping early research, or when deciding which problems are worth pursuing. Trigger phrases include "frame the problem", "discovery", "problem statement", "is this worth building", "early research", "opportunity sizing", "we have an idea for", "how might we", "what problem are we solving".
|
||||
---
|
||||
|
||||
# Discovery / Framing
|
||||
|
||||
The phase before solutioning. Goal: **define and quantify a focused customer problem,
|
||||
tied to a specific target segment, validated with external evidence** — so that later
|
||||
investment rides on a real problem, not a pet idea. Products built on a validated problem
|
||||
have a materially higher success rate; solving an unvalidated problem builds something
|
||||
that solves nothing.
|
||||
|
||||
This is the de-identified pattern. When run for a named client, the wrapper (their phase
|
||||
names, internal tools, region structure, named teams) stays in the client boundary.
|
||||
|
||||
> Pairs with [[stage-gate-review]] (this is the Frame→Concept gate's content) and the brain
|
||||
> principle on solutions-looking-for-problems. Killing a bad problem here is the cheapest
|
||||
> kill you will ever make.
|
||||
|
||||
---
|
||||
|
||||
## The four movements
|
||||
|
||||
```
|
||||
1. Understand the problem space → 2. Learn about customers
|
||||
↓ ↓
|
||||
4. Decide: pursue / table / kill ← 3. Identify & size problems
|
||||
```
|
||||
|
||||
Typical envelope: weeks, not months; cheap. The whole point is to de-risk before spend.
|
||||
|
||||
### 1. Understand the problem space (the ecosystem)
|
||||
Map the field before judging any idea. You are not validating a solution — you are
|
||||
learning the terrain.
|
||||
- **Ecosystem analysis** — top trends, market size (the whole pie, not your slice),
|
||||
key players (end users, customers, intermediaries), competitors and their gaps.
|
||||
- **Value-exchange map** — list every player; for each, the qualitative + quantitative
|
||||
value flowing to/from them; score it. **A scalable ecosystem has roughly equal value
|
||||
per player** — lopsided value = it won't scale across the chain. This is the single
|
||||
most overlooked check.
|
||||
- **External-forces scan (PESTL + localization)** — Political, Economic, Social,
|
||||
Technological, Legal, plus local/regulatory constraints that break the business model
|
||||
in a given market.
|
||||
|
||||
### 2. Learn about customers and end-customers
|
||||
- **Personas / design targets** — behaviours, needs & values, pain points, gains. Keep
|
||||
them universal where possible (cut across segments), specific where it matters.
|
||||
- **Journey maps** — steps & touchpoints, emotional valence per step, pain points and
|
||||
opportunity areas. Separate the customer journey from the end-customer journey.
|
||||
- **Assumptions → hypotheses** — dump everything you believe true about the target
|
||||
(quantity over quality), then prioritize on a **risk × certainty grid**: attack the
|
||||
assumptions that are *high risk AND low certainty* first. Sort each into
|
||||
**Desirability** (do they want it?), **Feasibility** (can we build it?),
|
||||
**Viability** (can we make money?). Convert each prioritized assumption into a testable
|
||||
hypothesis: *"If we do X, then Y will happen"* — falsifiable, specific outcome.
|
||||
- **Validate externally** — research with real customers/end-customers (interviews,
|
||||
focus groups, surveys, diaries). Record per persona: assumption → learning →
|
||||
validated/invalidated. **External validation is non-negotiable** — a problem confirmed
|
||||
only internally is not confirmed.
|
||||
|
||||
### 3. Identify and size the problems
|
||||
- **Problem statement** — one tight form: *"[who] needs a way to [need], but [compelling
|
||||
insight from research]."* The insight cites where/how the opportunity actually manifests,
|
||||
not a guess.
|
||||
- **Total Addressable Problem (TAP)** — *not* market size. Market size is the pie-in-the-sky
|
||||
figure. TAP = (what would they pay to solve *this specific problem*) × (how many have it),
|
||||
summed across use cases. A subset of the market, honestly bounded. Sizing the problem —
|
||||
not the market — is what makes prioritization real.
|
||||
|
||||
### 4. Determine next steps
|
||||
- **Prioritize** by TAP × strategic fit.
|
||||
- **Pursue / table / kill** — every problem gets one verdict. *Kill* is a first-class
|
||||
output, not a failure; a tabled problem is parked with its reason.
|
||||
- **How-Might-We** — reframe each pursued pain point as an open question to spark
|
||||
solution ideation. **Use verbs, not nouns — never pre-pack a solution into the HMW.**
|
||||
("HMW increase awareness…" not "HMW build an app that…").
|
||||
- **Problem brief** — one per pursued opportunity (see below). This is the deliverable that
|
||||
carries into the solutioning gate.
|
||||
|
||||
---
|
||||
|
||||
## The Problem Brief (required deliverable)
|
||||
|
||||
A problem brief, not a solution brief. Fields:
|
||||
|
||||
1. **Problem** — "We believe [user/customer/market] needs a way to [need], because/but/
|
||||
surprisingly [insight]." Quantitative proof where possible.
|
||||
2. **Customer** — who has it; segment specifics; their pain points / desired gains.
|
||||
3. **End-customer** (if different) — who you're ultimately innovating for.
|
||||
4. **Why it matters** — to them and to you; what you observed that makes this a priority.
|
||||
5. **Ecosystem** — relevant signals, localization needs, competitive/comparative landscape.
|
||||
6. **Total Addressable Problem** — the bounded value of the problem.
|
||||
7. **Thought-starters / HMW** — reframed questions, no solutions.
|
||||
8. **Desired value & outcomes** — impact if the problem were solved.
|
||||
9. **Strategic benefit** — how success is measured for the business; strategy fit.
|
||||
|
||||
If a brief drifts into describing a solution, it has failed — send it back to the problem.
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Solution smuggling** — an HMW or brief that already names the answer. The discovery
|
||||
is over before it began.
|
||||
- **Internal-only validation** — "we all agree it's a problem." That is conviction, not
|
||||
evidence. See [[stage-gate-gate-killers]].
|
||||
- **Market size as problem size** — quoting a $XXbn TAM to justify a narrow bet.
|
||||
- **Skipping the kill** — every idea "has potential." A discovery phase that never kills a
|
||||
problem is theatre.
|
||||
- **Lopsided value exchange** — a solution that enriches one player and starves another;
|
||||
it will not scale no matter how good the UX.
|
||||
|
||||
---
|
||||
|
||||
## Operator judgment (hard-won)
|
||||
|
||||
### Compressing discovery by stakes
|
||||
- **High-stakes bet:** run all four movements hard — especially external customer research
|
||||
and the riskiest-assumption tests. Don't thin the validation.
|
||||
- **Low-stakes bet:** keep movement 1 (ecosystem) + movement 3 (problem statement + a rough
|
||||
size). Size with **TAM / SAM / SOM** instead of a full Total Addressable Problem build.
|
||||
- **Customer research can be skipped or AI-simulated for low stakes** — a well-framed agent,
|
||||
prompted *as* the target persona, can give a directional read fast. Treat this as a
|
||||
cheap directional signal only — it is **not** external validation and never clears a
|
||||
high-stakes desirability assumption. Use it to decide whether real research is worth doing.
|
||||
|
||||
### The bar for "externally validated"
|
||||
No fixed floor — case by case. The invariant is the *posture*, not the count: **go looking
|
||||
for disproving evidence, not reinforcing evidence.** A problem survives discovery when you
|
||||
tried to kill it with real external input and couldn't — not when N people nodded along.
|
||||
(Same disease as a faked gate — see [[stage-gate-gate-killers]].)
|
||||
|
||||
### Tell for solution smuggling
|
||||
The solution is described **crisply and confidently**, but the moment you ask *whose* problem
|
||||
and *how do you know*, the answer goes **fuzzy** — the need is vague, the validation is
|
||||
hand-wavy. Crisp solution + fuzzy problem = reverse-engineered justification. Stop and reframe.
|
||||
|
||||
### War-story patterns (de-identified)
|
||||
- **Sized big, fizzled in reality.** A problem that scored a large TAP/TAM but never
|
||||
converted to sustained real-world usage. Lesson: **problem size ≠ adoption.** A big
|
||||
addressable problem measures willingness to *care*, not willingness to *change behavior*.
|
||||
Add a "would they actually switch/use it repeatedly?" test before trusting a large size.
|
||||
- **Tabled/iceboxed a valid problem others then won.** A real problem killed or parked on
|
||||
weak conviction or bad timing, which competitors later executed well. Lesson: **the kill
|
||||
is not free.** Killing protects against sunk cost, but a wrong table is also a cost —
|
||||
record *why* you tabled and a signal that would reopen it, so a right problem killed for
|
||||
the wrong reason can come back.
|
||||
|
||||
> Named instances of both war stories are client-tagged and held out of this public skill
|
||||
> pending client-store decision. The transferable lesson lives here; the wrapper stays out.
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: dream
|
||||
description: >
|
||||
Run a "dream" — a reflective memory consolidation pass over an agent's memory
|
||||
directory. Use this skill whenever the user says "dream", "run a dream", "consolidate
|
||||
my memory files", "clean up my MEMORY.md", or asks Claude to do a memory maintenance
|
||||
pass, prune stale notes, or reorganize topic files. Also trigger when the user wants
|
||||
to rebuild a memory index, merge duplicate facts, or convert relative dates in notes
|
||||
to absolute ones. This is an agentic, multi-phase workflow — always use this skill
|
||||
rather than improvising the steps.
|
||||
---
|
||||
|
||||
# Dream — Memory Consolidation Skill
|
||||
|
||||
You are performing a **dream**: a reflective, agentic pass over a memory directory.
|
||||
Your goal is to synthesize recent signal into durable, well-organized memory so that
|
||||
future sessions can orient quickly.
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight
|
||||
|
||||
Before starting, confirm:
|
||||
1. **Where is the memory directory?** Ask the user if not obvious from context.
|
||||
Common locations: `~/memory/`, `~/.agent/memory/`, `./memory/`, a path in an env var like `$MEMORY_DIR`.
|
||||
2. **Are there transcripts or daily logs to scan?** Ask if not obvious.
|
||||
3. **Any topics to skip or treat as sensitive?**
|
||||
|
||||
Once confirmed, proceed through the four phases in order. Narrate each phase briefly as you go.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Orient
|
||||
|
||||
**Goal**: Get a map of what exists before touching anything.
|
||||
|
||||
```bash
|
||||
ls -la <memory_dir>/
|
||||
cat <memory_dir>/MEMORY.md
|
||||
```
|
||||
|
||||
For each file listed (excluding MEMORY.md):
|
||||
- Read or skim it (first 40–60 lines is usually enough unless it's small).
|
||||
- Note: topic, approximate recency, any obvious staleness or duplication.
|
||||
|
||||
Build a mental inventory:
|
||||
- Files present, rough line counts
|
||||
- Topics covered
|
||||
- Any files that look abandoned, mislabeled, or overlapping
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Gather Recent Signal
|
||||
|
||||
**Goal**: Find new facts, corrections, and drift since the last dream.
|
||||
|
||||
Check in this order:
|
||||
|
||||
1. **Daily logs** — read recent entries (last 7–14 days).
|
||||
Look for: new decisions, changed preferences, completed projects, new relationships/tools.
|
||||
|
||||
2. **Drifted facts** — scan existing topic files for statements that may now be false.
|
||||
Examples: "currently evaluating X" (did they pick one?), "planning to do Y" (done or dropped?), relative dates like "last week" or "recently".
|
||||
|
||||
3. **Transcripts** — only grep narrowly if there's a specific gap.
|
||||
Avoid bulk-reading transcripts; it's slow and noisy. Use targeted patterns:
|
||||
```bash
|
||||
grep -r "decided\|switched to\|no longer\|now using\|moved to" <transcripts_dir>/ | tail -40
|
||||
```
|
||||
|
||||
Collect a list of **updates to make**: new facts, corrections, removals.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Consolidate
|
||||
|
||||
**Goal**: Apply the updates. Leave memory files cleaner and more accurate than you found them.
|
||||
|
||||
For each topic file:
|
||||
|
||||
- **Merge duplicates**: if the same fact appears in two files, keep it in the more specific one and remove from the general one.
|
||||
- **Convert relative dates**: replace "last week", "recently", "a few months ago" with an absolute date (use the current date as reference; estimate if necessary and note the uncertainty).
|
||||
- **Delete contradicted facts**: if a new fact supersedes an old one, remove the old one outright — don't leave both.
|
||||
- **Tighten language**: convert vague hedges ("probably uses", "might be") to definite statements where the evidence supports it, or remove if genuinely unknown.
|
||||
- **Add new facts** from Phase 2 to the appropriate topic file. Create a new topic file if no good home exists.
|
||||
|
||||
After editing files, do a final pass:
|
||||
```bash
|
||||
grep -n "last week\|recently\|a few months\|soon\|currently planning" <memory_dir>/*.md
|
||||
```
|
||||
Clean up any remaining relative time references.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Prune and Index
|
||||
|
||||
**Goal**: Rebuild MEMORY.md as a clean, navigable index under 200 lines.
|
||||
|
||||
**MEMORY.md structure**:
|
||||
```markdown
|
||||
# Memory Index
|
||||
_Last updated: YYYY-MM-DD_
|
||||
|
||||
## Overview
|
||||
One short paragraph: who this agent is, primary context, most important standing facts.
|
||||
|
||||
## Topic Files
|
||||
| File | Contents | Last updated |
|
||||
|------|----------|-------------|
|
||||
| person.md | Identity, preferences, background | YYYY-MM-DD |
|
||||
| projects.md | Active and recent projects | YYYY-MM-DD |
|
||||
| tools.md | Stack, infra, dev environment | YYYY-MM-DD |
|
||||
| ... | ... | ... |
|
||||
|
||||
## Quick Facts
|
||||
- Bullet list of the 10–15 most frequently-needed facts (role, location, key tools, etc.)
|
||||
|
||||
## Recent Changes
|
||||
- Bullet list of what changed in this dream (so the next session knows what's fresh)
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Remove any pointers to files that no longer exist.
|
||||
- Add pointers for any new files created in Phase 3.
|
||||
- Keep Quick Facts ≤ 15 bullets — this is for speed, not completeness.
|
||||
- Recent Changes section replaces itself each dream (don't accumulate).
|
||||
- Total MEMORY.md length: **200 lines max**.
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
After completing all four phases, return a **dream summary** to the user:
|
||||
|
||||
```
|
||||
## Dream complete — YYYY-MM-DD
|
||||
|
||||
### What changed
|
||||
- [file]: [what was updated]
|
||||
- MEMORY.md: rebuilt index, N topic files indexed
|
||||
|
||||
### Facts added
|
||||
- ...
|
||||
|
||||
### Facts removed / corrected
|
||||
- ...
|
||||
|
||||
### Files created
|
||||
- ...
|
||||
|
||||
### Files deleted or merged
|
||||
- ...
|
||||
|
||||
### Still uncertain / needs follow-up
|
||||
- ...
|
||||
```
|
||||
|
||||
Keep it concise — a few bullets per section, not exhaustive diffs. The goal is for the user to quickly confirm the dream went well and catch any mistakes.
|
||||
|
||||
---
|
||||
|
||||
## Tips and Edge Cases
|
||||
|
||||
- **No MEMORY.md exists yet**: create one from scratch using the structure above. Treat all existing files as "first time indexed".
|
||||
- **Memory dir is empty**: create MEMORY.md and a starter `scratch.md` noting the date and that the memory system is new.
|
||||
- **Conflicting facts with no clear resolution**: note both in the file with a date stamp and flag in "Still uncertain" section of the summary.
|
||||
- **Large transcript dumps**: resist reading them in full. Grep is your friend. If you must read, read the last N lines only.
|
||||
- **Files with sensitive content**: if the user flagged topics to skip, skip them entirely — don't even open them.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: experiment-spec
|
||||
description: Write a rigorous experiment spec for a research phase before any code or training runs. Use instead of feature-spec for scientific/ML research projects. Enforces falsifiable hypothesis, quantitative acceptance criteria, baseline comparison, and null-result protocol.
|
||||
---
|
||||
|
||||
# Experiment Spec
|
||||
|
||||
## Overview
|
||||
|
||||
An experiment spec is the scientific contract for one research phase or experiment, written before any implementation or training begins. It is the research analogue of `feature-spec` — same discipline, different vocabulary.
|
||||
|
||||
**Core principle:** If you cannot write a falsifiable hypothesis with a quantitative acceptance criterion, you do not understand the experiment well enough to run it.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Starting a new research phase (Phase 0, Phase 1, etc.)
|
||||
- Running any experiment that will produce metrics used to make a go/no-go decision
|
||||
- Any time the question is "does X work?" rather than "build X"
|
||||
- Before touching training code, data, or hyperparameters for a new question
|
||||
|
||||
**When NOT to use:**
|
||||
- Implementing a specific component whose behaviour is already defined by a phase spec (use `feature-spec` instead)
|
||||
- Exploratory data analysis with no hypothesis (use a notebook; note it as EDA)
|
||||
- Bug fixes or refactors
|
||||
|
||||
## Iron Laws
|
||||
|
||||
1. **The hypothesis must be falsifiable.** "JEPA is promising" is not a hypothesis. "JEPA embeddings will achieve silhouette > 0.35 on held-out data" is. If you cannot state conditions under which the hypothesis is false, it is not a hypothesis.
|
||||
2. **Acceptance criteria must be quantitative and pre-registered.** Write the number before you run the experiment. Moving the goalposts after seeing results is p-hacking.
|
||||
3. **A baseline is mandatory.** Every experiment must compare against at least one simpler baseline. "Better than nothing" is not a baseline.
|
||||
4. **A null-result protocol is mandatory.** State what you will conclude and do if the hypothesis is rejected. "Try harder" is not a protocol.
|
||||
5. **The training cutoff is sacred.** No post-cutoff data informs any decision in the spec or implementation.
|
||||
|
||||
## Spec Template
|
||||
|
||||
```markdown
|
||||
# Experiment Spec: [Phase N — Short Name]
|
||||
|
||||
## Hypothesis
|
||||
|
||||
> "[Falsifiable claim]: We believe [X] will produce [Y], measurable by [Z]."
|
||||
|
||||
State conditions under which this hypothesis is FALSE.
|
||||
|
||||
## Background
|
||||
|
||||
Why this experiment? What does it build on? What prior result or decision motivates it?
|
||||
(2–4 sentences. Reference DECISIONS.md or brain wing entries where relevant.)
|
||||
|
||||
## Design
|
||||
|
||||
### Data
|
||||
- Source, date range, pairs/assets, features used
|
||||
- Train / validation / test split (respect training cutoff)
|
||||
|
||||
### Model / method
|
||||
- Architecture, configuration, key hyperparameters
|
||||
- What is being varied vs. held fixed
|
||||
|
||||
### Baseline
|
||||
- What simpler method is being compared against?
|
||||
- Why is this the right baseline?
|
||||
|
||||
### Ablations (if any)
|
||||
- What variants will be run to isolate the effect being studied?
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] [Primary criterion — quantitative threshold on primary metric]
|
||||
- [ ] [Baseline comparison — e.g. "exceeds baseline by >X%"]
|
||||
- [ ] [Reproducibility — reruns within ±Y% of reported metric]
|
||||
- [ ] [Collapse/sanity check — e.g. "PC1/rolling-HV correlation < 0.85"]
|
||||
|
||||
## Out of Scope
|
||||
|
||||
What this experiment explicitly does NOT answer, even if related.
|
||||
Anything plausibly in scope that is deferred goes here.
|
||||
|
||||
## Null Result Protocol
|
||||
|
||||
If the primary acceptance criterion is NOT met:
|
||||
- What do we conclude?
|
||||
- What is the next step? (Investigate X, pivot to Y, terminate programme)
|
||||
- What gets written to the brain and results/summaries/?
|
||||
|
||||
## Risks
|
||||
|
||||
What could go wrong, and how would it be detected?
|
||||
At least one risk must be listed.
|
||||
```
|
||||
|
||||
## Worked Example
|
||||
|
||||
```markdown
|
||||
# Experiment Spec: Phase 0 — SSL Feasibility Gate
|
||||
|
||||
## Hypothesis
|
||||
|
||||
> We believe that a masked autoencoder (MAE) trained on FX hourly data will produce
|
||||
> latent embeddings that show structural separability by volatility regime without
|
||||
> explicit regime labels, measurable by silhouette score > 0.20 on held-out 2023 data.
|
||||
|
||||
This hypothesis is FALSE if silhouette score ≤ 0.20 on the held-out evaluation.
|
||||
|
||||
## Background
|
||||
|
||||
Before investing in JEPA-specific machinery, we need to confirm that SSL-based
|
||||
representation learning can find regime structure in FX time-series at all. MAE is
|
||||
the simplest SSL baseline — if it cannot find structure, JEPA will not either.
|
||||
Added post Full Grill (2026-05-27). See DECISIONS.md: "Phase 0: SSL feasibility gate".
|
||||
|
||||
## Design
|
||||
|
||||
### Data
|
||||
- Source: DUKASCopy, EUR/USD hourly, 2008–2022 (train), 2023 (held-out test)
|
||||
- Features: log-return, rolling 20-period HV, VIX (daily interpolated to hourly)
|
||||
- Regime label (for evaluation only, not training): rolling 30-day HV percentile,
|
||||
binary high/low threshold at 50th percentile
|
||||
|
||||
### Model
|
||||
- Masked Autoencoder: 1D temporal masking (mask contiguous 24h window)
|
||||
- Encoder: 3-layer 1D CNN + positional encoding
|
||||
- Decoder: 2-layer MLP reconstructing masked segment
|
||||
- Context window: 120 hours (5 days)
|
||||
|
||||
### Baseline
|
||||
- PCA on raw feature vectors (same window) — tests whether any dimensionality
|
||||
reduction shows regime structure, not just SSL
|
||||
|
||||
### Ablations
|
||||
- Masking horizon: K ∈ {8h, 24h, 72h} — does horizon affect embedding quality?
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Silhouette score > 0.20 on held-out 2023 data (k-means, k=3, vs. HV regime label)
|
||||
- [ ] MAE silhouette exceeds PCA baseline silhouette
|
||||
- [ ] Rerun within ±10% of reported silhouette
|
||||
- [ ] PC1 / rolling-HV correlation < 0.95 (not purely encoding volatility level)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- JEPA implementation (Phase 1)
|
||||
- Multi-pair training (Phase 1+)
|
||||
- VaR or ES computation
|
||||
- Any use of post-2023 data
|
||||
|
||||
## Null Result Protocol
|
||||
|
||||
If silhouette ≤ 0.20:
|
||||
- Conclude: SSL cannot reliably find regime structure in EUR/USD hourly data with
|
||||
these features at this resolution
|
||||
- Next step: investigate whether (a) hourly resolution is too noisy (try daily),
|
||||
(b) 3 features are insufficient, or (c) regime label definition is too coarse
|
||||
- Record result in results/summaries/phase-0-null.md and brain wing jepa-fx/failures/
|
||||
|
||||
## Risks
|
||||
|
||||
- Encoder collapses to near-constant output: detect via reconstruction loss plateau
|
||||
in first 10 epochs; mitigation: add batch norm, reduce learning rate
|
||||
- Regime label too coarse (binary HV): silhouette may be low even with good structure;
|
||||
mitigation: also evaluate with 4-class label (HV quartiles)
|
||||
```
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
| Failure mode | What it looks like | Fix |
|
||||
|---|---|---|
|
||||
| Non-falsifiable hypothesis | "JEPA shows promise" | Rewrite with a number |
|
||||
| Post-hoc criteria | Threshold chosen after seeing results | Write the number first, commit the spec |
|
||||
| No baseline | Silhouette of 0.30 sounds good until PCA achieves 0.35 | Always include a dumber method |
|
||||
| Missing null protocol | "We'll figure it out if it fails" | Write it now — it forces clarity about what you're actually betting on |
|
||||
| Cutoff violation | Architecture choice informed by 2024 data patterns | Never open the test set during development |
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
**At spec start:**
|
||||
- `brain_query wing=jepa-fx hall=decisions` — load current architectural decisions
|
||||
- `brain_query wing=jepa-fx hall=failures` — load known failure modes; address them in Risks section
|
||||
|
||||
**After spec is approved:**
|
||||
- `brain_write` to `jepa-fx/hypotheses/` with the hypothesis and acceptance criteria
|
||||
|
||||
**After experiment concludes:**
|
||||
- `brain_write` to `jepa-fx/failures/` with any new failure modes discovered
|
||||
- `session_log` with outcome
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Use `feature-spec` for implementing a specific component within an already-specced phase
|
||||
- Use `grill-me` on the spec before running the experiment if the hypothesis feels shaky
|
||||
- Use `tdd` once the spec is approved — each acceptance criterion maps to a test
|
||||
- Use `session-retrospective` after the experiment concludes
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
name: feature-spec
|
||||
description: Write a tight implementation-level spec for a single feature or component before coding it. Use after a project-level spec exists (see spec-driven-dev) and you need to nail down one feature inside it.
|
||||
---
|
||||
|
||||
# Feature Spec
|
||||
|
||||
## Overview
|
||||
|
||||
A feature spec is the implementation-level contract for one feature or component, written by an engineer before code. It is shorter and more focused than a project-level spec (`spec-driven-dev`) — it assumes the project context, the stack, and the user are already known. Its job is to force one decision: **what does done look like, and what is explicitly not in this feature.**
|
||||
|
||||
**Core principle:** If you cannot write three measurable success criteria, you do not understand the feature well enough to build it.
|
||||
|
||||
## When to Use
|
||||
|
||||
- About to start implementing a feature inside an existing, already-specced project
|
||||
- A change that touches more than one file or one package
|
||||
- A change that introduces a new public API, schema, or external contract
|
||||
- Any time the requirements feel "obvious" — that feeling usually means assumptions are stacking silently
|
||||
|
||||
**When NOT to use:**
|
||||
- Project kickoff or first-time architecture decisions — use `spec-driven-dev` instead, which covers the broader lifecycle (Objective, Tech Stack, Project Structure, Boundaries, etc.)
|
||||
- Single-line fixes, typos, dependency bumps
|
||||
- Spike or throwaway prototypes (note them as such; no spec needed)
|
||||
|
||||
## Iron Laws
|
||||
|
||||
1. **Success criteria must be measurable.** "The system is fast" is banned. "p99 < 200ms under 100 RPS" is valid. If you cannot state a measurement, the criterion is not a criterion.
|
||||
2. **An "Out of scope" section is mandatory.** If you do not draw the boundary, the implementer (you, future-you, or another agent) will guess wrong.
|
||||
3. **Every technical decision in "Technical approach" must have a rationale.** A decision is any choice where a reasonable alternative existed and was not taken. "Use Postgres" is not a decision; "use Postgres because the rest of the stack uses it and we need transactions across user + invoice" is.
|
||||
|
||||
## Spec Template
|
||||
|
||||
```markdown
|
||||
# Spec: [Feature Name]
|
||||
|
||||
## Problem Statement
|
||||
|
||||
What problem does this feature solve? For whom? Why now?
|
||||
(2-4 sentences. If you cannot answer "why now," ask before writing the rest.)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Criterion 1 — measurable and verifiable
|
||||
- [ ] Criterion 2 — measurable and verifiable
|
||||
- [ ] Criterion 3 — measurable and verifiable
|
||||
|
||||
## Constraints
|
||||
|
||||
Non-negotiable requirements the solution must satisfy.
|
||||
Examples: "must not change the public API", "must complete in < 500ms",
|
||||
"must not require a database migration", "must work offline".
|
||||
|
||||
## Out of Scope
|
||||
|
||||
What this feature explicitly does NOT do, even if related.
|
||||
Anything plausibly in scope that you decided to defer goes here.
|
||||
|
||||
## Technical Approach
|
||||
|
||||
The architecture or design decisions, with rationale for each.
|
||||
Each bullet should answer "why this choice over the obvious alternative."
|
||||
|
||||
## Risks
|
||||
|
||||
What could go wrong, and how it would be mitigated or detected.
|
||||
At least one risk must be listed; "no risks" usually means insufficient analysis.
|
||||
```
|
||||
|
||||
## Worked Example
|
||||
|
||||
```markdown
|
||||
# Spec: IBAN extraction from Swedish PDF invoices
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Invoice processors at small accounting firms manually copy IBANs from PDF
|
||||
invoices into the banking system. The current parser handles ~60% of formats;
|
||||
40% require manual fallback, defeating the automation premise.
|
||||
|
||||
Why now: the pilot customer is onboarding three new firms next month, all of
|
||||
which use formats currently in the 40%.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] IBAN extracted correctly from ≥ 95% of a 200-invoice test corpus
|
||||
- [ ] Zero false positives (extracting a non-IBAN string as an IBAN) on the corpus
|
||||
- [ ] p99 extraction time < 500ms per invoice
|
||||
- [ ] Returns a structured error (not a panic) for unrecognized formats
|
||||
|
||||
## Constraints
|
||||
|
||||
- Must not require a new dependency (PDF library is already chosen)
|
||||
- Must work without network access (no external IBAN validation API)
|
||||
- Must be callable from the existing `Parser.Extract(ctx, r)` signature
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Non-Swedish IBAN formats (separate spec when needed)
|
||||
- BIC/SWIFT extraction (handled by a separate parser)
|
||||
- OCR — input is assumed to be text-extractable PDF, not scanned image
|
||||
- Validating that the IBAN refers to a real account
|
||||
|
||||
## Technical Approach
|
||||
|
||||
- Layered regex-then-checksum approach: regex narrows candidates, MOD-97
|
||||
checksum validates. Rationale: pure regex is too permissive; pure
|
||||
checksum requires too many candidates to test.
|
||||
- Use the existing `pdfplumber` text layer rather than re-extracting.
|
||||
Rationale: extraction is already the slow path; reusing the layer
|
||||
avoids doubling extraction time.
|
||||
- Return `(iban string, ok bool, err error)` not `(*IBAN, error)`.
|
||||
Rationale: the caller almost always wants to know "did we find one"
|
||||
separately from "did extraction itself fail," and a pointer return
|
||||
invites nil-deref bugs.
|
||||
|
||||
## Risks
|
||||
|
||||
- Regex tuned to current corpus may overfit; mitigation: corpus is held out
|
||||
per-firm, so we will see degradation at the next firm onboarding before
|
||||
it reaches production.
|
||||
- MOD-97 implementation bugs are subtle; mitigation: cross-check against
|
||||
the Wikipedia reference implementation in tests.
|
||||
```
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "It's a small feature, I don't need a spec" | Small features still need a measurable definition of done. The spec stays small too. |
|
||||
| "I'll write the spec after, when I know more" | Then it is documentation, not a spec. The point is forcing clarity *before* the code locks in your assumptions. |
|
||||
| "The success criteria are obvious" | If they are obvious, they are easy to write down. If they are hard to write down, they are not obvious. |
|
||||
| "I don't have time to enumerate out-of-scope items" | You will spend more time arguing over scope creep at review than writing this section now. |
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
The brain holds prior feature specs across the project. Use it to keep constraints and out-of-scope decisions coherent across related features.
|
||||
|
||||
**At spec start:**
|
||||
- Run `brain_query` for prior specs in the same area or for the same problem domain. Reusing the same constraints and out-of-scope decisions across related features keeps the system coherent.
|
||||
|
||||
**After spec is approved:**
|
||||
- Run `brain_write` with the spec's success criteria and out-of-scope list. Future related specs can cross-reference rather than re-derive.
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||
|
||||
**At end of each phase:**
|
||||
- `session_log` with `{skill: "feature-spec", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**Phases for this skill:** draft
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached (spec drafted and approved).
|
||||
- `fail` — the phase's intended outcome was NOT reached (spec rejected, blocked on unresolved questions).
|
||||
- `skip` — phase was skipped intentionally.
|
||||
|
||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
||||
|
||||
## Mode 2 Routing Note
|
||||
|
||||
Spec writing requires interpretation of intent and tradeoff reasoning — work where Claude generally outperforms a 9-13B local model. This skill is intentionally **not** a Mode 2 routing candidate. In Mode 2, the routing layer should leave `feature-spec` calls on Claude.
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Load `spec-driven-dev` instead if this is the first spec for a project (broader template, includes tech stack, project structure, boundaries).
|
||||
- Load `problem-analysis` skill before this if the problem statement itself feels fuzzy.
|
||||
- Load `tdd` skill once the spec is approved — each success criterion becomes a test.
|
||||
- Load `planning` skill if the spec implies more than ~5 implementation tasks.
|
||||
@@ -0,0 +1,487 @@
|
||||
---
|
||||
name: gitea-ci
|
||||
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, buildah image build, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
|
||||
---
|
||||
|
||||
# Gitea CI/CD Pipeline Setup
|
||||
|
||||
## Overview
|
||||
|
||||
This skill sets up a complete CI/CD pipeline on Gitea with a self-hosted runner. It covers the full journey from a bare repo to a working pipeline with: lint/test gate, OCI image build, k3s deploy, and GitHub mirror. It also captures non-obvious act_runner quirks that will burn you if you don't know them.
|
||||
|
||||
Load this skill when starting a new project that needs CI, or when debugging a broken Gitea Actions pipeline.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Setting up CI for a new project on this infra (Gitea + koala/iguana/flamingo)
|
||||
- Debugging a Gitea Actions pipeline that silently fails or behaves unexpectedly
|
||||
- Migrating an existing project to Gitea Actions from another CI system
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Gather Context
|
||||
|
||||
Before writing any YAML, collect the following. Use AskUserQuestion for anything not obvious from the project:
|
||||
|
||||
| Item | Where to find it | Why it matters |
|
||||
|------|-----------------|----------------|
|
||||
| Gitea repo URL | git remote -v | Base URL for API calls |
|
||||
| Runner machine | CLAUDE.md / infra notes | Which machine runs jobs |
|
||||
| Primary language | go.mod, package.json, etc. | Drives the toolchain steps |
|
||||
| Container registry | Ask or check existing config | `localhost:5000` on koala, or external |
|
||||
| k3s namespace | k8s/ manifests or ask | Deploy target |
|
||||
| GitHub mirror repo | Ask | Target for the mirror job |
|
||||
| Image name | Ask or derive from repo name | Used in buildah + k3s steps |
|
||||
|
||||
Check the project's `CLAUDE.md` for infra notes first — often this answers most questions without needing to ask.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Verify Runner Is Live
|
||||
|
||||
Before writing the workflow, confirm the runner is registered and healthy.
|
||||
|
||||
```bash
|
||||
# Check registered runners via Gitea API
|
||||
curl -s "https://<gitea-host>/api/v1/repos/<owner>/<repo>/actions/runners" \
|
||||
-H "Authorization: token $GITEA_TOKEN" | python3 -m json.tool
|
||||
```
|
||||
|
||||
If no runners appear, the runner is not registered. On this infra, `koala` runs `act_runner`. Check with:
|
||||
```bash
|
||||
ssh koala "systemctl status act_runner"
|
||||
# or if running as a process:
|
||||
ssh koala "pgrep -a act_runner"
|
||||
```
|
||||
|
||||
act_runner config is typically at `~/.config/act_runner/` or wherever it was installed. Registration command:
|
||||
```bash
|
||||
act_runner register \
|
||||
--instance https://<gitea-host> \
|
||||
--token <runner-registration-token> \
|
||||
--name koala \
|
||||
--labels self-hosted
|
||||
```
|
||||
|
||||
The registration token is in Gitea: Settings → Actions → Runners → Create Runner.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Write the Workflow
|
||||
|
||||
The standard pipeline has 4-5 jobs. Copy the template below and fill in the `env.IMAGE` value and any project-specific toolchain steps.
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
IMAGE: <project-name> # ← change this
|
||||
|
||||
jobs:
|
||||
# ── 1. Quality gate ─────────────────────────────────────────────────────────
|
||||
check:
|
||||
name: Lint / Test / Vet
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false # self-hosted: Go cache persists on disk between runs
|
||||
|
||||
- name: Verify toolchain
|
||||
run: |
|
||||
go version
|
||||
task --version
|
||||
govulncheck -version 2>&1 || true
|
||||
|
||||
- name: Install golangci-lint
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh \
|
||||
| sh -s -- -b "$(go env GOPATH)/bin" v2.11.4
|
||||
golangci-lint --version
|
||||
|
||||
- name: Run checks
|
||||
run: task check
|
||||
|
||||
# ── 2. Build image ──────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: Build & Import
|
||||
needs: check
|
||||
runs-on: self-hosted
|
||||
if: github.event_name != 'pull_request'
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.sha-tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Derive image tags
|
||||
id: meta
|
||||
run: |
|
||||
SHA=$(git rev-parse --short HEAD)
|
||||
echo "sha-tag=${SHA}" >> "$GITHUB_OUTPUT"
|
||||
REF="${{ github.ref }}"
|
||||
if [[ "$REF" == refs/tags/v* ]]; then
|
||||
echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build and push to local registry
|
||||
run: |
|
||||
REGISTRY="localhost:5000"
|
||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||||
buildah build \
|
||||
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||||
--label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \
|
||||
-t ${REF} \
|
||||
-t ${REGISTRY}/${{ env.IMAGE }}:latest \
|
||||
.
|
||||
buildah push --tls-verify=false ${REF}
|
||||
buildah push --tls-verify=false ${REGISTRY}/${{ env.IMAGE }}:latest
|
||||
[[ -n "${{ steps.meta.outputs.version-tag }}" ]] && \
|
||||
buildah push --tls-verify=false \
|
||||
${REF} \
|
||||
${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.version-tag }} || true
|
||||
echo "✓ Image pushed to ${REF}"
|
||||
|
||||
- name: Smoke test
|
||||
run: |
|
||||
REGISTRY="localhost:5000"
|
||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||||
CNAME="smoke-${{ steps.meta.outputs.sha-tag }}"
|
||||
sudo k3s ctr images pull --plain-http ${REF}
|
||||
OUTPUT=$(timeout 5 sudo k3s ctr run --rm ${REF} ${CNAME} /<binary-name> 2>&1 || true)
|
||||
sudo k3s ctr containers delete ${CNAME} 2>/dev/null || true
|
||||
echo "$OUTPUT" | grep -q "<project-name>" \
|
||||
&& echo "✓ Smoke test passed" \
|
||||
|| echo "⚠ Smoke test inconclusive: $OUTPUT"
|
||||
|
||||
# ── 3. Deploy to k3s ────────────────────────────────────────────────────────
|
||||
deploy:
|
||||
name: Deploy to staging (k3s)
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Apply manifests and update image
|
||||
env:
|
||||
IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
|
||||
run: |
|
||||
kubectl apply -f k8s/namespace.yaml
|
||||
kubectl apply -f k8s/deployment.yaml
|
||||
kubectl set image deployment/<project-name> \
|
||||
<project-name>=localhost:5000/${{ env.IMAGE }}:${IMAGE_TAG} \
|
||||
--namespace <project-name>
|
||||
|
||||
- name: Verify rollout
|
||||
run: |
|
||||
kubectl rollout status deployment/<project-name> \
|
||||
--namespace <project-name> \
|
||||
--timeout=120s \
|
||||
|| {
|
||||
echo "── pod status ──"
|
||||
kubectl get pods -n <project-name> -o wide
|
||||
echo "── pod events ──"
|
||||
kubectl get events -n <project-name> --sort-by='.lastTimestamp' | tail -20
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── 4. Mirror to GitHub ─────────────────────────────────────────────────────
|
||||
mirror:
|
||||
name: Mirror to GitHub
|
||||
needs: deploy
|
||||
runs-on: self-hosted
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Push to GitHub
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo '${{ secrets.GH_DEPLOY_KEY }}' > ~/.ssh/id_rsa_gh_mirror
|
||||
chmod 600 ~/.ssh/id_rsa_gh_mirror
|
||||
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa_gh_mirror -o IdentitiesOnly=yes" \
|
||||
git push git@github.com:<gh-org>/<repo>.git HEAD:main
|
||||
rm ~/.ssh/id_rsa_gh_mirror
|
||||
echo "✓ Mirrored to GitHub"
|
||||
```
|
||||
|
||||
Replace all `<placeholders>` before committing.
|
||||
|
||||
---
|
||||
|
||||
## Trunk-Based Development and CI
|
||||
|
||||
This infra uses TBD. CI on every push to main is the quality gate — not branch protection.
|
||||
|
||||
### What this means in practice
|
||||
|
||||
- Commit directly to main for all solo and single-agent work
|
||||
- Each commit: one logical change + `task check` passing locally before push
|
||||
- CI runs `task check` again on push — if it was green locally it will be green in CI
|
||||
- No PRs, no feature branches, no squash-merges for normal work
|
||||
|
||||
### The pre-push ritual
|
||||
|
||||
```bash
|
||||
# In project root before every push:
|
||||
task check && git push origin main
|
||||
```
|
||||
|
||||
Wire this into your shell or a git pre-push hook so it's automatic.
|
||||
|
||||
### When CI catches something you missed locally
|
||||
|
||||
Fix forward — commit the fix directly to main. Do not revert unless the broken
|
||||
commit introduced a data migration or irreversible infrastructure change.
|
||||
|
||||
### Feature flags instead of feature branches
|
||||
|
||||
If a feature is too large for one session, hide it behind a build tag or a config
|
||||
flag until ready:
|
||||
|
||||
```go
|
||||
// cmd/server/main.go
|
||||
if os.Getenv("ENABLE_EXPERIMENTAL_FEATURE") == "true" {
|
||||
server.RegisterExperimentalRoutes(mux)
|
||||
}
|
||||
```
|
||||
|
||||
Commit the hidden feature incrementally. Activate it when complete.
|
||||
|
||||
### When to break the rule
|
||||
|
||||
- **Parallel agents on the same repo:** short-lived `agent/<description>` branch,
|
||||
merge to main inside the same session.
|
||||
- **External contributor or client four-eyes requirement:** PR flow, document the
|
||||
reason in `PROJECT.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Set Up GitHub Mirror
|
||||
|
||||
The mirror job needs an SSH deploy key with write access to the GitHub repo.
|
||||
|
||||
```bash
|
||||
# On your local machine:
|
||||
ssh-keygen -t ed25519 -C "gitea-mirror-<project>" -f /tmp/gh_mirror_key -N ""
|
||||
cat /tmp/gh_mirror_key.pub # add this to GitHub repo → Settings → Deploy Keys (Allow write)
|
||||
cat /tmp/gh_mirror_key # add this to Gitea repo → Settings → Secrets → GH_DEPLOY_KEY
|
||||
rm /tmp/gh_mirror_key /tmp/gh_mirror_key.pub
|
||||
```
|
||||
|
||||
The private key goes in Gitea as the `GH_DEPLOY_KEY` secret. The public key goes in GitHub as a deploy key with write access.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Add Project Secrets
|
||||
|
||||
Set secrets via the Gitea API — faster than the UI and scriptable:
|
||||
|
||||
```bash
|
||||
# Template:
|
||||
curl -s -X PUT \
|
||||
"https://<gitea-host>/api/v1/repos/<owner>/<repo>/actions/secrets/<SECRET_NAME>" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"data":"<secret-value>"}' \
|
||||
-w "\nHTTP %{http_code}"
|
||||
# Expected: HTTP 204
|
||||
|
||||
# Verify secrets are set:
|
||||
curl -s "https://<gitea-host>/api/v1/repos/<owner>/<repo>/actions/secrets" \
|
||||
-H "Authorization: token $GITEA_TOKEN" | python3 -m json.tool
|
||||
```
|
||||
|
||||
Required secrets for the baseline pipeline: `GH_DEPLOY_KEY`.
|
||||
|
||||
Any project-specific secrets (API keys, tokens, credentials) are added the same way.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Debug Failing Runs
|
||||
|
||||
When a run fails, use the Gitea API to get logs without opening a browser.
|
||||
|
||||
```bash
|
||||
GITEA_HOST="https://<gitea-host>"
|
||||
REPO="<owner>/<repo>"
|
||||
TOKEN="$GITEA_TOKEN"
|
||||
|
||||
# List recent runs
|
||||
curl -s "$GITEA_HOST/api/v1/repos/$REPO/actions/runs?limit=5" \
|
||||
-H "Authorization: token $TOKEN" | python3 -c "
|
||||
import json,sys
|
||||
for r in json.load(sys.stdin)['workflow_runs']:
|
||||
print(f'#{r[\"id\"]} {r[\"status\"]:12} {r.get(\"conclusion\",\"\"):10} {r[\"display_title\"][:50]}')
|
||||
"
|
||||
|
||||
# List jobs for a run (replace RUN_ID)
|
||||
curl -s "$GITEA_HOST/api/v1/repos/$REPO/actions/runs/<RUN_ID>/jobs" \
|
||||
-H "Authorization: token $TOKEN" | python3 -c "
|
||||
import json,sys
|
||||
for j in json.load(sys.stdin)['jobs']:
|
||||
print(f'Job {j[\"id\"]}: {j[\"name\"]:30} | {j[\"status\"]:10} | {j.get(\"conclusion\",\"\")}')
|
||||
"
|
||||
|
||||
# Get full log for a job (replace JOB_ID)
|
||||
curl -s "$GITEA_HOST/api/v1/repos/$REPO/actions/jobs/<JOB_ID>/logs" \
|
||||
-H "Authorization: token $TOKEN" | grep -E "ERROR|error|FAIL|undefined|cannot" | tail -30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠ act_runner Gotchas
|
||||
|
||||
These are non-obvious bugs and sharp edges that will silently break your pipeline. Read all of them.
|
||||
|
||||
### 1. Steps with `env:` blocks that use secret expressions are silently skipped
|
||||
|
||||
**Symptom**: A step produces no log output at all — not even its step header (`⭐ Run Main ...`). Subsequent steps fail as if the skipped step never ran.
|
||||
|
||||
**Cause**: In act_runner v0.4.x, steps that have an `env:` block containing `${{ secrets.* }}` expressions are silently dropped before execution.
|
||||
|
||||
**Wrong**:
|
||||
```yaml
|
||||
- name: Write config
|
||||
env:
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
run: |
|
||||
echo "KEY=$API_KEY" > config.env
|
||||
```
|
||||
|
||||
**Right** — inline the expression directly in the `run:` script:
|
||||
```yaml
|
||||
- name: Write config
|
||||
run: |
|
||||
echo 'KEY=${{ secrets.API_KEY }}' > config.env
|
||||
```
|
||||
|
||||
act_runner evaluates `${{ }}` expressions before passing the script to bash. The values are redacted in log output. No `env:` block needed.
|
||||
|
||||
### 2. Heredocs in `run: |` blocks — the indented EOF trap
|
||||
|
||||
**Symptom**: A file is written with 10-100× more content than expected. JSON files fail to parse. The step appears to succeed.
|
||||
|
||||
**Cause**: In a YAML literal block scalar (`run: |`), all content is at the block's indentation level. If you indent your heredoc content (to look neat), the `EOF` terminator also gets indented. Bash requires the terminator to be at column 0 — an indented `EOF` is NOT recognized, so the heredoc consumes everything that follows until end-of-script.
|
||||
|
||||
**Wrong**:
|
||||
```yaml
|
||||
- name: Write file
|
||||
run: |
|
||||
cat > output.json <<EOF
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
EOF ← indented, NOT recognized as terminator
|
||||
chmod 600 output.json
|
||||
```
|
||||
|
||||
The file ends up containing the JSON, the literal string ` EOF`, and ` chmod 600 output.json`.
|
||||
|
||||
**Right** — avoid heredocs entirely. Use `echo` for simple lines, `printf` for single-line formatted output:
|
||||
```yaml
|
||||
- name: Write file
|
||||
run: |
|
||||
echo '{"key":"${{ secrets.SOME_VALUE }}"}' > output.json
|
||||
chmod 600 output.json
|
||||
```
|
||||
|
||||
For JSON with dynamic values, use python3 as a one-liner (but see gotcha #3):
|
||||
```yaml
|
||||
- name: Write token file
|
||||
run: |
|
||||
python3 -c "import json; open('token.json','w').write(json.dumps({'key':'${{ secrets.TOKEN }}','type':'bearer'}))"
|
||||
```
|
||||
|
||||
### 3. Multiline `python3 -c` breaks YAML block scalar parsing
|
||||
|
||||
**Symptom**: The workflow file is pushed but no CI run appears — Gitea silently rejects the workflow YAML.
|
||||
|
||||
**Cause**: Unindented code in a multiline `python3 -c "..."` block (with line breaks inside the YAML `run: |`) is at column 0, which the YAML parser interprets as ending the block scalar and starting a new YAML key.
|
||||
|
||||
**Wrong**:
|
||||
```yaml
|
||||
run: |
|
||||
python3 -c "
|
||||
import json ← column 0, YAML parser thinks block scalar ended
|
||||
d = {...}
|
||||
"
|
||||
```
|
||||
|
||||
**Right** — keep the python3 invocation on one line:
|
||||
```yaml
|
||||
run: |
|
||||
python3 -c "import json; d = {...}; open('f','w').write(json.dumps(d))"
|
||||
```
|
||||
|
||||
If the one-liner is too long, write a small `.py` file in a previous step and execute it.
|
||||
|
||||
### 4. Validate YAML locally before pushing
|
||||
|
||||
Gitea silently ignores workflows with YAML parse errors — no run appears, no error message.
|
||||
|
||||
```bash
|
||||
python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/ci.yml').read()); print('YAML valid')"
|
||||
```
|
||||
|
||||
Run this before every push that touches the workflow file.
|
||||
|
||||
### 5. Commit generated files instead of gitignoring them
|
||||
|
||||
**Symptom**: Lint/build fails in CI with `undefined: SomeGeneratedType` even though it works locally.
|
||||
|
||||
**Cause**: Code generation tools (templ, sqlc, wire, protoc, etc.) produce `_generated.go` or `_templ.go` files that are often in `.gitignore`. CI runners don't have the generator installed and can't regenerate them.
|
||||
|
||||
**Fix**: Remove the generated files from `.gitignore` and commit them. They are deterministic, diffable Go source — committing them is the standard practice for projects that don't want to install generators in CI.
|
||||
|
||||
If you need generators in CI anyway (e.g., for proto changes), add a generation step BEFORE lint in the `check` job and install the tool explicitly.
|
||||
|
||||
### 6. `errcheck` will flag `fmt.Sscanf` and similar "can't fail in practice" calls
|
||||
|
||||
Parsers that convert strings to numbers (e.g., parsing API responses where you know the format) will trigger `errcheck`. Suppress with blank assignment:
|
||||
|
||||
```go
|
||||
// before
|
||||
fmt.Sscanf(s, "%d", &n)
|
||||
|
||||
// after — communicates intent and satisfies errcheck
|
||||
_, _ = fmt.Sscanf(s, "%d", &n)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking CI setup complete:
|
||||
|
||||
- [ ] `python3 -c "import yaml; yaml.safe_load(...)"` passes on the workflow YAML
|
||||
- [ ] Runner appears as active in Gitea repo → Settings → Actions → Runners
|
||||
- [ ] A push to `main` triggers a run visible in Gitea repo → Actions
|
||||
- [ ] `check` job passes (lint + test + vet green)
|
||||
- [ ] `build` job produces an image in the local registry (`curl localhost:5000/v2/<image>/tags/list`)
|
||||
- [ ] `deploy` job shows rollout success in pod logs
|
||||
- [ ] `mirror` job shows "✓ Mirrored to GitHub" and the commit appears on GitHub
|
||||
- [ ] A PR (non-main push) runs `check` only, skips build/deploy/mirror
|
||||
- [ ] Any project-specific secrets are set via API and verified with a secrets list call
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `tdd` — for writing the tests that the `check` job runs
|
||||
- `planning` — for breaking down the work before writing pipeline YAML
|
||||
- `problem-analysis` — for debugging when a job fails in unexpected ways
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: grill-me
|
||||
description: Critically interrogates plans, ideas, and proposals to make them sharper, more robust, and better grounded — with the explicit goal of producing a meaningfully better plan by the end. Use this skill whenever the user wants to stress-test a plan, prepare for hard questions, check if an idea is ready to act on, validate a hypothesis, or explicitly asks to be challenged, questioned, or grilled. Also trigger when a conversation has been exploring an idea for a while and it's time to converge — use this skill to check if the idea is actually ready. Trigger phrases include "grill me", "challenge this", "poke holes", "am I missing something", "is this ready", "stress-test this", "devil's advocate", "what am I not seeing".
|
||||
---
|
||||
|
||||
# Grill-Me
|
||||
|
||||
A structured interrogation skill that pressure-tests plans and ideas before they become commitments. The goal is not to kill ideas — it's to produce a **demonstrably better plan** than the one that walked in.
|
||||
|
||||
## When to use each mode
|
||||
|
||||
Three interrogation modes. Pick based on context:
|
||||
|
||||
**Mode 1 — Quick Poke** (idea is early, user wants light pressure)
|
||||
3–5 sharp questions, each with a proposal. Fast. No structure overhead. Good for early Diamond 1 conversations.
|
||||
|
||||
**Mode 2 — Full Grill** (idea is approaching commitment, user wants thorough interrogation)
|
||||
Structured pass through all lenses below. Ends with a Plan Delta. Good for end of Diamond 1 or before starting a project.
|
||||
|
||||
**Mode 3 — Pre-mortem** (a plan exists and is about to be executed)
|
||||
Assume it failed. Work backwards. What went wrong? Good for Diamond 2 → Deliver transitions.
|
||||
|
||||
Read the context to pick the right mode. If unsure, ask: "Quick poke or full grill?"
|
||||
|
||||
---
|
||||
|
||||
## Always bring a proposal
|
||||
|
||||
When a question requires input or a decision, don't just ask — bring a recommendation:
|
||||
|
||||
> "What's the smallest version that validates the hypothesis? I'd suggest starting with just the ntfy URL-push channel (Phase 1), because it gives you real usage data before investing in the automated sync. Does that match your intuition?"
|
||||
|
||||
The format: sharp question → "I'd suggest [X]" → one-sentence reasoning → quick confirmation ask.
|
||||
|
||||
This respects the user's time, models good thinking, and makes it easier to say "yes, agreed" than to construct an answer from scratch. If you genuinely have no strong opinion, say so explicitly — but that should be rare.
|
||||
|
||||
---
|
||||
|
||||
## Full Grill — interrogation lenses
|
||||
|
||||
Run through each lens. Skip lenses that clearly don't apply, but say so explicitly ("Skipping competitive lens — this is internal tooling"). For each question that requires a decision, include a proposal.
|
||||
|
||||
### 1. Hypothesis clarity
|
||||
- Can the core claim be stated as: "We believe [X] will produce [Y], measurable by [Z]"?
|
||||
- Is Y falsifiable? Could an experiment prove this wrong?
|
||||
- Is Z actually measurable with available tools, or is it hand-wavy?
|
||||
|
||||
### 2. Assumptions audit
|
||||
- List the top 3 assumptions the plan depends on.
|
||||
- Which assumption, if wrong, kills the whole thing?
|
||||
- Has any assumption been validated, or are they all untested?
|
||||
|
||||
### 3. Futures resilience (Double Diamond + Futures Thinking lens)
|
||||
- Name 2–3 plausible alternative futures (not just the one assumed).
|
||||
- Does the plan still make sense in each of them?
|
||||
- What's the earliest signal that one of those other futures is manifesting?
|
||||
|
||||
### 4. Scope and reversibility
|
||||
- What is explicitly out of scope? (Unstated scope is a risk.)
|
||||
- Which decisions in this plan are irreversible?
|
||||
- What's the smallest version of this that would still validate the hypothesis?
|
||||
|
||||
### 5. Cost and effort reality check
|
||||
- What's the most expensive part? Time, money, attention?
|
||||
- Is there a simpler path to the same learning?
|
||||
- What does "good enough" look like vs "perfect"?
|
||||
|
||||
### 6. Failure modes
|
||||
- What's the most likely way this fails? (Not catastrophically — just quietly doesn't work.)
|
||||
- What's the most catastrophic failure mode, even if unlikely?
|
||||
- Is there a canary — an early signal that things are going wrong?
|
||||
|
||||
### 7. Who and why
|
||||
- Who benefits if this succeeds?
|
||||
- Who is harmed, blocked, or ignored?
|
||||
- Is there someone who should be consulted who hasn't been?
|
||||
|
||||
### 8. Next action clarity
|
||||
- Is the next step concrete enough that it could be handed to an agent or a person with no further explanation?
|
||||
- Is there a single person (or agent) responsible for it?
|
||||
- What would stop it from happening this week?
|
||||
|
||||
---
|
||||
|
||||
## Pre-mortem mode
|
||||
|
||||
Set the scene: "It's [6 months / 1 year] from now. This project has failed. Not dramatically — it just quietly didn't deliver. Walk me through what happened."
|
||||
|
||||
Then probe:
|
||||
- Was it a wrong assumption, a scope problem, or an execution problem?
|
||||
- Was there a moment where a different decision would have changed the outcome?
|
||||
- What should we do differently before we start?
|
||||
|
||||
Include a proposal for each finding: "To prevent this, I'd suggest [X]."
|
||||
|
||||
---
|
||||
|
||||
## Output format
|
||||
|
||||
**Quick Poke:** Questions numbered, each followed by a proposal. No preamble. Uncomfortable is fine.
|
||||
|
||||
**Full Grill:** One section per lens. Each section: 1–2 sentences of framing + 2–3 sharp questions + proposals where relevant. End with:
|
||||
- "The three things most worth resolving before moving forward are: [X, Y, Z]."
|
||||
- A **Plan Delta** section (see below).
|
||||
|
||||
**Pre-mortem:** Narrative first (2–3 sentences setting the failure scene), then structured findings with proposals, then "three things to do differently".
|
||||
|
||||
---
|
||||
|
||||
## Plan Delta — mandatory at the end of Full Grill and Pre-mortem
|
||||
|
||||
The grill is not done when the questions have been asked. It's done when the plan is **demonstrably stronger**. At minimum:
|
||||
|
||||
- At least one weak assumption has been named and either validated or explicitly accepted as a known risk
|
||||
- The hypothesis is sharper and more falsifiable than when we started
|
||||
- Something has changed: a scope reduction, a clearer metric, a reordered phase, a dropped assumption
|
||||
|
||||
If the plan hasn't changed at all after a Full Grill, something went wrong — the questions weren't sharp enough or the answers weren't pushed hard enough.
|
||||
|
||||
End every Full Grill and Pre-mortem with:
|
||||
|
||||
> **Plan delta**
|
||||
> Before: [one sentence on the original plan]
|
||||
> After: [one sentence on what concretely changed]
|
||||
> Still open: [1–2 unresolved questions and how/when they'll be resolved]
|
||||
|
||||
---
|
||||
|
||||
## Tone guidance
|
||||
|
||||
The grill should feel like a smart, trusted colleague who genuinely wants the plan to succeed — not a critic looking for reasons to say no. Questions should be sharp but not hostile. If something is clearly solid, say so briefly before moving on.
|
||||
|
||||
Don't soften questions with preambles. "Have you considered that..." is weaker than "What happens if...". Prefer the latter.
|
||||
|
||||
If the plan is actually good, say so: "Lenses 1–4 look solid. The real open questions are in lenses 5 and 6."
|
||||
|
||||
---
|
||||
|
||||
## Integration with Double Diamond
|
||||
|
||||
This skill maps to the **convergence moments** in the Double Diamond workflow:
|
||||
|
||||
- **End of Diamond 1 (Define):** Use Full Grill to check if the hypothesis is ready to become a project. The three convergence criteria (falsifiable hypothesis, measurable success criterion, futures-resilient) come directly from lenses 1 and 3.
|
||||
- **End of Diamond 2 (Deliver):** Use Pre-mortem before promoting to pre-prod.
|
||||
- **Mid-Diamond 2 (Develop):** Use Quick Poke when an agent's experiment result is ambiguous and you're deciding whether to continue or pivot.
|
||||
|
||||
The skill does NOT replace human judgment at convergence points — it sharpens the plan so that judgment is better informed.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: own-the-task
|
||||
description: Take ownership of a delegated task like a hired employee — interview first, write a handover brief, propose a plan, then execute. Use whenever the user says "take this off my plate", "own this", "act as my AI employee", "interview me first", "ask me questions before you start", or delegates anything open-ended where the real goal, the why, and the definition of done are not yet on the table. Trigger even when "interview" is never said — any delegation where getting the intent wrong would waste the whole effort belongs here.
|
||||
---
|
||||
|
||||
# Own the task
|
||||
|
||||
The user is delegating, not dictating. Value comes from the questions that change the shape of the work, not from a fast answer to the surface request.
|
||||
|
||||
**Intake → Brief → Plan → Execute.** Don't skip ahead. A task looking obvious usually means the real goal hasn't surfaced yet.
|
||||
|
||||
## 1. Intake
|
||||
|
||||
Ask the 5 highest-signal questions — the ones whose answers would most change your approach. **One per turn, wait for the answer.** Skip anything you could infer or look up. Follow up on any gap, contradiction, or opportunity; follow-ups don't count against the five.
|
||||
|
||||
Cover:
|
||||
|
||||
1. **Real goal** — the outcome underneath the stated deliverable.
|
||||
2. **Why** it matters to them.
|
||||
3. **Attention budget** — what they want to be *asked* about vs *told* about afterwards, plus hard constraints (time, money, people, politics, tooling, taste). This work fails by pulling them back in, not by missing a spec.
|
||||
4. **Definition of done** — observable. Then always follow up: *what changes for you once that's true?* Cold, that question gets a shrug; after the observable answer, it surfaces criteria they can't state directly.
|
||||
5. **Failure modes** — what bad looks like, or what previous attempts got wrong. Success criteria are rehearsed; being burned isn't.
|
||||
|
||||
Weak → strong:
|
||||
- "What's your deadline?" → "If this were half-finished on the deadline, what's the half you'd want done?"
|
||||
- "How involved do you want to be?" → "What would you be annoyed to be asked about — and what would you be annoyed to find out afterwards?"
|
||||
- "Any constraints?" → "What's been tried here before, and what made it not stick?"
|
||||
|
||||
If they answer thinly or say "you decide", take the decision, state the assumption, move on.
|
||||
|
||||
## 2. Handover brief
|
||||
|
||||
Write it as if you had amnesia and were briefing your future self — a stranger should be able to pick the task up cold.
|
||||
|
||||
```
|
||||
**Task:** / **Real goal:** / **Why:**
|
||||
**Constraints:** [hard vs soft, marked]
|
||||
**Attention budget:** [what comes back to them, what doesn't]
|
||||
**Definition of done:** / **What a win feels like:**
|
||||
**Failure modes to avoid:**
|
||||
**Open assumptions:** [what you decided for them, so they can correct it]
|
||||
```
|
||||
|
||||
## 3. Plan
|
||||
|
||||
Short: numbered steps with what each produces, the first checkpoint, and what you'll decide yourself (drawn from the attention budget, not guessed). Then ask for a go-ahead — one question, not a menu.
|
||||
|
||||
## 4. Execute
|
||||
|
||||
Decide the small things. Surface only what changes the outcome. If reality contradicts the brief, re-propose early rather than quietly bending the work. Deliver against the definition of done and say plainly where you fell short.
|
||||
|
||||
## Skip this
|
||||
|
||||
For small, well-specified, or factual requests. If the task is nearly specified, ask one or two questions and say why you're going short.
|
||||
@@ -0,0 +1,276 @@
|
||||
---
|
||||
name: planning
|
||||
description: Decompose work into small, verifiable, ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable units.
|
||||
---
|
||||
|
||||
# Planning and Task Breakdown
|
||||
|
||||
## Overview
|
||||
|
||||
Good task breakdown is the difference between reliable delivery and a tangled mess. Every task should be small enough to implement, test, and verify in a single focused session.
|
||||
|
||||
**Input:** A spec (load `spec-driven-dev` skill) or clear requirements.
|
||||
|
||||
**Output:** A prioritized, ordered list of tasks with acceptance criteria and verification steps.
|
||||
|
||||
## When to Use
|
||||
|
||||
- You have a spec and need to break it into implementable units
|
||||
- A task feels too large or vague to start
|
||||
- You need to communicate scope to Mathias
|
||||
- The implementation order isn't obvious
|
||||
- You want to identify parallelization opportunities
|
||||
|
||||
**When NOT to use:** Single-file changes with obvious scope.
|
||||
|
||||
## The Planning Process
|
||||
|
||||
### Step 1: Read-Only Discovery
|
||||
|
||||
Before writing any tasks, operate in read-only mode:
|
||||
|
||||
- Read the spec and relevant code
|
||||
- Identify existing patterns, conventions, and infrastructure
|
||||
- Map dependencies between components
|
||||
- Note risks and unknowns
|
||||
|
||||
**Do NOT write code during planning.** The output is a plan document, not implementation.
|
||||
|
||||
### Step 2: Map the Dependency Graph
|
||||
|
||||
Visualize what depends on what:
|
||||
|
||||
```
|
||||
Database schema / migrations
|
||||
│
|
||||
├── Domain types (pure Go structs + interfaces)
|
||||
│ │
|
||||
│ ├── Service layer (business logic)
|
||||
│ │ │
|
||||
│ │ └── HTTP handlers
|
||||
│ │
|
||||
│ └── Store implementations (postgres, in-memory)
|
||||
│
|
||||
└── Seed data / test fixtures
|
||||
```
|
||||
|
||||
**Build foundations first.** You can't test a handler before the service exists. You can't write a service before the domain types are defined.
|
||||
|
||||
### Step 3: Slice Vertically
|
||||
|
||||
Build one complete feature path at a time, not one layer at a time.
|
||||
|
||||
**Bad (horizontal slicing):**
|
||||
```
|
||||
Task 1: Write all database migrations
|
||||
Task 2: Write all store implementations
|
||||
Task 3: Write all service methods
|
||||
Task 4: Write all handlers
|
||||
```
|
||||
No working software until Task 4. If Task 1 is wrong, you discover it late.
|
||||
|
||||
**Good (vertical slicing):**
|
||||
```
|
||||
Task 1: User can create an account (migration + store + service + handler for create)
|
||||
Task 2: User can log in (auth store + service + handler)
|
||||
Task 3: User can view their invoices (query + service + handler + response type)
|
||||
```
|
||||
Each task delivers working, testable functionality. If Task 1 reveals a design issue, Tasks 2+ can adapt.
|
||||
|
||||
### Step 4: Write Tasks
|
||||
|
||||
Each task follows this structure:
|
||||
|
||||
```markdown
|
||||
## Task [N]: [Short descriptive title]
|
||||
|
||||
**Description:** One sentence explaining what this task accomplishes.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] [Specific, testable condition]
|
||||
- [ ] [Specific, testable condition]
|
||||
|
||||
**Verification:**
|
||||
- [ ] Tests pass: `go test -run TestFeatureName ./...`
|
||||
- [ ] Build: `go build ./...`
|
||||
- [ ] Manual check: [description if needed]
|
||||
|
||||
**Dependencies:** Task [N-1], Task [N-2] (or "None")
|
||||
|
||||
**Files likely touched:**
|
||||
- `internal/domain/user.go`
|
||||
- `internal/store/user_store.go`
|
||||
- `internal/service/user_service.go`
|
||||
|
||||
**Estimated scope:** [XS/S/M/L]
|
||||
```
|
||||
|
||||
### Step 5: Order and Add Checkpoints
|
||||
|
||||
Arrange tasks so:
|
||||
1. Dependencies are satisfied (build foundation first)
|
||||
2. Each task leaves the system in a working state
|
||||
3. High-risk tasks are early (fail fast)
|
||||
4. Checkpoints occur after every 2–3 tasks
|
||||
|
||||
```markdown
|
||||
## Checkpoint: After Tasks 1–3
|
||||
|
||||
- [ ] `go test ./...` passes
|
||||
- [ ] `go build ./...` succeeds
|
||||
- [ ] Core user flow works end-to-end (can register and log in)
|
||||
- [ ] Mathias reviews before continuing
|
||||
```
|
||||
|
||||
## Task Sizing Guidelines
|
||||
|
||||
| Size | Files | Scope | Example |
|
||||
|------|-------|-------|---------|
|
||||
| **XS** | 1 | Single function or type | Add a validation rule to User |
|
||||
| **S** | 1–2 | One endpoint or store method | Add GetByEmail to UserStore |
|
||||
| **M** | 3–5 | One feature slice | User registration (domain + store + service + handler) |
|
||||
| **L** | 5–8 | Multi-component feature | Search with filtering, pagination, and sorting |
|
||||
| **XL** | 8+ | **Too large — break it down** | — |
|
||||
|
||||
**Break any XL task into smaller tasks.** An agent performs best on XS, S, and M tasks.
|
||||
|
||||
**Break a task down further when:**
|
||||
- It would take more than 2–3 hours of focused work
|
||||
- The acceptance criteria have more than 4 bullet points
|
||||
- It touches two independent subsystems (auth AND billing in the same task)
|
||||
- You find yourself writing "and" in the task title
|
||||
|
||||
## Plan Document Template
|
||||
|
||||
```markdown
|
||||
# Implementation Plan: [Feature/Project Name]
|
||||
|
||||
## Overview
|
||||
[One paragraph: what we're building and the key technical decisions]
|
||||
|
||||
## Architecture Decisions
|
||||
- [Decision 1 and rationale]
|
||||
- [Decision 2 and rationale]
|
||||
|
||||
## Dependency Graph
|
||||
[Diagram or list showing what depends on what]
|
||||
|
||||
## Task List
|
||||
|
||||
### Phase 1: Foundation
|
||||
|
||||
- [ ] **Task 1:** [Title] — [Scope: XS/S/M]
|
||||
- AC: [criteria]
|
||||
- Verify: `go test -run TestX ./...`
|
||||
- Files: [list]
|
||||
|
||||
- [ ] **Task 2:** [Title] — [Scope: XS/S/M]
|
||||
...
|
||||
|
||||
### Checkpoint: Foundation Complete
|
||||
- [ ] `go test ./...` passes
|
||||
- [ ] Build clean
|
||||
- [ ] Foundation behavior works end-to-end
|
||||
|
||||
### Phase 2: Core Features
|
||||
|
||||
- [ ] **Task 3:** [Title] — [Scope: S/M]
|
||||
...
|
||||
|
||||
### Checkpoint: Core Features Complete
|
||||
- [ ] User-facing scenarios work
|
||||
- [ ] Mathias reviews before Phase 3
|
||||
|
||||
### Phase 3: Polish and Edge Cases
|
||||
|
||||
- [ ] **Task 5:** Handle [edge case]
|
||||
- [ ] **Task 6:** [Performance/error handling]
|
||||
|
||||
### Checkpoint: Complete
|
||||
- [ ] All acceptance criteria from the spec are met
|
||||
- [ ] `go test -race ./...` passes
|
||||
- [ ] govulncheck passes
|
||||
- [ ] Ready for review
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|-----------|
|
||||
| [External API behavior unknown] | High | Spike task first |
|
||||
| [Data volume may be larger than expected] | Medium | Add pagination before shipping |
|
||||
|
||||
## Open Questions
|
||||
- [Question requiring Mathias's input]
|
||||
```
|
||||
|
||||
## Parallelization
|
||||
|
||||
When working in parallel (multiple sessions or agents):
|
||||
|
||||
- **Safe to parallelize:** Independent feature slices, tests for already-implemented features
|
||||
- **Must be sequential:** Database migrations, shared interface changes, dependency chains
|
||||
- **Needs coordination:** Features that share an API contract — define the contract first, then parallelize
|
||||
|
||||
## Go-Specific Planning Notes
|
||||
|
||||
### Database First, Then Domain
|
||||
|
||||
If the feature requires a schema change:
|
||||
1. Write and apply migration first
|
||||
2. Generate sqlc code (`task generate` or `sqlc generate`)
|
||||
3. Then write domain types and service
|
||||
|
||||
### Interface Contracts Before Parallel Work
|
||||
|
||||
If two tasks depend on a shared interface:
|
||||
```go
|
||||
// Define this first, in its own task
|
||||
type InvoiceStore interface {
|
||||
Save(ctx context.Context, inv Invoice) error
|
||||
GetByID(ctx context.Context, id InvoiceID) (Invoice, error)
|
||||
}
|
||||
```
|
||||
|
||||
Then write the postgres implementation and the service in parallel.
|
||||
|
||||
### Spike Tasks for Unknowns
|
||||
|
||||
If a risk involves an unknown (new API, unfamiliar library, performance unknown):
|
||||
|
||||
```markdown
|
||||
## Task 0 (Spike): Validate Bankgirot API integration
|
||||
|
||||
**Description:** Verify we can submit an ISO 20022 message to Bankgirot sandbox.
|
||||
**Timebox:** 2 hours
|
||||
**Output:** Working code or decision to change approach
|
||||
**Not:** Production-quality code — throw it away after learning
|
||||
```
|
||||
|
||||
Spikes are timeboxed explorations. The code is thrown away. The learning is kept.
|
||||
|
||||
## Verification Before Starting
|
||||
|
||||
Before beginning implementation:
|
||||
|
||||
- [ ] Every task has acceptance criteria
|
||||
- [ ] Every task has a verification step
|
||||
- [ ] Task dependencies are identified and ordered correctly
|
||||
- [ ] No task touches more than ~5 files
|
||||
- [ ] Checkpoints exist between major phases
|
||||
- [ ] Mathias has reviewed and approved the plan
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "I'll figure it out as I go" | That's how you end up with rework. 10 minutes of planning saves hours. |
|
||||
| "The tasks are obvious" | Write them down. Explicit tasks surface hidden dependencies. |
|
||||
| "Planning is overhead" | Planning IS the task. Implementation without a plan is just typing. |
|
||||
| "I can hold it all in my head" | Context windows are finite. Written plans survive session boundaries. |
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Requires: `spec-driven-dev` skill output
|
||||
- During implementation: load `tdd` skill per task
|
||||
- For large refactoring tasks: load `refactoring` skill
|
||||
- For architecture decisions: load `solid` skill
|
||||
@@ -0,0 +1,273 @@
|
||||
---
|
||||
name: playwright
|
||||
description: Add and run Playwright E2E tests for any project with a web UI — covers setup, HTMX-specific patterns, SSE/streaming, Taskfile integration, and CI wiring.
|
||||
---
|
||||
|
||||
# Playwright E2E Testing
|
||||
|
||||
## When to use
|
||||
|
||||
- Project has a web UI (HTMX, React, plain HTML — any stack)
|
||||
- You want to verify the golden path in a real browser, not just unit-test handlers
|
||||
- Chat/SSE streams, HTMX swaps, or dynamic DOM need testing that Go tests cannot cover
|
||||
|
||||
## Setup (first time in a project)
|
||||
|
||||
### 1. Install
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
npm install --save-dev @playwright/test
|
||||
npx playwright install chromium # chromium only — enough for CI
|
||||
```
|
||||
|
||||
### 2. Directory layout
|
||||
|
||||
```
|
||||
<repo>/
|
||||
e2e/
|
||||
playwright.config.ts
|
||||
tests/
|
||||
smoke.spec.ts
|
||||
chat.spec.ts
|
||||
package.json
|
||||
Taskfile.yml ← add e2e task here
|
||||
```
|
||||
|
||||
Keep `e2e/` at repo root. Add to `.gitignore`:
|
||||
```
|
||||
node_modules/
|
||||
e2e/test-results/
|
||||
e2e/playwright-report/
|
||||
```
|
||||
|
||||
### 3. Config (`e2e/playwright.config.ts`)
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
timeout: 30_000,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL ?? 'http://localhost:8080',
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Taskfile task
|
||||
|
||||
```yaml
|
||||
tasks:
|
||||
e2e:
|
||||
desc: "Run Playwright E2E tests against running dev server"
|
||||
dir: e2e
|
||||
cmds:
|
||||
- npx playwright test {{.CLI_ARGS}}
|
||||
env:
|
||||
BASE_URL: "http://localhost:8080"
|
||||
|
||||
e2e:ui:
|
||||
desc: "Open Playwright UI mode"
|
||||
dir: e2e
|
||||
cmds:
|
||||
- npx playwright test --ui
|
||||
```
|
||||
|
||||
Run with: `task e2e` or `task e2e -- --headed` or `task e2e -- tests/chat.spec.ts`
|
||||
|
||||
---
|
||||
|
||||
## HTMX-specific patterns
|
||||
|
||||
HTMX updates the DOM asynchronously. Never use `waitForSelector` alone — wait for HTMX to finish the swap.
|
||||
|
||||
### Wait for HTMX swap to complete
|
||||
|
||||
```typescript
|
||||
// After triggering an action that causes an HTMX request:
|
||||
await page.locator('[hx-target]').waitFor({ state: 'visible' });
|
||||
|
||||
// Better: wait for htmx:afterSwap event on the target element
|
||||
await page.evaluate(() =>
|
||||
new Promise<void>(resolve =>
|
||||
document.body.addEventListener('htmx:afterSwap', () => resolve(), { once: true })
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Wait for specific content to appear after swap
|
||||
|
||||
```typescript
|
||||
await page.locator('#result').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('#result')).toContainText('expected text');
|
||||
```
|
||||
|
||||
### Avoid timing races
|
||||
|
||||
```typescript
|
||||
// BAD — may grab stale content before swap completes
|
||||
await page.click('button');
|
||||
const text = await page.locator('#output').textContent();
|
||||
|
||||
// GOOD — assert after waiting
|
||||
await page.click('button');
|
||||
await expect(page.locator('#output')).toContainText('expected text', { timeout: 5000 });
|
||||
```
|
||||
|
||||
### Forms with HTMX submit
|
||||
|
||||
```typescript
|
||||
await page.fill('input[name="query"]', 'hello');
|
||||
await page.keyboard.press('Enter'); // or click submit button
|
||||
await expect(page.locator('#response')).not.toBeEmpty({ timeout: 10_000 });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SSE / streaming patterns
|
||||
|
||||
For chat UIs or any endpoint using `text/event-stream`:
|
||||
|
||||
```typescript
|
||||
test('chat stream delivers response', async ({ page }) => {
|
||||
await page.goto('/chat');
|
||||
|
||||
// Type and submit
|
||||
await page.fill('#message-input', 'what is 2+2?');
|
||||
await page.click('#send-btn');
|
||||
|
||||
// SSE response appears incrementally — wait for streaming to stop
|
||||
// Strategy: wait until the response element stops growing
|
||||
const responseEl = page.locator('#chat-response');
|
||||
await responseEl.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
|
||||
// Poll until content stabilises (streaming done)
|
||||
let prev = '';
|
||||
let stable = 0;
|
||||
while (stable < 3) {
|
||||
const cur = await responseEl.textContent() ?? '';
|
||||
if (cur === prev && cur.length > 0) stable++;
|
||||
else { stable = 0; prev = cur; }
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
expect(prev.length).toBeGreaterThan(0);
|
||||
});
|
||||
```
|
||||
|
||||
For a done-signal in the DOM (e.g. a `[data-streaming="false"]` attribute set when stream ends):
|
||||
|
||||
```typescript
|
||||
await page.locator('[data-streaming="false"]').waitFor({ timeout: 20_000 });
|
||||
const text = await page.locator('#chat-response').textContent();
|
||||
expect(text).toBeTruthy();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Golden path smoke test template
|
||||
|
||||
```typescript
|
||||
// e2e/tests/smoke.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('smoke', () => {
|
||||
test('home page loads', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/.+/);
|
||||
await expect(page.locator('body')).not.toBeEmpty();
|
||||
});
|
||||
|
||||
test('main nav is present', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('nav')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Screenshot on failure
|
||||
|
||||
Already configured via `screenshot: 'only-on-failure'` in the config. Artifacts land in `e2e/test-results/`. View them with:
|
||||
|
||||
```bash
|
||||
npx playwright show-report e2e/playwright-report
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI wiring (Gitea workflow)
|
||||
|
||||
```yaml
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node deps
|
||||
working-directory: e2e
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
working-directory: e2e
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Start server
|
||||
run: ./bin/server &
|
||||
env:
|
||||
PORT: 8080
|
||||
|
||||
- name: Wait for server
|
||||
run: |
|
||||
for i in $(seq 1 20); do
|
||||
curl -sf http://localhost:8080/ && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run E2E tests
|
||||
working-directory: e2e
|
||||
run: npx playwright test
|
||||
env:
|
||||
BASE_URL: http://localhost:8080
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
When adding Playwright to a project:
|
||||
|
||||
- [ ] `npm init -y && npm install --save-dev @playwright/test` in `e2e/`
|
||||
- [ ] `npx playwright install chromium`
|
||||
- [ ] `e2e/playwright.config.ts` with baseURL and screenshot on failure
|
||||
- [ ] At least one smoke test covering the golden path
|
||||
- [ ] `.gitignore` entries: `node_modules/`, `test-results/`, `playwright-report/`
|
||||
- [ ] `task e2e` wired in Taskfile.yml
|
||||
- [ ] CI step added to Gitea workflow
|
||||
|
||||
## Failure triage
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| `TimeoutError` on HTMX element | Swap not finished | Use `htmx:afterSwap` event or increase timeout |
|
||||
| Stale content asserted | Race between click and swap | Move assertion after explicit wait |
|
||||
| SSE test flaky | Stream not done yet | Poll for stable content or use done-signal attribute |
|
||||
| CI fails, local passes | Server not ready | Add wait-for-server loop in CI |
|
||||
| Screenshots empty | Screenshot taken before render | Add `await page.waitForLoadState('networkidle')` |
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: problem-analysis
|
||||
description: Deeply understand a problem before coding begins. Identify the real problem, stakeholders, constraints, and success criteria. Use before starting any non-trivial feature.
|
||||
---
|
||||
|
||||
# Problem Analysis
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Understand WHAT needs to be solved and WHY — before considering HOW.**
|
||||
|
||||
Premature solution thinking is one of the most expensive mistakes in software. A feature built on a misunderstood problem is waste, no matter how well it's coded.
|
||||
|
||||
> "A problem well-stated is a problem half-solved." — Charles Kettering
|
||||
|
||||
## When to Use
|
||||
|
||||
**Always use before:**
|
||||
- Starting any non-trivial feature (more than a day's work)
|
||||
- Tackling a vague or ambiguous requirement
|
||||
- Making an architectural decision
|
||||
- Starting a new project or significant component
|
||||
|
||||
**When NOT to use:** Bug fixes with clear reproduction steps, one-line changes, configuration updates.
|
||||
|
||||
## The Analysis Process
|
||||
|
||||
### Step 1: Understand the Real Problem
|
||||
|
||||
Don't accept the first statement of a problem at face value. Dig to the real underlying issue.
|
||||
|
||||
**Questions to ask:**
|
||||
- What is the user actually trying to accomplish?
|
||||
- What pain or friction does this create today?
|
||||
- Why is this a problem now? What changed?
|
||||
- What happens if we don't solve it?
|
||||
- Who else is affected?
|
||||
|
||||
**The 5 Whys technique:** Ask "why" five times to reach the root cause.
|
||||
|
||||
```
|
||||
Problem: "The invoice export is too slow"
|
||||
Why? → "Users have to wait 30 seconds"
|
||||
Why? → "We're generating all 5000 invoices in a single query"
|
||||
Why? → "The feature was built for 50 invoices, now there are 5000"
|
||||
Why? → "We didn't anticipate growth"
|
||||
Root: "Batch processing architecture needed, not performance tuning"
|
||||
```
|
||||
|
||||
The solution to "export is slow" might be pagination, async export, or caching — not just query optimization.
|
||||
|
||||
### Step 2: Identify Stakeholders and Their Perspectives
|
||||
|
||||
Every stakeholder has a different view of the problem.
|
||||
|
||||
| Stakeholder | Typical concern |
|
||||
|-------------|----------------|
|
||||
| End user | "I need to accomplish X in Y time" |
|
||||
| Business | "This costs us money / blocks revenue" |
|
||||
| Ops/DevOps | "This creates operational burden" |
|
||||
| Compliance | "This creates regulatory risk" |
|
||||
| Developer | "This is hard to maintain or extend" |
|
||||
|
||||
**Map the stakeholders:** Who is directly affected? Who is indirectly affected? Who has authority over the solution?
|
||||
|
||||
### Step 3: Define User Needs and Pain Points
|
||||
|
||||
Move from "what users say they want" to "what users actually need."
|
||||
|
||||
```
|
||||
User says: "I need a report with 20 columns"
|
||||
User needs: "I need to identify which customers are at risk of churning"
|
||||
|
||||
These may have completely different solutions.
|
||||
```
|
||||
|
||||
**Techniques:**
|
||||
- User journey mapping: walk through the current process step by step
|
||||
- Pain point identification: where is friction, delay, error, confusion?
|
||||
- Job-to-be-done framing: "When [situation], I want to [motivation], so I can [outcome]"
|
||||
|
||||
### Step 4: Clarify Requirements and Constraints
|
||||
|
||||
**Functional requirements:** What must the system do? (In user terms, not technical terms)
|
||||
- "Users can export invoices as PDF"
|
||||
- "The system sends a confirmation email within 5 minutes"
|
||||
|
||||
**Non-functional requirements:** How must the system behave?
|
||||
- Performance: "Export completes in under 5 seconds for 90% of users"
|
||||
- Security: "Only authenticated users can view their own invoices"
|
||||
- Availability: "Export feature available 99.9% of the time"
|
||||
|
||||
**Constraints:** What is fixed and non-negotiable?
|
||||
- Budget/time
|
||||
- Existing systems that must be integrated
|
||||
- Regulatory requirements
|
||||
- Technology choices already made
|
||||
|
||||
### Step 5: Clarify Scope
|
||||
|
||||
Explicitly define what is **in scope** and what is **out of scope**. Out-of-scope items are often as important as in-scope items for preventing scope creep.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
In scope:
|
||||
- [Feature A]
|
||||
- [Feature B]
|
||||
|
||||
Out of scope (for this version):
|
||||
- [Feature C] — will be addressed in v2
|
||||
- [Feature D] — not part of this problem
|
||||
|
||||
Assumptions requiring validation:
|
||||
- [Assumption 1]
|
||||
- [Assumption 2]
|
||||
```
|
||||
|
||||
### Step 6: Define Success Criteria
|
||||
|
||||
Success criteria must be specific and testable. Vague criteria are not criteria.
|
||||
|
||||
```
|
||||
Vague: "Make the dashboard faster"
|
||||
|
||||
Specific and testable:
|
||||
- Dashboard LCP < 2.5s on a 4G connection (measured with Lighthouse)
|
||||
- Initial data load completes in < 500ms (measured from API call to render)
|
||||
- Page is usable within 3s even if non-critical data hasn't loaded
|
||||
```
|
||||
|
||||
**Success criteria format:** `[Metric] [comparison] [threshold] [measurement method]`
|
||||
|
||||
### Step 7: Identify Risks and Unknowns
|
||||
|
||||
| Category | Questions to ask |
|
||||
|----------|-----------------|
|
||||
| Technical | Do we know how to build this? What might surprise us? |
|
||||
| Business | Does the market actually want this? Is the problem real? |
|
||||
| Regulatory | Are there compliance implications? |
|
||||
| Operational | Will this create new operational burden? |
|
||||
| Integration | What external systems does this depend on? |
|
||||
|
||||
## Output: Problem Analysis Document
|
||||
|
||||
The analysis should produce a `problem-analysis.md` document covering:
|
||||
|
||||
```markdown
|
||||
# Problem Analysis: [Feature/System Name]
|
||||
|
||||
## Problem Statement
|
||||
[One paragraph: what problem exists, for whom, and why it matters]
|
||||
|
||||
## Context
|
||||
[How this problem arose, what triggered the need to solve it now]
|
||||
|
||||
## Stakeholders
|
||||
| Stakeholder | Role | Primary concern |
|
||||
|------------|------|----------------|
|
||||
| [Name/role] | [Decision maker/user/affected] | [Their core need] |
|
||||
|
||||
## User Needs and Pain Points
|
||||
- [Pain point 1]: [Specific friction or failure today]
|
||||
- [Pain point 2]: ...
|
||||
|
||||
## Functional Requirements
|
||||
(In user terms, not technical terms)
|
||||
- [ ] [Requirement 1]
|
||||
- [ ] [Requirement 2]
|
||||
|
||||
## Non-Functional Requirements
|
||||
- Performance: [specific, measurable]
|
||||
- Security: [specific]
|
||||
- Availability: [specific]
|
||||
|
||||
## Scope
|
||||
**In scope:** [list]
|
||||
**Out of scope:** [list]
|
||||
|
||||
## Assumptions Requiring Validation
|
||||
- [Assumption 1]: [How to validate]
|
||||
- [Assumption 2]: ...
|
||||
|
||||
## Risks and Unknowns
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|-----------|-----------|
|
||||
| [Risk 1] | High/Med/Low | High/Med/Low | [Strategy] |
|
||||
|
||||
## Success Criteria
|
||||
- [Metric]: [threshold] ([measurement method])
|
||||
- [Metric]: [threshold] ([measurement method])
|
||||
|
||||
## Open Questions
|
||||
- [Question requiring answer before implementation begins]
|
||||
```
|
||||
|
||||
## Mathias Context
|
||||
|
||||
As a digital PM building production software:
|
||||
|
||||
- **Capture the why:** The problem document should capture business context, not just technical requirements. Agents reading it should understand why this matters.
|
||||
- **Think about the user journey:** Walk through the entire experience, not just the feature in isolation.
|
||||
- **Challenge the requirement:** Is this the right problem to solve? Is there a simpler solution?
|
||||
- **Estimate impact:** How many users are affected? How often? What's the cost of not solving it?
|
||||
|
||||
## Transition to Next Steps
|
||||
|
||||
After the problem is understood:
|
||||
|
||||
1. Load `user-stories` skill to decompose into implementable stories
|
||||
2. Load `spec-driven-dev` skill to write a technical specification
|
||||
3. Load `planning` skill to break into tasks
|
||||
|
||||
**Do NOT jump to implementation.** A complete problem analysis prevents the most expensive mistakes.
|
||||
|
||||
## Red Flags — When Analysis Is Incomplete
|
||||
|
||||
- Requirements stated in technical terms ("add an index") instead of user terms ("make search faster")
|
||||
- No clear success criteria ("it should be better")
|
||||
- No stakeholders identified
|
||||
- Assumptions not written down
|
||||
- Scope not defined
|
||||
- "We know what we need to build" — this is usually premature
|
||||
@@ -0,0 +1,368 @@
|
||||
---
|
||||
name: refactoring
|
||||
description: Systematic refactoring methodology based on Martin Fowler's catalog. Apply during the REFACTOR phase of TDD, never while making code changes that modify behavior.
|
||||
---
|
||||
|
||||
# Refactoring
|
||||
|
||||
## Core Principle
|
||||
|
||||
> "Refactoring is a controlled technique for improving the design of existing code. Its essence is applying a series of small behavior-preserving transformations." — Martin Fowler
|
||||
|
||||
**The First Rule:** Never refactor without tests. Tests are your safety net. If tests don't exist, write them first (load `tdd` skill).
|
||||
|
||||
**The Second Rule:** Each refactoring step must keep tests green. If a step breaks tests, undo it.
|
||||
|
||||
**Never mix refactoring and behavior change.** Separate commits for each.
|
||||
|
||||
## When to Refactor
|
||||
|
||||
- After the GREEN phase of TDD (REFACTOR phase)
|
||||
- When code smell makes a feature hard to add
|
||||
- When the Boy Scout Rule demands it — you're in the area, leave it better
|
||||
- When technical debt is blocking velocity
|
||||
|
||||
**When NOT to refactor:**
|
||||
- Code that works and won't change
|
||||
- Code being replaced soon
|
||||
- When you don't have tests
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Ensure Test Coverage
|
||||
|
||||
```bash
|
||||
go test ./... # All tests pass first
|
||||
go test -race ./... # Race detector clean
|
||||
```
|
||||
|
||||
If tests don't exist, write them before refactoring. See the `tdd` skill.
|
||||
|
||||
### Step 2: Identify the Target
|
||||
|
||||
Load the `code-review` skill to detect smells. Prioritize:
|
||||
1. **Architectural smells** — Shotgun Surgery, Divergent Change, God Objects
|
||||
2. **Design smells** — Long Methods, Feature Envy, Primitive Obsession
|
||||
3. **Readability smells** — naming, comments, duplication
|
||||
|
||||
### Step 3: Apply Techniques in Small Steps
|
||||
|
||||
Each technique below is one small step. Make the change. Run tests. Commit if green. Repeat.
|
||||
|
||||
### Step 4: Verify After Each Step
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
```
|
||||
|
||||
If red: undo the step (git checkout), understand why, try a smaller step.
|
||||
|
||||
## Technique Catalog
|
||||
|
||||
### Composing Methods
|
||||
|
||||
**Extract Function**
|
||||
When: a code block can be named meaningfully; a function does multiple things.
|
||||
```go
|
||||
// Before
|
||||
func processOrder(o *Order) {
|
||||
// validate
|
||||
if o.Items == nil { panic("no items") }
|
||||
// calculate
|
||||
total := 0.0
|
||||
for _, item := range o.Items {
|
||||
total += item.Price
|
||||
}
|
||||
// save
|
||||
db.Save(o, total)
|
||||
}
|
||||
|
||||
// After: each step extracted
|
||||
func processOrder(o *Order) {
|
||||
validateOrder(o)
|
||||
total := calculateTotal(o.Items)
|
||||
saveOrder(o, total)
|
||||
}
|
||||
```
|
||||
Cross-reference: when naming extracted functions, load `clean-code` skill for naming conventions.
|
||||
|
||||
**Inline Function**
|
||||
When: a function's body is as clear as its name; a function only does what its name says.
|
||||
|
||||
**Extract Variable**
|
||||
When: a complex expression is hard to read inline.
|
||||
```go
|
||||
// Before
|
||||
if user.Subscription.Level >= 2 && !user.IsBanned && user.VerifiedAt != nil { ... }
|
||||
|
||||
// After
|
||||
canAccessPremium := user.Subscription.Level >= 2 && !user.IsBanned && user.VerifiedAt != nil
|
||||
if canAccessPremium { ... }
|
||||
```
|
||||
|
||||
**Replace Temp with Query**
|
||||
When: a temporary variable holds the result of an expression used once.
|
||||
|
||||
### Moving Features Between Types
|
||||
|
||||
**Move Method**
|
||||
When: Feature Envy detected — a method uses another type's data more than its own.
|
||||
```go
|
||||
// Before: Order envies Customer
|
||||
func (o *Order) getShippingCost() float64 {
|
||||
if o.Customer.Country == "SE" { return 0 }
|
||||
return 50
|
||||
}
|
||||
|
||||
// After: moved to Customer
|
||||
func (c *Customer) ShippingCost() float64 {
|
||||
if c.Country == "SE" { return 0 }
|
||||
return 50
|
||||
}
|
||||
```
|
||||
|
||||
**Extract Type**
|
||||
When: a cluster of methods and fields have a different responsibility than the rest of the type.
|
||||
|
||||
**Inline Type**
|
||||
When: a type isn't doing enough to justify its existence (Lazy Class smell).
|
||||
|
||||
**Hide Delegate**
|
||||
When: callers access objects through chains (`a.B().C()`).
|
||||
|
||||
**Remove Middle Man**
|
||||
When: a type only delegates without adding value.
|
||||
|
||||
### Organizing Data
|
||||
|
||||
**Replace Data Value with Object**
|
||||
When: Primitive Obsession detected — a string is being used for email, user ID, currency.
|
||||
```go
|
||||
// Before
|
||||
type User struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
// After: Email type carries validation and prevents misuse
|
||||
type Email struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func NewEmail(s string) (Email, error) {
|
||||
if !strings.Contains(s, "@") {
|
||||
return Email{}, fmt.Errorf("invalid email: %q", s)
|
||||
}
|
||||
return Email{value: s}, nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Email Email
|
||||
}
|
||||
```
|
||||
|
||||
**Replace Magic Number with Constant**
|
||||
```go
|
||||
// Before
|
||||
if retries > 3 { ... }
|
||||
|
||||
// After
|
||||
const maxRetries = 3
|
||||
if retries > maxRetries { ... }
|
||||
```
|
||||
|
||||
### Simplifying Conditionals
|
||||
|
||||
**Decompose Conditional**
|
||||
When: complex conditionals obscure intent.
|
||||
```go
|
||||
// Before
|
||||
if plan.ExpiresAt.Before(time.Now()) && plan.GracePeriodDays > 0 { ... }
|
||||
|
||||
// After
|
||||
if plan.IsExpiredWithGrace() { ... }
|
||||
```
|
||||
|
||||
**Replace Nested Conditional with Guard Clauses**
|
||||
When: deep nesting; use early returns instead.
|
||||
```go
|
||||
// Before: arrow code
|
||||
func process(o *Order) error {
|
||||
if o != nil {
|
||||
if len(o.Items) > 0 {
|
||||
if o.Customer != nil {
|
||||
// actual logic
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// After: guard clauses
|
||||
func process(o *Order) error {
|
||||
if o == nil {
|
||||
return errors.New("nil order")
|
||||
}
|
||||
if len(o.Items) == 0 {
|
||||
return errors.New("empty order")
|
||||
}
|
||||
if o.Customer == nil {
|
||||
return errors.New("no customer")
|
||||
}
|
||||
// actual logic
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Replace Conditional with Polymorphism**
|
||||
When: repeated switch/if-else on a type string. Replace with an interface.
|
||||
See the `solid` skill, OCP section for full example.
|
||||
|
||||
**Introduce Null Object**
|
||||
When: repeated nil checks for a dependency.
|
||||
|
||||
### Simplifying Function Calls
|
||||
|
||||
**Rename Method/Variable**
|
||||
The most powerful refactoring. When you understand what something does, give it a name that reflects that understanding.
|
||||
Cross-reference: load `clean-code` skill for naming conventions.
|
||||
|
||||
**Introduce Parameter Object**
|
||||
When: Long Parameter List smell — multiple parameters that always appear together.
|
||||
```go
|
||||
// Before
|
||||
func Search(query string, page, pageSize int, sortBy string, ascending bool) Results { ... }
|
||||
|
||||
// After
|
||||
type SearchOptions struct {
|
||||
Query string
|
||||
Page int
|
||||
PageSize int
|
||||
SortBy string
|
||||
Ascending bool
|
||||
}
|
||||
|
||||
func Search(opts SearchOptions) Results { ... }
|
||||
```
|
||||
|
||||
**Separate Query from Modifier (Command-Query Separation)**
|
||||
When: a function both changes state and returns data.
|
||||
```go
|
||||
// Bad: modifies AND returns
|
||||
func (s *Stack) PopAndReturn() (Item, bool) {
|
||||
// removes and returns
|
||||
}
|
||||
|
||||
// Good: separate
|
||||
func (s *Stack) Peek() (Item, bool) { ... } // query only
|
||||
func (s *Stack) Pop() bool { ... } // command only
|
||||
```
|
||||
|
||||
### Dealing with Generalization
|
||||
|
||||
**Extract Interface**
|
||||
When: you have multiple implementations or want to enable testing with a test double.
|
||||
```go
|
||||
// Before: depends on concrete type
|
||||
type Service struct {
|
||||
db *postgres.DB
|
||||
}
|
||||
|
||||
// After: depends on interface
|
||||
type Store interface {
|
||||
Save(ctx context.Context, u User) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
store Store
|
||||
}
|
||||
```
|
||||
|
||||
**Replace Inheritance with Delegation (Go: Embedding → Explicit Delegation)**
|
||||
When: embedding is used but only some methods of the embedded type are needed.
|
||||
|
||||
## Refactoring Sequence Dependencies
|
||||
|
||||
Apply in this order to avoid breaking changes:
|
||||
|
||||
1. **Extract Variable** → then Extract Function → then Move Function
|
||||
2. **Encapsulate Field** → before other data refactorings
|
||||
3. **Extract Function** → before Move Function
|
||||
4. **Rename** → before Extract Interface
|
||||
|
||||
## Go-Specific Refactoring Notes
|
||||
|
||||
### Extracting interfaces
|
||||
|
||||
In Go, define the interface at the point of use, not at the implementation:
|
||||
```go
|
||||
// The service package defines what it needs
|
||||
package service
|
||||
|
||||
type UserStore interface {
|
||||
GetByID(ctx context.Context, id UserID) (User, error)
|
||||
}
|
||||
```
|
||||
|
||||
### Error handling during refactoring
|
||||
|
||||
When extracting functions, preserve the error wrapping chain:
|
||||
```go
|
||||
// Extracted function must wrap errors with its context
|
||||
func validateOrder(o *Order) error {
|
||||
if len(o.Items) == 0 {
|
||||
return errors.New("order has no items")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Caller wraps the context
|
||||
if err := validateOrder(o); err != nil {
|
||||
return fmt.Errorf("process order: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Struct options pattern (for long parameter lists)
|
||||
|
||||
Go idiom for optional parameters:
|
||||
```go
|
||||
type ServerOption func(*Server)
|
||||
|
||||
func WithTimeout(d time.Duration) ServerOption {
|
||||
return func(s *Server) { s.timeout = d }
|
||||
}
|
||||
|
||||
func NewServer(addr string, opts ...ServerOption) *Server {
|
||||
s := &Server{addr: addr, timeout: 30 * time.Second}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
## Refactoring Commit Strategy
|
||||
|
||||
Refactoring commits should be separate from feature commits:
|
||||
- `refactor: extract validateOrder from processOrder`
|
||||
- `refactor: replace string email with Email value type`
|
||||
- `refactor: rename UserData to UserProfile`
|
||||
|
||||
Never combine: `feat: add discount + refactor: extract discount calculator`
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After each refactoring session:
|
||||
- [ ] `go test ./...` passes
|
||||
- [ ] `go test -race ./...` passes
|
||||
- [ ] No behavior change (same inputs produce same outputs)
|
||||
- [ ] Committed with a `refactor:` prefix commit message
|
||||
- [ ] Smells addressed are documented (if doing a review)
|
||||
|
||||
## Cross-References
|
||||
|
||||
- When applying Extract Function: load `clean-code` skill for naming conventions
|
||||
- When introducing interfaces: load `solid` skill for DIP and ISP guidance
|
||||
- When detecting smells: load `code-review` skill
|
||||
- Source: Martin Fowler, *Refactoring: Improving the Design of Existing Code* (2nd ed.)
|
||||
- Full catalog: https://refactoring.guru/refactoring
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: regulatory-risk-assessment
|
||||
description: Produce a structured regulatory risk assessment / risk-register entry for regulated-industry features (banking, payments, PSD2/PSR, DORA, AML). Use when filing a CAD issue that needs a Risk:LOW/MEDIUM/HIGH level, or designing features touching payments, auth, data storage, or external APIs ("what are the risks?", "compliance gate", "risk register").
|
||||
---
|
||||
|
||||
# regulatory-risk-assessment
|
||||
|
||||
Produce a structured regulatory risk assessment for a feature, component,
|
||||
or integration — generating a risk register entry that satisfies the
|
||||
audit requirements of regulated-industry clients (banking, finance,
|
||||
insurance, PSD2/PSR, DORA, AML/KYC contexts).
|
||||
|
||||
**Use when:**
|
||||
- Filing a Gitea issue dispatched via CAD (needs Risk: LOW/MEDIUM/HIGH)
|
||||
- Designing a feature touching payments, auth, data storage, external APIs
|
||||
- Preparing a client deliverable in a regulated industry
|
||||
- "what are the risks?" / "compliance gate" / "risk register" in session
|
||||
|
||||
**Do not use for:** routine refactoring, docs-only changes, internal
|
||||
tooling with no external data or user impact.
|
||||
|
||||
## What this skill produces
|
||||
|
||||
A docs/risk-register.md section with this schema:
|
||||
|
||||
### R-[DOMAIN]-[NN] — [Short risk title]
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Risk | What could go wrong (concrete, specific) |
|
||||
| Regulatory reference | Which obligation/regulation, if any |
|
||||
| Likelihood | H / M / L |
|
||||
| Impact | H / M / L |
|
||||
| Overall | H / M / L (highest of likelihood x impact) |
|
||||
| Mitigation | What we are doing about it |
|
||||
| Validation | Test name or Gitea issue number |
|
||||
| Status | open / mitigated / accepted |
|
||||
|
||||
Risk ID namespace:
|
||||
R-AUTH-NN authentication and authorization
|
||||
R-DATA-NN data storage, retention, privacy
|
||||
R-API-NN external API integration
|
||||
R-PAY-NN payment and financial transactions
|
||||
R-INFRA-NN infrastructure and availability
|
||||
R-AGENT-NN agentic / AI execution
|
||||
R-COMP-NN compliance and regulatory obligation
|
||||
|
||||
## Mechanics
|
||||
|
||||
Step 1 — Scope the assessment
|
||||
1. What external systems does this touch?
|
||||
2. What user data does it read, write, or transmit?
|
||||
3. What happens on silent failure? Loud failure?
|
||||
4. What is the blast radius of a worst-case bug?
|
||||
5. Is a regulation implicated?
|
||||
|
||||
Step 2 — Enumerate risks (common examples)
|
||||
Auth/OAuth: token theft, refresh failure, insufficient scope
|
||||
Email/Gmail: misclassification archives HUMAN thread, PII in logs
|
||||
Payment/PAIN.001: wrong amount, duplicate submission, missing field
|
||||
Agentic: irreversible action without approval, prompt injection, spirals
|
||||
Infra: single point of failure, secret in logs
|
||||
|
||||
Step 3 — Declare overall risk level
|
||||
Highest individual risk = feature overall level (LOW/MEDIUM/HIGH).
|
||||
This is the **Risk:** declaration in the CAD agent-ready issue contract.
|
||||
|
||||
Step 4 — Write validations
|
||||
Every mitigation needs a validation:
|
||||
- Specific test name (TestXxx)
|
||||
- Gitea issue number
|
||||
- Manual verification step with acceptance criteria
|
||||
Not acceptable: "will test later", "review manually"
|
||||
|
||||
Step 5 — Update docs/risk-register.md
|
||||
Append entries. Create if absent:
|
||||
|
||||
# Risk Register
|
||||
All entries follow R-[DOMAIN]-[NN] schema.
|
||||
See skills/regulatory-risk-assessment/SKILL.md for conventions.
|
||||
Last updated: [date]
|
||||
|
||||
## Integration with assessor-loop
|
||||
|
||||
For complex regulated-industry features (PSD2/PSR, DORA, AML), route to
|
||||
mathias/assessor-loop for deep obligation decomposition first.
|
||||
Use this skill standalone for internal tooling or general engineering risk.
|
||||
|
||||
## Integration with CAD
|
||||
|
||||
Every CAD-dispatched issue must include:
|
||||
**Risk:** LOW | MEDIUM | HIGH
|
||||
|
||||
CAD pre-flight (agentsquad#29) rejects issues without it.
|
||||
If genuinely no risks: declare LOW and note why.
|
||||
|
||||
## Example entry
|
||||
|
||||
### R-DATA-01 — Email misclassification archives a HUMAN thread
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Risk | LLM labels a real person's email as NOISE, causing auto-archive |
|
||||
| Regulatory reference | None (internal) |
|
||||
| Likelihood | M |
|
||||
| Impact | H |
|
||||
| Overall | H |
|
||||
| Mitigation | Phase 1 read-only; HUMAN class never auto-archived in any phase |
|
||||
| Validation | TestClassifier_HumanThreadNeverArchived; 1-week Phase 1 review |
|
||||
| Status | open |
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: secret-hygiene
|
||||
description: Prevent agent sessions from rendering credentials into the transcript, and recover safely when one leaks. Load BEFORE any command that touches credentials - kubectl exec/env/describe, kubectl get secret, whole-file SOPS decryption, token generation or rotation, envFrom inspection, or edits to encrypted config. Also load when a credential has already been exposed and a rotation chain is needed. Triggers - "rotate this token", "decrypt the config", "check the env var", "leaked credential", "did that print a secret", "why is this secret wrong".
|
||||
---
|
||||
|
||||
# Secret hygiene for agent sessions
|
||||
|
||||
Goal: stop the recurring leak pattern where a diagnostic command renders a
|
||||
SOPS-encrypted value or a Kubernetes Secret value into the session
|
||||
transcript, forcing a rotation.
|
||||
|
||||
**Confirmed past leaks** (see `mathias/infra#39` rotation umbrella):
|
||||
- `POSTGRES_PASSWORD` — `kubectl exec ... env` rendered envFrom value
|
||||
- `INFRA_MCP_TOKEN` — `sops -d ingress.enc.yaml` rendered the bearer in the configuration-snippet
|
||||
- `brain_app` postgres DSN — pre-fix ingestion pod log (hyperguild#20)
|
||||
|
||||
Three are pattern, not bug.
|
||||
|
||||
## Rules — never do these
|
||||
|
||||
| Forbidden | Why | Use instead |
|
||||
|---|---|---|
|
||||
| `kubectl exec <pod> -- env` (or `printenv`) | Renders every envFrom + valueFrom.secretKeyRef plaintext | `kubectl describe pod <pod>` — k8s redacts secretKeyRef in describe |
|
||||
| `sops -d <file>` to inspect | Whole-file decrypt dumps everything | `sops --extract '["key"]' <file>` for pinpoint reads |
|
||||
| `kubectl get secret ... -o jsonpath='{.data}'` (whole map) | Returns base64 of every key; trivial to b64-decode the lot | `... -o jsonpath='{.data.SPECIFIC_KEY}'` for the one key needed |
|
||||
| Echoing a generated token to verify length / content | Renders the new token into transcript, defeating the rotation | Verify functionally: `curl ... -H "Authorization: Bearer $(cat /tmp/tokfile)"` — never echo `$NEW_TOK` |
|
||||
| Storing a token in a shell variable then running `echo` / `set` / `env` | Same as above | Pipe through file (mode 600) or stdin to the next consumer |
|
||||
|
||||
## Rules — do these instead
|
||||
|
||||
- **Verify by side-effect, not by value.** If a token rotation should make
|
||||
the old token return 401 and the new one return 200, test those two
|
||||
HTTP responses. Never check `$OLD == $LEAKED` by printing it.
|
||||
- **Pinpoint SOPS reads.** When you need ONE key from a SOPS file, always
|
||||
`sops --extract '["..."]'`. The shape of the key path is safe to log;
|
||||
the value isn't.
|
||||
- **Stash tokens in `/dev/shm/<unique>` mode 600** and pass via filename,
|
||||
not env. `shred -u` after use. Avoid `/tmp` if the system has `/dev/shm`.
|
||||
- **If you must dump a secret to confirm wiring**, immediately rotate it
|
||||
in the same commit train. Treat the dump as "this is now compromised".
|
||||
|
||||
## Rotation chain pattern when a token is already exposed
|
||||
|
||||
If a token was leaked in a prior step of the same session, chain two
|
||||
rotations:
|
||||
|
||||
1. **T1**: replace the leaked value with a new one. The verification step
|
||||
may also leak T1 (acceptable risk).
|
||||
2. **T2**: replace T1 with a fresh value. Run this without any decrypt
|
||||
step that emits to stdout. The new value lives only in the encrypted
|
||||
file + the consumer's secret store (`~/.llmkeys`, the k8s Secret).
|
||||
|
||||
Example T2 path (Python, never decrypts to stdout):
|
||||
|
||||
```python
|
||||
new_t2 = subprocess.check_output(["openssl","rand","-hex","32"]).decode().strip()
|
||||
plaintext = subprocess.check_output(["sops","-d", path]).decode() # in-memory only
|
||||
patched = re.sub(r'Bearer [a-f0-9]+(?=")', f"Bearer {new_t2}", plaintext)
|
||||
# write patched via /dev/shm tmp + sops -e → write back
|
||||
# DO NOT print(patched), DO NOT print(new_t2)
|
||||
```
|
||||
|
||||
After T2 lands via GitOps + Flux, update consumers from the file stash
|
||||
without echoing.
|
||||
|
||||
## Detection ideas (not blocking — telemetry)
|
||||
|
||||
- Add a `bash` pre-hook in Claude harness that refuses obvious patterns
|
||||
(`kubectl exec * env*`, `sops -d *.enc.yaml`). Filed as a follow-up.
|
||||
- Run a periodic grep over `~/.claude/projects/*/sessions/*.jsonl` for
|
||||
high-entropy strings matching `[a-f0-9]{32,}` to catch new leak surface.
|
||||
|
||||
## Reference
|
||||
|
||||
- `gitea.d-ma.be/mathias/infra#39` — umbrella credential rotation issue with confirmed-leak roster.
|
||||
- `gitea.d-ma.be/mathias/infra#44` — POSTGRES_PASSWORD rotation procedure (template for future incidents).
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: session-retrospective
|
||||
description: Review a completed coding session (from brain/sessions/*.jsonl) and surface non-obvious learnings worth preserving. Use after a session ends, before context is lost.
|
||||
---
|
||||
|
||||
# Session Retrospective
|
||||
|
||||
## Overview
|
||||
|
||||
A session retrospective reads the structured log of a completed session — every skill invocation, what was attempted, what failed, what passed, how long it took — and identifies what is worth preserving in the brain for future sessions. The output is a small list of learnings, each with enough context that someone reading them cold understands why they matter.
|
||||
|
||||
**Core principle:** A retrospective that surfaces only obvious patterns is a retrospective that wastes the brain. Filter aggressively for non-obvious.
|
||||
|
||||
## Iron Laws
|
||||
|
||||
1. **Do not call `brain_write` from this skill.** Surface candidates only — the caller decides what to write.
|
||||
2. **Run `brain_query` first** to avoid surfacing learnings that are already in the brain.
|
||||
3. **Every Recommendation must generalize beyond this session.** Project-specific paths and identifiers belong in Context, not Recommendation.
|
||||
|
||||
## When to Use
|
||||
|
||||
- After a coding session ends, before the operator switches context
|
||||
- After a multi-day push on the same area, summarizing the whole stretch
|
||||
- When the brain is feeling stale on a project area — re-running retrospective on older sessions can re-surface forgotten context
|
||||
|
||||
**Do not use for:**
|
||||
- Sessions with no novel content (single-attempt passes, mechanical operations only)
|
||||
- Sessions less than ~30 minutes long, unless something dramatic happened in them
|
||||
|
||||
## Inputs
|
||||
|
||||
The session log is in JSON Lines format. Each project that uses the brain stores logs in its own `brain/sessions/` directory (for hyperguild, that is `~/dev/AI/hyperguild/brain/sessions/<session-id>.jsonl`). Each line is one event:
|
||||
|
||||
```json
|
||||
{"ts": "...", "phase": "tdd_red", "skill": "tdd", "outcome": "fail", "notes": "..."}
|
||||
```
|
||||
|
||||
Either pass the session ID explicitly, or default to the most recent file.
|
||||
|
||||
## What is Worth Preserving
|
||||
|
||||
- **Patterns that worked** — approaches clean enough to repeat next time, especially if non-obvious from the codebase alone
|
||||
- **Failures that revealed something** — bugs whose root cause taught you something about the codebase, the toolchain, or the approach
|
||||
- **Decisions made during the session** — architectural, structural, tooling choices that future-you would want to know the reasoning behind
|
||||
- **Anything that contradicts or extends established patterns** — these are the highest-value learnings
|
||||
|
||||
## What is NOT Worth Preserving
|
||||
|
||||
- Routine red-green-refactor cycles with no surprises
|
||||
- Single-attempt passes with no interesting context
|
||||
- Mechanical operations (file moves, renames, formatting fixes)
|
||||
- Failures whose cause was a typo
|
||||
- Anything that is already in `CLAUDE.md` or an existing memory file
|
||||
|
||||
**Decision procedure:** If the answer would be discoverable by reading CLAUDE.md, the codebase, or standard toolchain docs, drop it. If you are still unsure, ask: "would future-me, three months from now, want this to surface when querying the brain?" If no, drop it.
|
||||
|
||||
## Output Format
|
||||
|
||||
Respond in markdown. For each learning worth preserving:
|
||||
|
||||
```
|
||||
**Learning:** One sentence describing what was learned.
|
||||
**Context:** Why this session surfaced it — what made it non-obvious.
|
||||
**Recommendation:** What should be done differently or repeated going forward.
|
||||
```
|
||||
|
||||
End with a summary line: `N learnings worth writing to brain` or `No novel learnings in this session.`
|
||||
|
||||
The caller (operator or another agent) decides which learnings to actually write to the brain via `brain_write`. This skill does not write directly.
|
||||
|
||||
## Worked Example
|
||||
|
||||
Input session log shows: a 3-hour session implementing a JSON ingestion pipeline. The session log shows three failed attempts at handling escape characters in LLM-generated JSON, then a passing fix that introduced a `repairJSON` helper.
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
**Learning:** LLM-generated JSON containing markdown can produce invalid escape sequences (e.g. `\_` and `\#`) that break the standard JSON parser even when the structure is correct.
|
||||
**Context:** The first three TDD cycles failed because the test corpus included markdown-flavored content from the LLM and the parser rejected the entire payload. The fourth cycle introduced a `repairJSON` pre-pass that normalizes escapes before parsing. Without this context, future sessions will rediscover the same failure mode.
|
||||
**Recommendation:** When ingesting LLM output as JSON, route it through `repairJSON` before `json.Unmarshal`. Add this to the ingestion-pipeline reference doc.
|
||||
|
||||
**Learning:** Integration tests with `defer resp.Body.Close()` will fail CI's errcheck even when local `go test` is green.
|
||||
**Context:** Task 8 of the brain MCP migration shipped this pattern; CI caught it 30 minutes later. The codebase enforces `defer func() { _ = resp.Body.Close() }()` to satisfy errcheck.
|
||||
**Recommendation:** Implementer subagents must run `task check` (lint + test + vet) per task, not just `go test`. (Note: this learning has since been written to memory — a real run of this skill would now find it via `brain_query` and drop the candidate. Kept here as illustration.)
|
||||
|
||||
2 learnings worth writing to brain
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why It Fails |
|
||||
|---|---|
|
||||
| "Lots of tests passed today" | Outcome summary, not a learning. Brain queries on this return nothing useful. |
|
||||
| "Refactored the parser" | Activity log, not a learning. What was learned by doing it? |
|
||||
| Listing every failure as a learning | Most failures are typos or mechanical fixes. Filter to the ones with insight. |
|
||||
| Project-specific paths in the recommendation | Recommendations should generalize where possible — "ingestion pipelines" not "internal/ingest/v3/parser.go". |
|
||||
| Writing to brain directly from this skill | This skill surfaces candidates only. The caller decides what to write. |
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
The brain holds prior session learnings; checking it prevents this retrospective from re-surfacing what is already preserved.
|
||||
|
||||
**At retrospective start:**
|
||||
- Run `brain_query` with the session's main topic to surface recent related learnings. Avoid emitting a "learning" that is already in the brain.
|
||||
|
||||
**After retrospective output:**
|
||||
- This skill does NOT call `brain_write`. The caller does, selectively.
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||
|
||||
**At end of each phase:**
|
||||
- `session_log` with `{skill: "session-retrospective", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**Phases for this skill:** surfaced
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached (candidates were emitted).
|
||||
- `fail` — the phase's intended outcome was NOT reached (retrospective could not produce coherent candidates).
|
||||
- `skip` — phase was skipped intentionally (no novel learnings worth surfacing).
|
||||
|
||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
||||
|
||||
## Mode 2 Routing Note
|
||||
|
||||
Reading long session logs and pulling themes is high-volume, mechanical pattern-recognition work — a strong Mode 2 routing candidate. Until Plan 6 ships the routing pod, treat as Mode 1 only.
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Load `trainer` skill when you have more than ~5 candidates, or when the retrospective output feels too generous and needs a stricter quality gate before any of it goes into the brain. Trainer is the stricter cousin of this skill.
|
||||
- Pair with `brain_query` calls for prior session learnings on the same topic to avoid duplication.
|
||||
@@ -0,0 +1,259 @@
|
||||
---
|
||||
name: solid
|
||||
description: Apply SOLID design principles in Go. Use during architecture decisions, design reviews, and when adding new abstractions.
|
||||
---
|
||||
|
||||
# SOLID Principles
|
||||
|
||||
## Overview
|
||||
|
||||
SOLID helps structure software to be flexible, maintainable, and testable. These principles reduce coupling and increase cohesion.
|
||||
|
||||
In Go, several principles manifest differently than in classical OOP languages. Where Go idioms conflict with OOP-centric SOLID, **Go wins**. The tension is noted explicitly.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Principle | One-Liner | Question to Ask |
|
||||
|-----------|-----------|-----------------|
|
||||
| **S**RP | One reason to change | "Does this type have ONE reason to change?" |
|
||||
| **O**CP | Open for extension, closed for modification | "Can I extend without modifying?" |
|
||||
| **L**SP | Subtypes are substitutable | "Can any implementation replace another safely?" |
|
||||
| **I**SP | Small, focused interfaces | "Are clients forced to depend on unused methods?" |
|
||||
| **D**IP | Depend on abstractions | "Do I accept interfaces, not concrete types?" |
|
||||
|
||||
## S — Single Responsibility Principle
|
||||
|
||||
> "A class should have one, and only one, reason to change."
|
||||
|
||||
In Go: a **type** or **package** should have one reason to change. A package that handles both domain logic and database queries has two reasons to change.
|
||||
|
||||
**Detection:**
|
||||
- Can you describe the type's responsibility without using "and"?
|
||||
- Would different stakeholders (product, ops, DBA) request changes to different parts?
|
||||
|
||||
**Go example:**
|
||||
```go
|
||||
// Bad: multiple responsibilities
|
||||
type UserHandler struct {}
|
||||
|
||||
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ... }
|
||||
func (h *UserHandler) SaveToDatabase(u User) error { ... }
|
||||
func (h *UserHandler) SendWelcomeEmail(u User) error { ... }
|
||||
|
||||
// Good: single responsibility each
|
||||
type UserHandler struct {
|
||||
store UserStore
|
||||
mailer Mailer
|
||||
}
|
||||
|
||||
type PostgresUserStore struct { db *sql.DB }
|
||||
func (s *PostgresUserStore) Save(ctx context.Context, u User) error { ... }
|
||||
|
||||
type SMTPMailer struct { client *smtp.Client }
|
||||
func (m *SMTPMailer) SendWelcome(ctx context.Context, email string) error { ... }
|
||||
```
|
||||
|
||||
## O — Open/Closed Principle
|
||||
|
||||
> "Software entities should be open for extension but closed for modification."
|
||||
|
||||
In Go: add new behavior by implementing an interface or adding a new type, not by modifying existing code. The canonical Go pattern is a `switch` on a string type that needs a new case every time — replace it with an interface.
|
||||
|
||||
**Go example:**
|
||||
```go
|
||||
// Bad: must modify to add new payment method
|
||||
func ProcessPayment(method string, amount Money) error {
|
||||
switch method {
|
||||
case "stripe":
|
||||
return stripeCharge(amount)
|
||||
case "paypal":
|
||||
return paypalCharge(amount)
|
||||
// Must add cases here for every new method!
|
||||
}
|
||||
return errors.New("unknown payment method")
|
||||
}
|
||||
|
||||
// Good: open for extension via new types
|
||||
type PaymentProcessor interface {
|
||||
Charge(ctx context.Context, amount Money) error
|
||||
}
|
||||
|
||||
type StripeProcessor struct { client *stripe.Client }
|
||||
func (p *StripeProcessor) Charge(ctx context.Context, amount Money) error { ... }
|
||||
|
||||
type PayPalProcessor struct { client *paypal.Client }
|
||||
func (p *PayPalProcessor) Charge(ctx context.Context, amount Money) error { ... }
|
||||
|
||||
// Add new payment method: implement PaymentProcessor. No existing code changes.
|
||||
```
|
||||
|
||||
## L — Liskov Substitution Principle
|
||||
|
||||
> "Subtypes must be substitutable for their base types without altering program correctness."
|
||||
|
||||
In Go: **interfaces are structural**. Any type that implements the method set satisfies the interface. This means callers should be able to use any implementation interchangeably without knowing the difference.
|
||||
|
||||
The key question: does your implementation honor the **contract** (documented behavior, not just method signatures)?
|
||||
|
||||
**Go example:**
|
||||
```go
|
||||
// UserStore contract: Save persists a user and returns ErrDuplicate if email exists
|
||||
type UserStore interface {
|
||||
Save(ctx context.Context, u User) error
|
||||
GetByEmail(ctx context.Context, email string) (User, error)
|
||||
}
|
||||
|
||||
// Good: both implementations honor the contract
|
||||
type PostgresUserStore struct { ... }
|
||||
func (s *PostgresUserStore) Save(ctx context.Context, u User) error { ... }
|
||||
|
||||
type InMemoryUserStore struct { users map[string]User }
|
||||
func (s *InMemoryUserStore) Save(ctx context.Context, u User) error {
|
||||
if _, exists := s.users[u.Email]; exists {
|
||||
return ErrDuplicate // Must return ErrDuplicate, not some other error
|
||||
}
|
||||
s.users[u.Email] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bad: violates contract — callers of UserStore cannot substitute this
|
||||
type BrokenStore struct { ... }
|
||||
func (s *BrokenStore) Save(ctx context.Context, u User) error {
|
||||
panic("not implemented") // Violates contract
|
||||
}
|
||||
```
|
||||
|
||||
**Go tension:** Go has no inheritance, so "refused bequest" (subclass ignoring parent methods) doesn't apply. The LSP concern in Go is about interface implementations that partially satisfy the contract through panics or no-ops.
|
||||
|
||||
## I — Interface Segregation Principle
|
||||
|
||||
> "Clients should not be forced to depend on methods they do not use."
|
||||
|
||||
In Go: this is idiomatic. The `io.Reader`, `io.Writer`, and `io.Closer` pattern is the model — small, focused interfaces that can be composed when needed.
|
||||
|
||||
**Go example:**
|
||||
```go
|
||||
// Bad: fat interface — callers that only read must depend on write methods too
|
||||
type FileStore interface {
|
||||
Read(name string) ([]byte, error)
|
||||
Write(name string, data []byte) error
|
||||
Delete(name string) error
|
||||
List(dir string) ([]string, error)
|
||||
Stats(name string) (FileInfo, error)
|
||||
}
|
||||
|
||||
// Good: segregated interfaces — callers depend only on what they need
|
||||
type FileReader interface {
|
||||
Read(name string) ([]byte, error)
|
||||
}
|
||||
|
||||
type FileWriter interface {
|
||||
Write(name string, data []byte) error
|
||||
}
|
||||
|
||||
type FileDeleter interface {
|
||||
Delete(name string) error
|
||||
}
|
||||
|
||||
// Compose when needed
|
||||
type FileReadWriter interface {
|
||||
FileReader
|
||||
FileWriter
|
||||
}
|
||||
|
||||
// Handler that only reads: depends on FileReader only
|
||||
func NewReportHandler(store FileReader) *ReportHandler { ... }
|
||||
```
|
||||
|
||||
**Go idiom:** Define interfaces at the point of use, not at the point of implementation. The implementation package should not define the interface — the package that consumes it should.
|
||||
|
||||
```go
|
||||
// In the consumer package (handler)
|
||||
type UserStore interface {
|
||||
GetByID(ctx context.Context, id UserID) (User, error)
|
||||
}
|
||||
|
||||
// The postgres package doesn't need to know about this interface
|
||||
// It just implements the method, and Go's structural typing handles the rest
|
||||
```
|
||||
|
||||
## D — Dependency Inversion Principle
|
||||
|
||||
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
|
||||
|
||||
In Go: pass dependencies as interface parameters. Never instantiate concrete dependencies inside a function or type that contains business logic.
|
||||
|
||||
```go
|
||||
// Bad: high-level order service depends on low-level email implementation
|
||||
type OrderService struct {
|
||||
emailClient *sendgrid.Client // Locked to SendGrid
|
||||
db *sql.DB // Locked to PostgreSQL
|
||||
}
|
||||
|
||||
// Good: depends on abstractions
|
||||
type Mailer interface {
|
||||
Send(ctx context.Context, to, subject, body string) error
|
||||
}
|
||||
|
||||
type OrderRepository interface {
|
||||
Save(ctx context.Context, o Order) error
|
||||
GetByID(ctx context.Context, id OrderID) (Order, error)
|
||||
}
|
||||
|
||||
type OrderService struct {
|
||||
repo OrderRepository
|
||||
mailer Mailer
|
||||
}
|
||||
|
||||
func NewOrderService(repo OrderRepository, mailer Mailer) *OrderService {
|
||||
return &OrderService{repo: repo, mailer: mailer}
|
||||
}
|
||||
```
|
||||
|
||||
**The Dependency Rule:** Source code dependencies point **inward** toward domain logic, never outward toward infrastructure.
|
||||
|
||||
```
|
||||
HTTP handlers → Application services → Domain types
|
||||
Database layer → Application services → Domain types
|
||||
|
||||
Domain types know nothing about HTTP, SQL, or external APIs.
|
||||
```
|
||||
|
||||
## Applying SOLID at Architecture Level
|
||||
|
||||
| Principle | Package/Module Application |
|
||||
|-----------|---------------------------|
|
||||
| SRP | Each package has one clear purpose |
|
||||
| OCP | New features = new packages/types, not edits to existing |
|
||||
| LSP | All implementations of an interface are interchangeable |
|
||||
| ISP | Interfaces defined at point of use, as narrow as possible |
|
||||
| DIP | Domain packages import nothing from infrastructure packages |
|
||||
|
||||
## Go Tensions with OOP-centric SOLID
|
||||
|
||||
| OOP SOLID | Go reality |
|
||||
|-----------|-----------|
|
||||
| Inheritance hierarchies for OCP | Use interfaces + new types instead |
|
||||
| Abstract base classes for LSP | No inheritance; use interface contracts |
|
||||
| Explicit interface declarations | Interfaces are implicit; define where consumed |
|
||||
| "Program to an interface" as ritual | Only extract interface when you have 2+ implementations or need testability |
|
||||
|
||||
**Don't over-abstract.** A function that takes a `*sql.DB` directly is fine if there's only one implementation and it's never tested in isolation. Extract an interface when you need it.
|
||||
|
||||
## Red Flags
|
||||
|
||||
| Flag | Likely Violation |
|
||||
|------|-----------------|
|
||||
| Type that "handles X and Y and Z" | SRP |
|
||||
| Large `switch` on a type string | OCP |
|
||||
| Implementation that panics on some methods | LSP |
|
||||
| Interface with 10+ methods | ISP |
|
||||
| `new(ConcreteType)` inside business logic | DIP |
|
||||
| Package imports something from `infrastructure/` | DIP |
|
||||
|
||||
## References
|
||||
|
||||
- `references/solid-principles.md` — canonical SOLID reference with TypeScript examples
|
||||
- `references/go-adaptation.md` — this workspace's Go-specific rewrite of each principle
|
||||
- Load `clean-code` skill for naming and structure
|
||||
- Load `code-review` skill for detecting violations during review
|
||||
@@ -0,0 +1,318 @@
|
||||
# Software Architecture
|
||||
|
||||
## The Goal of Architecture
|
||||
|
||||
Enable the development team to:
|
||||
1. **Add** features with minimal friction
|
||||
2. **Change** existing features safely
|
||||
3. **Remove** features cleanly
|
||||
4. **Test** features in isolation
|
||||
5. **Deploy** independently when possible
|
||||
|
||||
## Architectural Principles
|
||||
|
||||
### 1. Vertical Boundaries (Features/Slices)
|
||||
|
||||
Organize by **feature**, not by technical layer.
|
||||
|
||||
```
|
||||
BAD: Layer-first
|
||||
src/
|
||||
controllers/
|
||||
UserController.ts
|
||||
OrderController.ts
|
||||
services/
|
||||
UserService.ts
|
||||
OrderService.ts
|
||||
repositories/
|
||||
UserRepository.ts
|
||||
OrderRepository.ts
|
||||
|
||||
GOOD: Feature-first
|
||||
src/
|
||||
users/
|
||||
UserController.ts
|
||||
UserService.ts
|
||||
UserRepository.ts
|
||||
orders/
|
||||
OrderController.ts
|
||||
OrderService.ts
|
||||
OrderRepository.ts
|
||||
```
|
||||
|
||||
**Why:** Changes to "users" feature stay in `users/`. High cohesion within features.
|
||||
|
||||
### 2. Horizontal Boundaries (Layers)
|
||||
|
||||
Separate concerns into layers with clear dependencies.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Presentation │ UI, Controllers, CLI
|
||||
├──────────────────────────────────────┤
|
||||
│ Application │ Use Cases, Orchestration
|
||||
├──────────────────────────────────────┤
|
||||
│ Domain │ Business Logic, Entities
|
||||
├──────────────────────────────────────┤
|
||||
│ Infrastructure │ Database, APIs, External
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. The Dependency Rule
|
||||
|
||||
**Dependencies point INWARD.**
|
||||
|
||||
```
|
||||
Infrastructure → Application → Domain
|
||||
↓ ↓ ↓
|
||||
(outer) (middle) (inner)
|
||||
```
|
||||
|
||||
- Inner layers know NOTHING about outer layers
|
||||
- Domain has zero dependencies on infrastructure
|
||||
- Use interfaces to invert dependencies
|
||||
|
||||
```typescript
|
||||
// Domain defines the interface (inner)
|
||||
interface UserRepository {
|
||||
save(user: User): Promise<void>;
|
||||
findById(id: UserId): Promise<User | null>;
|
||||
}
|
||||
|
||||
// Infrastructure implements it (outer)
|
||||
class PostgresUserRepository implements UserRepository {
|
||||
save(user: User): Promise<void> {
|
||||
// SQL here
|
||||
}
|
||||
}
|
||||
|
||||
// Domain service uses the interface
|
||||
class UserService {
|
||||
constructor(private repo: UserRepository) {} // Depends on abstraction
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Contracts
|
||||
|
||||
Interfaces define boundaries between components.
|
||||
|
||||
```typescript
|
||||
// The contract
|
||||
interface PaymentGateway {
|
||||
charge(amount: Money, card: CardDetails): Promise<ChargeResult>;
|
||||
refund(chargeId: string): Promise<RefundResult>;
|
||||
}
|
||||
|
||||
// Multiple implementations possible
|
||||
class StripeGateway implements PaymentGateway { }
|
||||
class PayPalGateway implements PaymentGateway { }
|
||||
class MockGateway implements PaymentGateway { } // For tests
|
||||
```
|
||||
|
||||
### 5. Cross-Cutting Concerns
|
||||
|
||||
Concerns that span multiple features: logging, auth, validation, error handling.
|
||||
|
||||
**Options:**
|
||||
- Middleware/interceptors
|
||||
- Decorators
|
||||
- Aspect-oriented approaches
|
||||
- Base classes (use sparingly)
|
||||
|
||||
```typescript
|
||||
// Middleware approach
|
||||
class LoggingMiddleware {
|
||||
handle(request: Request, next: Handler): Response {
|
||||
console.log(`Request: ${request.path}`);
|
||||
const response = next(request);
|
||||
console.log(`Response: ${response.status}`);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Conway's Law
|
||||
|
||||
> "Organizations design systems that mirror their communication structure."
|
||||
|
||||
**Implication:** Team structure affects architecture. Align both intentionally.
|
||||
|
||||
---
|
||||
|
||||
## Common Architectural Styles
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
Traditional layers: Presentation → Business → Persistence
|
||||
|
||||
**Pros:** Simple, well-understood
|
||||
**Cons:** Can become a "big ball of mud" without discipline
|
||||
|
||||
### Hexagonal Architecture (Ports & Adapters)
|
||||
|
||||
Domain at center, adapters around the edges.
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ HTTP Adapter │
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
┌─────────────────▼─────────────────┐
|
||||
│ DOMAIN │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ Business Logic │ │
|
||||
│ │ Use Cases │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────┬─────────────────┘
|
||||
│
|
||||
┌─────────▼───────────┐
|
||||
│ Database Adapter │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**Ports:** Interfaces defined by the domain
|
||||
**Adapters:** Implementations that connect to the outside world
|
||||
|
||||
### Clean Architecture
|
||||
|
||||
Similar to Hexagonal, with explicit layers:
|
||||
|
||||
1. **Entities** - Enterprise business rules
|
||||
2. **Use Cases** - Application business rules
|
||||
3. **Interface Adapters** - Controllers, Presenters, Gateways
|
||||
4. **Frameworks & Drivers** - Web, DB, External interfaces
|
||||
|
||||
---
|
||||
|
||||
## Feature-Driven Structure (Frontend)
|
||||
|
||||
```
|
||||
src/
|
||||
features/
|
||||
auth/
|
||||
components/
|
||||
LoginForm.tsx
|
||||
SignupForm.tsx
|
||||
hooks/
|
||||
useAuth.ts
|
||||
services/
|
||||
authService.ts
|
||||
types/
|
||||
auth.types.ts
|
||||
index.ts # Public API
|
||||
checkout/
|
||||
components/
|
||||
hooks/
|
||||
services/
|
||||
types/
|
||||
index.ts
|
||||
shared/
|
||||
components/ # Truly shared UI
|
||||
hooks/ # Truly shared hooks
|
||||
utils/ # Truly shared utilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature-Driven Structure (Backend)
|
||||
|
||||
```
|
||||
src/
|
||||
modules/
|
||||
users/
|
||||
domain/
|
||||
User.ts
|
||||
UserRepository.ts # Interface
|
||||
application/
|
||||
CreateUser.ts # Use case
|
||||
GetUser.ts # Use case
|
||||
infrastructure/
|
||||
PostgresUserRepo.ts
|
||||
presentation/
|
||||
UserController.ts
|
||||
UserDTO.ts
|
||||
orders/
|
||||
domain/
|
||||
application/
|
||||
infrastructure/
|
||||
presentation/
|
||||
shared/
|
||||
domain/ # Shared value objects
|
||||
infrastructure/ # Shared infra utilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Walking Skeleton
|
||||
|
||||
Start with a minimal end-to-end slice:
|
||||
|
||||
1. **Thinnest possible feature** that touches all layers
|
||||
2. **Deployable** from day one
|
||||
3. **Proves the architecture** works
|
||||
|
||||
Example walking skeleton for e-commerce:
|
||||
- User can view ONE product (hardcoded)
|
||||
- User can add it to cart
|
||||
- User can "checkout" (just logs)
|
||||
|
||||
From there, flesh out each feature fully.
|
||||
|
||||
---
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ E2E / Acceptance Tests │ Few, slow, high confidence
|
||||
├────────────────────────────────────────────┤
|
||||
│ Integration Tests │ Some, medium speed
|
||||
├────────────────────────────────────────────┤
|
||||
│ Unit Tests │ Many, fast, isolated
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Test by layer:**
|
||||
- **Domain:** Unit tests (most tests here)
|
||||
- **Application:** Integration tests with mocked infra
|
||||
- **Infrastructure:** Integration tests with real dependencies
|
||||
- **E2E:** Critical paths only
|
||||
|
||||
---
|
||||
|
||||
## Architecture Decision Records (ADRs)
|
||||
|
||||
Document significant decisions:
|
||||
|
||||
```markdown
|
||||
# ADR 001: Use PostgreSQL for persistence
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
We need a database. Options: PostgreSQL, MongoDB, MySQL
|
||||
|
||||
## Decision
|
||||
PostgreSQL for:
|
||||
- ACID compliance
|
||||
- Team familiarity
|
||||
- JSON support for flexibility
|
||||
|
||||
## Consequences
|
||||
- Need PostgreSQL expertise
|
||||
- Schema migrations required
|
||||
- Excellent query capabilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Red Flags in Architecture
|
||||
|
||||
- **Circular dependencies** between modules
|
||||
- **Domain depending on infrastructure**
|
||||
- **Framework code in business logic**
|
||||
- **No clear boundaries** between features
|
||||
- **Shared mutable state** across modules
|
||||
- **"Util" or "Common" packages** that grow forever
|
||||
- **Database schema driving domain model**
|
||||
@@ -0,0 +1,286 @@
|
||||
# Managing Complexity
|
||||
|
||||
## The Two Types of Complexity
|
||||
|
||||
### Essential Complexity
|
||||
Inherent to the problem domain. Cannot be removed, only managed.
|
||||
- Business rules
|
||||
- Domain logic
|
||||
- User requirements
|
||||
|
||||
### Accidental Complexity
|
||||
Introduced by our solutions. CAN and SHOULD be minimized.
|
||||
- Poor abstractions
|
||||
- Unnecessary indirection
|
||||
- Framework ceremony
|
||||
- Technical debt
|
||||
|
||||
**Goal: Minimize accidental complexity while clearly expressing essential complexity.**
|
||||
|
||||
---
|
||||
|
||||
## Detecting Complexity
|
||||
|
||||
### 1. Change Amplification
|
||||
Small changes require touching many files.
|
||||
|
||||
**Symptom:** "To add this field, I need to update 15 files."
|
||||
|
||||
**Cause:** Scattered responsibilities, poor abstraction boundaries.
|
||||
|
||||
### 2. Cognitive Load
|
||||
Code is hard to understand, requires holding too much in memory.
|
||||
|
||||
**Symptom:** "I need to understand 10 other classes to understand this one."
|
||||
|
||||
**Cause:** Tight coupling, hidden dependencies, unclear naming.
|
||||
|
||||
### 3. Unknown Unknowns
|
||||
Behavior is surprising, side effects are hidden.
|
||||
|
||||
**Symptom:** "I changed this, and something completely unrelated broke."
|
||||
|
||||
**Cause:** Global state, hidden dependencies, implicit contracts.
|
||||
|
||||
---
|
||||
|
||||
## The XP Values for Fighting Complexity
|
||||
|
||||
From Extreme Programming:
|
||||
|
||||
### 1. Communication
|
||||
Code should communicate clearly. Names, structure, tests all contribute.
|
||||
|
||||
### 2. Simplicity
|
||||
Do the simplest thing that could possibly work.
|
||||
|
||||
### 3. Feedback
|
||||
Fast feedback loops catch complexity early. TDD, CI, code review.
|
||||
|
||||
### 4. Courage
|
||||
Refactor aggressively. Don't let complexity accumulate.
|
||||
|
||||
### 5. Respect
|
||||
Respect future readers (including yourself). Write for humans first.
|
||||
|
||||
---
|
||||
|
||||
## KISS - Keep It Simple, Silly
|
||||
|
||||
> "The simplest solution that works is usually the best."
|
||||
|
||||
### How to Apply:
|
||||
1. Start with the obvious solution
|
||||
2. Only add complexity when REQUIRED
|
||||
3. Prefer boring, well-understood approaches
|
||||
4. Question every abstraction
|
||||
|
||||
```typescript
|
||||
// Over-engineered
|
||||
class UserServiceFactoryProvider {
|
||||
private static instance: UserServiceFactoryProvider;
|
||||
|
||||
static getInstance(): UserServiceFactoryProvider { ... }
|
||||
createFactory(): UserServiceFactory { ... }
|
||||
}
|
||||
|
||||
// KISS
|
||||
class UserService {
|
||||
getUser(id: string): User { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## YAGNI - You Aren't Gonna Need It
|
||||
|
||||
> "Don't build features until they're actually needed."
|
||||
|
||||
### Warning Signs:
|
||||
- "We might need this later"
|
||||
- "It would be nice to have"
|
||||
- "Just in case"
|
||||
- "For future extensibility"
|
||||
|
||||
### The Cost of YAGNI Violations:
|
||||
1. **Development time** - Building unused features
|
||||
2. **Maintenance burden** - Code that must be maintained
|
||||
3. **Cognitive load** - More to understand
|
||||
4. **Wrong abstraction** - Guessing future needs incorrectly
|
||||
|
||||
```typescript
|
||||
// YAGNI violation: Building for hypothetical needs
|
||||
class User {
|
||||
// "We might need these someday"
|
||||
middleName?: string;
|
||||
secondaryEmail?: string;
|
||||
faxNumber?: string;
|
||||
linkedinProfile?: string;
|
||||
twitterHandle?: string;
|
||||
}
|
||||
|
||||
// YAGNI: Only what's needed NOW
|
||||
class User {
|
||||
name: string;
|
||||
email: Email;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DRY - Don't Repeat Yourself (with The Rule of Three)
|
||||
|
||||
> "Every piece of knowledge should have a single, unambiguous representation."
|
||||
|
||||
### BUT: The Rule of Three
|
||||
|
||||
**Don't extract duplication until you see it THREE times.**
|
||||
|
||||
Why? The wrong abstraction is worse than duplication.
|
||||
|
||||
```
|
||||
Duplication #1 → Leave it
|
||||
Duplication #2 → Note it, leave it
|
||||
Duplication #3 → NOW extract it
|
||||
```
|
||||
|
||||
### Example:
|
||||
```typescript
|
||||
// First time - leave it
|
||||
function processUserOrder(order) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
}
|
||||
|
||||
// Second time - note the similarity, but leave it
|
||||
function processGuestOrder(order) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
sendGuestEmail(order);
|
||||
}
|
||||
|
||||
// Third time - NOW extract
|
||||
function processCorporateOrder(order) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
applyCorporateDiscount(order);
|
||||
}
|
||||
|
||||
// After three, extract the common parts
|
||||
function processOrder(order: Order, postProcessing: (o: Order) => void) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
postProcessing(order);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Separation of Concerns
|
||||
|
||||
> "Each module should address a single concern."
|
||||
|
||||
### Concerns to Separate:
|
||||
- **Business logic** vs **Infrastructure**
|
||||
- **What** (policy) vs **How** (mechanism)
|
||||
- **Input** vs **Processing** vs **Output**
|
||||
- **Data** vs **Behavior**
|
||||
|
||||
### Example:
|
||||
```typescript
|
||||
// BAD: Mixed concerns
|
||||
class OrderProcessor {
|
||||
process(order: Order) {
|
||||
// Validation
|
||||
if (!order.items.length) throw new Error('Empty');
|
||||
|
||||
// Business logic
|
||||
let total = 0;
|
||||
for (const item of order.items) {
|
||||
total += item.price * item.quantity;
|
||||
}
|
||||
|
||||
// Persistence
|
||||
const db = new Database();
|
||||
db.query(`INSERT INTO orders...`);
|
||||
|
||||
// Notification
|
||||
const email = new EmailClient();
|
||||
email.send(order.customer.email, 'Order confirmed');
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Separated concerns
|
||||
class OrderProcessor {
|
||||
constructor(
|
||||
private validator: OrderValidator,
|
||||
private calculator: OrderCalculator,
|
||||
private repository: OrderRepository,
|
||||
private notifier: OrderNotifier
|
||||
) {}
|
||||
|
||||
process(order: Order): ProcessResult {
|
||||
this.validator.validate(order);
|
||||
const total = this.calculator.calculateTotal(order);
|
||||
const savedOrder = this.repository.save(order);
|
||||
this.notifier.notifyConfirmation(savedOrder);
|
||||
return ProcessResult.success(savedOrder);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Managing Technical Debt
|
||||
|
||||
### Types of Technical Debt:
|
||||
1. **Deliberate** - Conscious trade-off for speed
|
||||
2. **Accidental** - Mistakes, lack of knowledge
|
||||
3. **Bit rot** - Code degrades over time
|
||||
|
||||
### The Boy Scout Rule:
|
||||
> "Leave the code better than you found it."
|
||||
|
||||
Every time you touch code:
|
||||
- Improve one small thing
|
||||
- Fix one naming issue
|
||||
- Extract one method
|
||||
- Add one missing test
|
||||
|
||||
### When to Pay Down Debt:
|
||||
- When it's in your path (you're already there)
|
||||
- When it's blocking new features
|
||||
- When it's causing bugs
|
||||
- During dedicated refactoring time
|
||||
|
||||
### When NOT to Refactor:
|
||||
- Code that works and won't change
|
||||
- Code being replaced soon
|
||||
- When you don't have tests
|
||||
|
||||
---
|
||||
|
||||
## The Four Elements of Simple Design
|
||||
|
||||
In priority order (from XP):
|
||||
|
||||
1. **Runs all the tests**
|
||||
- If it doesn't work, nothing else matters
|
||||
|
||||
2. **Expresses intent**
|
||||
- Clear names, obvious structure
|
||||
- Code tells the story
|
||||
|
||||
3. **No duplication**
|
||||
- DRY (but Rule of Three)
|
||||
- Single source of truth
|
||||
|
||||
4. **Minimal**
|
||||
- Fewest classes and methods possible
|
||||
- Remove anything unnecessary
|
||||
|
||||
If these four are true, the design is simple enough.
|
||||
@@ -0,0 +1,504 @@
|
||||
# Design Patterns
|
||||
|
||||
## What Are Design Patterns?
|
||||
|
||||
Reusable solutions to common design problems. A shared vocabulary for discussing design.
|
||||
|
||||
## WARNING: Don't Force Patterns
|
||||
|
||||
> "Let patterns emerge from refactoring, don't force them upfront."
|
||||
|
||||
Patterns should solve problems you HAVE, not problems you MIGHT have.
|
||||
|
||||
## When to Use Patterns
|
||||
|
||||
1. **You recognize the problem** - You've seen it before
|
||||
2. **The pattern fits** - Not forcing it
|
||||
3. **It simplifies** - Doesn't add unnecessary complexity
|
||||
4. **Team understands it** - Shared knowledge
|
||||
|
||||
---
|
||||
|
||||
## Creational Patterns
|
||||
|
||||
### Singleton
|
||||
|
||||
**Purpose:** Ensure only one instance exists.
|
||||
|
||||
**When to use:** Global configuration, connection pools, logging.
|
||||
|
||||
**Warning:** Often overused. Consider dependency injection instead.
|
||||
|
||||
```typescript
|
||||
class Logger {
|
||||
private static instance: Logger;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger();
|
||||
}
|
||||
return Logger.instance;
|
||||
}
|
||||
|
||||
log(message: string): void { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Factory
|
||||
|
||||
**Purpose:** Create objects without specifying exact class.
|
||||
|
||||
**When to use:** Object creation logic is complex, or varies by type.
|
||||
|
||||
```typescript
|
||||
interface Notification {
|
||||
send(message: string): void;
|
||||
}
|
||||
|
||||
class EmailNotification implements Notification { ... }
|
||||
class SMSNotification implements Notification { ... }
|
||||
class PushNotification implements Notification { ... }
|
||||
|
||||
class NotificationFactory {
|
||||
create(type: 'email' | 'sms' | 'push'): Notification {
|
||||
switch (type) {
|
||||
case 'email': return new EmailNotification();
|
||||
case 'sms': return new SMSNotification();
|
||||
case 'push': return new PushNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Builder
|
||||
|
||||
**Purpose:** Construct complex objects step by step.
|
||||
|
||||
**When to use:** Objects with many optional parameters, test data creation.
|
||||
|
||||
```typescript
|
||||
class UserBuilder {
|
||||
private user: Partial<User> = {};
|
||||
|
||||
withName(name: string): UserBuilder {
|
||||
this.user.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
withEmail(email: string): UserBuilder {
|
||||
this.user.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
withAge(age: number): UserBuilder {
|
||||
this.user.age = age;
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): User {
|
||||
return new User(
|
||||
this.user.name!,
|
||||
this.user.email!,
|
||||
this.user.age
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const user = new UserBuilder()
|
||||
.withName('Alice')
|
||||
.withEmail('alice@example.com')
|
||||
.build();
|
||||
```
|
||||
|
||||
### Prototype
|
||||
|
||||
**Purpose:** Create new objects by cloning existing ones.
|
||||
|
||||
**When to use:** Object creation is expensive, or you need copies with slight variations.
|
||||
|
||||
```typescript
|
||||
interface Prototype {
|
||||
clone(): Prototype;
|
||||
}
|
||||
|
||||
class Document implements Prototype {
|
||||
constructor(
|
||||
public title: string,
|
||||
public content: string,
|
||||
public metadata: Metadata
|
||||
) {}
|
||||
|
||||
clone(): Document {
|
||||
return new Document(
|
||||
this.title,
|
||||
this.content,
|
||||
{ ...this.metadata }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structural Patterns
|
||||
|
||||
### Adapter
|
||||
|
||||
**Purpose:** Make incompatible interfaces work together.
|
||||
|
||||
**When to use:** Integrating third-party libraries, legacy code.
|
||||
|
||||
```typescript
|
||||
// Third-party library with different interface
|
||||
class OldPaymentAPI {
|
||||
makePayment(cents: number): boolean { ... }
|
||||
}
|
||||
|
||||
// Our interface
|
||||
interface PaymentGateway {
|
||||
charge(amount: Money): ChargeResult;
|
||||
}
|
||||
|
||||
// Adapter
|
||||
class OldPaymentAdapter implements PaymentGateway {
|
||||
constructor(private oldAPI: OldPaymentAPI) {}
|
||||
|
||||
charge(amount: Money): ChargeResult {
|
||||
const cents = amount.toCents();
|
||||
const success = this.oldAPI.makePayment(cents);
|
||||
return success ? ChargeResult.success() : ChargeResult.failed();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Decorator
|
||||
|
||||
**Purpose:** Add behavior to objects dynamically.
|
||||
|
||||
**When to use:** Adding features without modifying existing code.
|
||||
|
||||
```typescript
|
||||
interface Notifier {
|
||||
send(message: string): void;
|
||||
}
|
||||
|
||||
class EmailNotifier implements Notifier {
|
||||
send(message: string): void {
|
||||
console.log(`Email: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Decorators
|
||||
class SMSDecorator implements Notifier {
|
||||
constructor(private wrapped: Notifier) {}
|
||||
|
||||
send(message: string): void {
|
||||
this.wrapped.send(message);
|
||||
console.log(`SMS: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
class SlackDecorator implements Notifier {
|
||||
constructor(private wrapped: Notifier) {}
|
||||
|
||||
send(message: string): void {
|
||||
this.wrapped.send(message);
|
||||
console.log(`Slack: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - compose behaviors
|
||||
const notifier = new SlackDecorator(
|
||||
new SMSDecorator(
|
||||
new EmailNotifier()
|
||||
)
|
||||
);
|
||||
notifier.send('Alert!'); // Sends to all three
|
||||
```
|
||||
|
||||
### Proxy
|
||||
|
||||
**Purpose:** Control access to an object.
|
||||
|
||||
**When to use:** Lazy loading, access control, logging, caching.
|
||||
|
||||
```typescript
|
||||
interface Image {
|
||||
display(): void;
|
||||
}
|
||||
|
||||
class RealImage implements Image {
|
||||
constructor(private filename: string) {
|
||||
this.loadFromDisk(); // Expensive
|
||||
}
|
||||
|
||||
private loadFromDisk(): void { ... }
|
||||
|
||||
display(): void { ... }
|
||||
}
|
||||
|
||||
// Lazy loading proxy
|
||||
class ImageProxy implements Image {
|
||||
private realImage: RealImage | null = null;
|
||||
|
||||
constructor(private filename: string) {}
|
||||
|
||||
display(): void {
|
||||
if (!this.realImage) {
|
||||
this.realImage = new RealImage(this.filename);
|
||||
}
|
||||
this.realImage.display();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Composite
|
||||
|
||||
**Purpose:** Treat individual objects and compositions uniformly.
|
||||
|
||||
**When to use:** Tree structures, hierarchies (files/folders, UI components).
|
||||
|
||||
```typescript
|
||||
interface Component {
|
||||
getPrice(): number;
|
||||
}
|
||||
|
||||
class Product implements Component {
|
||||
constructor(private price: number) {}
|
||||
|
||||
getPrice(): number {
|
||||
return this.price;
|
||||
}
|
||||
}
|
||||
|
||||
class Box implements Component {
|
||||
private children: Component[] = [];
|
||||
|
||||
add(component: Component): void {
|
||||
this.children.push(component);
|
||||
}
|
||||
|
||||
getPrice(): number {
|
||||
return this.children.reduce(
|
||||
(sum, child) => sum + child.getPrice(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const smallBox = new Box();
|
||||
smallBox.add(new Product(10));
|
||||
smallBox.add(new Product(20));
|
||||
|
||||
const bigBox = new Box();
|
||||
bigBox.add(smallBox);
|
||||
bigBox.add(new Product(50));
|
||||
|
||||
console.log(bigBox.getPrice()); // 80
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Behavioral Patterns
|
||||
|
||||
### Strategy
|
||||
|
||||
**Purpose:** Define a family of algorithms, make them interchangeable.
|
||||
|
||||
**When to use:** Multiple ways to do something, switchable at runtime.
|
||||
|
||||
```typescript
|
||||
interface PricingStrategy {
|
||||
calculate(basePrice: number): number;
|
||||
}
|
||||
|
||||
class RegularPricing implements PricingStrategy {
|
||||
calculate(basePrice: number): number {
|
||||
return basePrice;
|
||||
}
|
||||
}
|
||||
|
||||
class PremiumDiscount implements PricingStrategy {
|
||||
calculate(basePrice: number): number {
|
||||
return basePrice * 0.8; // 20% off
|
||||
}
|
||||
}
|
||||
|
||||
class BlackFriday implements PricingStrategy {
|
||||
calculate(basePrice: number): number {
|
||||
return basePrice * 0.5; // 50% off
|
||||
}
|
||||
}
|
||||
|
||||
class ShoppingCart {
|
||||
constructor(private pricing: PricingStrategy) {}
|
||||
|
||||
calculateTotal(items: Item[]): number {
|
||||
const base = items.reduce((sum, i) => sum + i.price, 0);
|
||||
return this.pricing.calculate(base);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Observer
|
||||
|
||||
**Purpose:** Notify multiple objects about state changes.
|
||||
|
||||
**When to use:** Event systems, pub/sub, reactive updates.
|
||||
|
||||
```typescript
|
||||
interface Observer {
|
||||
update(event: Event): void;
|
||||
}
|
||||
|
||||
class EventEmitter {
|
||||
private observers: Observer[] = [];
|
||||
|
||||
subscribe(observer: Observer): void {
|
||||
this.observers.push(observer);
|
||||
}
|
||||
|
||||
unsubscribe(observer: Observer): void {
|
||||
this.observers = this.observers.filter(o => o !== observer);
|
||||
}
|
||||
|
||||
notify(event: Event): void {
|
||||
this.observers.forEach(o => o.update(event));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
class OrderService extends EventEmitter {
|
||||
placeOrder(order: Order): void {
|
||||
// Process order...
|
||||
this.notify({ type: 'ORDER_PLACED', order });
|
||||
}
|
||||
}
|
||||
|
||||
class EmailService implements Observer {
|
||||
update(event: Event): void {
|
||||
if (event.type === 'ORDER_PLACED') {
|
||||
this.sendConfirmation(event.order);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Template Method
|
||||
|
||||
**Purpose:** Define algorithm skeleton, let subclasses override steps.
|
||||
|
||||
**When to use:** Common algorithm with varying steps.
|
||||
|
||||
```typescript
|
||||
abstract class DataExporter {
|
||||
// Template method - defines the algorithm
|
||||
export(data: Data[]): void {
|
||||
this.validate(data);
|
||||
const formatted = this.format(data);
|
||||
this.write(formatted);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
// Common steps
|
||||
private validate(data: Data[]): void { ... }
|
||||
private notify(): void { ... }
|
||||
|
||||
// Steps to override
|
||||
protected abstract format(data: Data[]): string;
|
||||
protected abstract write(content: string): void;
|
||||
}
|
||||
|
||||
class CSVExporter extends DataExporter {
|
||||
protected format(data: Data[]): string {
|
||||
return data.map(d => d.toCSV()).join('\n');
|
||||
}
|
||||
|
||||
protected write(content: string): void {
|
||||
fs.writeFileSync('export.csv', content);
|
||||
}
|
||||
}
|
||||
|
||||
class JSONExporter extends DataExporter {
|
||||
protected format(data: Data[]): string {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
protected write(content: string): void {
|
||||
fs.writeFileSync('export.json', content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command
|
||||
|
||||
**Purpose:** Encapsulate a request as an object.
|
||||
|
||||
**When to use:** Undo/redo, queuing, logging actions.
|
||||
|
||||
```typescript
|
||||
interface Command {
|
||||
execute(): void;
|
||||
undo(): void;
|
||||
}
|
||||
|
||||
class AddItemCommand implements Command {
|
||||
constructor(
|
||||
private cart: Cart,
|
||||
private item: Item
|
||||
) {}
|
||||
|
||||
execute(): void {
|
||||
this.cart.add(this.item);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
this.cart.remove(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
class CommandHistory {
|
||||
private history: Command[] = [];
|
||||
|
||||
execute(command: Command): void {
|
||||
command.execute();
|
||||
this.history.push(command);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const command = this.history.pop();
|
||||
command?.undo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern Awareness
|
||||
|
||||
### The Four-Dimensional Lens
|
||||
|
||||
When analyzing new code/libraries, ask:
|
||||
|
||||
1. **What problem does it solve?** (Creational, Structural, Behavioral)
|
||||
2. **What scope?** (Object-level, Class-level, System-level)
|
||||
3. **When is it applied?** (Compile-time, Runtime)
|
||||
4. **How coupled?** (Tight, Loose)
|
||||
|
||||
This helps recognize patterns even in unfamiliar code.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Problem | Solution |
|
||||
|--------------|---------|----------|
|
||||
| **God Object** | Class does everything | Split by responsibility |
|
||||
| **Spaghetti Code** | Tangled, no structure | Refactor to layers |
|
||||
| **Golden Hammer** | Using one pattern for everything | Match pattern to problem |
|
||||
| **Premature Optimization** | Optimizing before needed | YAGNI, profile first |
|
||||
| **Copy-Paste Programming** | Duplication | Extract, Rule of Three |
|
||||
@@ -0,0 +1,317 @@
|
||||
# SOLID Principles: Go Adaptation
|
||||
|
||||
This document rewrites each SOLID principle with idiomatic Go examples. Where Go idioms conflict with OOP-centric formulations, Go wins. Tension is noted.
|
||||
|
||||
## S — Single Responsibility Principle in Go
|
||||
|
||||
**OOP formulation:** A class should have one reason to change.
|
||||
|
||||
**Go formulation:** A type or package should have one reason to change. Responsibility maps to the unit of deployment (package), not just types.
|
||||
|
||||
### Go example
|
||||
|
||||
```go
|
||||
// Bad: Order type mixes domain, persistence, and notification
|
||||
package order
|
||||
|
||||
type Order struct {
|
||||
ID string
|
||||
Items []Item
|
||||
Customer Customer
|
||||
}
|
||||
|
||||
func (o *Order) Save(db *sql.DB) error { ... } // persistence concern
|
||||
func (o *Order) SendReceipt(smtp *mail.Client) error { ... } // notification concern
|
||||
func (o *Order) CalculateTotal() Money { ... } // domain concern (correct)
|
||||
|
||||
// Good: each type/package has one reason to change
|
||||
package order
|
||||
|
||||
// Domain type: only reason to change = business rules change
|
||||
type Order struct {
|
||||
ID OrderID
|
||||
Items []Item
|
||||
Customer Customer
|
||||
}
|
||||
|
||||
func (o Order) Total() Money {
|
||||
total := Money{}
|
||||
for _, item := range o.Items {
|
||||
total = total.Add(item.Price.Multiply(item.Quantity))
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
package orderstore
|
||||
|
||||
// Persistence: only reason to change = storage mechanism changes
|
||||
type Store struct { db *sql.DB }
|
||||
|
||||
func (s *Store) Save(ctx context.Context, o order.Order) error { ... }
|
||||
|
||||
package ordernotify
|
||||
|
||||
// Notification: only reason to change = notification channel changes
|
||||
type Notifier struct { mailer Mailer }
|
||||
|
||||
func (n *Notifier) SendReceipt(ctx context.Context, o order.Order) error { ... }
|
||||
```
|
||||
|
||||
### Package-level SRP
|
||||
|
||||
Package names should be nouns that describe one concept:
|
||||
- `store`, `cache`, `handler`, `validator` — single responsibility
|
||||
- `util`, `common`, `misc`, `helpers` — SRP violation waiting to happen
|
||||
|
||||
---
|
||||
|
||||
## O — Open/Closed Principle in Go
|
||||
|
||||
**OOP formulation:** Open for extension via subclassing, closed for modification.
|
||||
|
||||
**Go formulation:** Open for extension by implementing an interface or adding new types; closed for modification of existing types. No subclassing needed.
|
||||
|
||||
### Go example
|
||||
|
||||
```go
|
||||
// Bad: adding a new discount type requires modifying existing code
|
||||
func ApplyDiscount(order *Order, discountType string) Money {
|
||||
switch discountType {
|
||||
case "percentage":
|
||||
return order.Total().Multiply(0.9)
|
||||
case "fixed":
|
||||
return order.Total().Subtract(Money{Amount: 10})
|
||||
// Must add case here every time
|
||||
}
|
||||
return order.Total()
|
||||
}
|
||||
|
||||
// Good: add new discount types by implementing the interface
|
||||
type Discounter interface {
|
||||
Apply(total Money) Money
|
||||
}
|
||||
|
||||
type PercentageDiscount struct{ Percent float64 }
|
||||
func (d PercentageDiscount) Apply(total Money) Money {
|
||||
return total.Multiply(1 - d.Percent/100)
|
||||
}
|
||||
|
||||
type FixedDiscount struct{ Amount Money }
|
||||
func (d FixedDiscount) Apply(total Money) Money {
|
||||
return total.Subtract(d.Amount)
|
||||
}
|
||||
|
||||
// Adding SeniorDiscount requires zero changes to existing code
|
||||
type SeniorDiscount struct{}
|
||||
func (d SeniorDiscount) Apply(total Money) Money {
|
||||
return total.Multiply(0.85)
|
||||
}
|
||||
|
||||
func ApplyDiscount(order *Order, d Discounter) Money {
|
||||
return d.Apply(order.Total())
|
||||
}
|
||||
```
|
||||
|
||||
### Go tension
|
||||
|
||||
Go has no inheritance, so "closed for modification" is natural — you can't subclass a concrete type to override behavior. The extension point is always an interface. If you're adding switch cases to handle new types, that's the signal to introduce an interface.
|
||||
|
||||
---
|
||||
|
||||
## L — Liskov Substitution Principle in Go
|
||||
|
||||
**OOP formulation:** Subtypes must be substitutable for their base types.
|
||||
|
||||
**Go formulation:** Any implementation of an interface must honor the interface's documented contract, not just its method signatures. Go's structural typing means LSP is enforced by convention and documentation, not the compiler.
|
||||
|
||||
### Contract documentation
|
||||
|
||||
```go
|
||||
// UserStore: all implementations must honor this contract:
|
||||
// - Save: persists user, returns ErrDuplicateEmail if email already exists
|
||||
// - GetByEmail: returns ErrNotFound if user does not exist
|
||||
// - Both methods must be safe for concurrent use
|
||||
type UserStore interface {
|
||||
Save(ctx context.Context, u User) error
|
||||
GetByEmail(ctx context.Context, email string) (User, error)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrDuplicateEmail = errors.New("duplicate email")
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
// PostgresUserStore: honors contract
|
||||
type PostgresUserStore struct { db *sql.DB }
|
||||
func (s *PostgresUserStore) Save(ctx context.Context, u User) error {
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO users ...", u.Email)
|
||||
if isUniqueViolation(err) {
|
||||
return ErrDuplicateEmail // Returns documented error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// InMemoryUserStore: honors contract (for 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 // Same documented error
|
||||
}
|
||||
s.users[u.Email] = u
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### LSP violations to watch for
|
||||
|
||||
```go
|
||||
// Bad: violates contract — does not return ErrDuplicateEmail
|
||||
type CachedUserStore struct { ... }
|
||||
func (s *CachedUserStore) Save(ctx context.Context, u User) error {
|
||||
return errors.New("cache: duplicate key") // Different error type breaks callers
|
||||
}
|
||||
|
||||
// Bad: panics on some inputs — violates contract
|
||||
type LazyUserStore struct { ... }
|
||||
func (s *LazyUserStore) GetByEmail(ctx context.Context, email string) (User, error) {
|
||||
panic("not implemented yet") // Violates LSP
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## I — Interface Segregation Principle in Go
|
||||
|
||||
**OOP formulation:** Clients should not depend on methods they do not use.
|
||||
|
||||
**Go formulation:** Define the narrowest interface possible at the point of use. Go's structural typing makes this natural — you don't need the implementation to declare what it implements.
|
||||
|
||||
### io.Reader as the canonical example
|
||||
|
||||
```go
|
||||
// io.Reader is a single-method interface
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// Any type with Read() satisfies this — os.File, bytes.Buffer, net.Conn, etc.
|
||||
// Callers that only need to read accept Reader, not a fat interface
|
||||
func parseConfig(r io.Reader) (Config, error) { ... }
|
||||
```
|
||||
|
||||
### Define interfaces where you consume them
|
||||
|
||||
```go
|
||||
// package report — only needs to read invoices
|
||||
package report
|
||||
|
||||
// Define the interface here, at the point of use — not in the invoice package
|
||||
type InvoiceReader interface {
|
||||
GetByID(ctx context.Context, id InvoiceID) (Invoice, error)
|
||||
ListByCustomer(ctx context.Context, customerID CustomerID) ([]Invoice, error)
|
||||
}
|
||||
|
||||
func NewReporter(invoices InvoiceReader) *Reporter { ... }
|
||||
|
||||
// The invoice package's PostgresStore has 10+ methods
|
||||
// This interface only exposes what the reporter needs
|
||||
// Adding new methods to PostgresStore never forces changes to the reporter
|
||||
```
|
||||
|
||||
### Composing interfaces
|
||||
|
||||
```go
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// Compose at the call site, not in the definition
|
||||
type ReadWriter interface {
|
||||
Reader
|
||||
Writer
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D — Dependency Inversion Principle in Go
|
||||
|
||||
**OOP formulation:** High-level modules should not depend on low-level modules. Both depend on abstractions.
|
||||
|
||||
**Go formulation:** Accept interfaces, not concrete types. Domain packages import nothing from infrastructure packages. Infrastructure packages import from domain packages.
|
||||
|
||||
### The dependency direction rule
|
||||
|
||||
```
|
||||
cmd/ → internal/handler → internal/service → internal/domain
|
||||
internal/store (postgres) → internal/domain
|
||||
internal/mailer (smtp) → internal/domain
|
||||
|
||||
domain imports nothing from store, mailer, handler
|
||||
```
|
||||
|
||||
### Constructor injection pattern
|
||||
|
||||
```go
|
||||
// Good: domain service accepts interfaces
|
||||
package service
|
||||
|
||||
type UserService struct {
|
||||
store UserStore // interface
|
||||
mailer Mailer // interface
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewUserService constructs with injected dependencies.
|
||||
// All parameters are interfaces — callers provide implementations.
|
||||
func NewUserService(store UserStore, mailer Mailer, logger *slog.Logger) *UserService {
|
||||
return &UserService{store: store, mailer: mailer, logger: logger}
|
||||
}
|
||||
|
||||
// Bad: domain service imports infrastructure
|
||||
package service
|
||||
|
||||
import "github.com/example/myapp/internal/store/postgres"
|
||||
|
||||
type UserService struct {
|
||||
store *postgres.Store // Locked to PostgreSQL — breaks DIP
|
||||
}
|
||||
```
|
||||
|
||||
### Wire it up at the boundary
|
||||
|
||||
```go
|
||||
// cmd/server/main.go — the composition root
|
||||
func main() {
|
||||
db := mustOpenDB(cfg.DatabaseURL)
|
||||
store := postgres.NewUserStore(db)
|
||||
mailer := smtp.NewMailer(cfg.SMTP)
|
||||
logger := slog.Default()
|
||||
svc := service.NewUserService(store, mailer, logger)
|
||||
handler := handler.NewUserHandler(svc)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `main` function (or a dependency injection framework) is the only place that names concrete implementations. All other code depends on interfaces.
|
||||
|
||||
### When NOT to extract an interface
|
||||
|
||||
Don't extract an interface prematurely. These are fine as concrete dependencies:
|
||||
|
||||
- `*slog.Logger` — no interface needed; it already accepts a Handler interface internally
|
||||
- `*sql.DB` — acceptable in `store` packages; extract an interface at the service boundary
|
||||
- Standard library types that are stable and have no test double need
|
||||
|
||||
Only extract an interface when you have:
|
||||
1. Multiple implementations (real + test double), or
|
||||
2. A package boundary you want to keep clean (domain knows nothing of postgres)
|
||||
@@ -0,0 +1,328 @@
|
||||
# Object-Oriented Design
|
||||
|
||||
## Responsibility-Driven Design (RDD)
|
||||
|
||||
The key insight: **Objects are defined by their responsibilities, not their data.**
|
||||
|
||||
### Finding Objects
|
||||
|
||||
Start with:
|
||||
1. **Nouns** in requirements → candidate objects
|
||||
2. **Verbs** → candidate methods/behaviors
|
||||
3. **Domain concepts** → value objects
|
||||
|
||||
### Finding Responsibilities
|
||||
|
||||
Each object should answer:
|
||||
- What does this object **know**?
|
||||
- What does this object **do**?
|
||||
- What does this object **decide**?
|
||||
|
||||
### Object Stereotypes
|
||||
|
||||
Every class fits one (or maybe two) stereotypes:
|
||||
|
||||
| Stereotype | Purpose | Example |
|
||||
|------------|---------|---------|
|
||||
| **Information Holder** | Knows things, holds data | `User`, `Product`, `Address` |
|
||||
| **Structurer** | Maintains relationships | `OrderItems`, `UserGroup` |
|
||||
| **Service Provider** | Performs work | `PaymentProcessor`, `EmailSender` |
|
||||
| **Coordinator** | Orchestrates workflow | `OrderFulfillmentService` |
|
||||
| **Controller** | Makes decisions, delegates | `CheckoutController` |
|
||||
| **Interfacer** | Transforms between systems | `UserAPIAdapter`, `DatabaseMapper` |
|
||||
|
||||
### The Two Questions
|
||||
|
||||
For every class, ask:
|
||||
1. **"What pattern is this?"** - Which stereotype? Which design pattern?
|
||||
2. **"Is it doing too much?"** - Check object calisthenics rules
|
||||
|
||||
If you can't answer clearly, the class needs refactoring.
|
||||
|
||||
---
|
||||
|
||||
## Tell, Don't Ask
|
||||
|
||||
**Command objects to do work. Don't interrogate them and do the work yourself.**
|
||||
|
||||
```typescript
|
||||
// BAD: Asking, then doing
|
||||
if (account.getBalance() >= amount) {
|
||||
account.setBalance(account.getBalance() - amount);
|
||||
// more logic here...
|
||||
}
|
||||
|
||||
// GOOD: Telling
|
||||
const result = account.withdraw(amount);
|
||||
if (result.isSuccess()) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The object that has the data should have the behavior.
|
||||
|
||||
---
|
||||
|
||||
## Design by Contract (DbC)
|
||||
|
||||
Every method has:
|
||||
- **Preconditions** - What must be true BEFORE calling
|
||||
- **Postconditions** - What will be true AFTER calling
|
||||
- **Invariants** - What is ALWAYS true about the object
|
||||
|
||||
```typescript
|
||||
class BankAccount {
|
||||
private balance: Money;
|
||||
|
||||
// INVARIANT: balance is never negative
|
||||
|
||||
// PRECONDITION: amount > 0
|
||||
// POSTCONDITION: balance decreased by amount OR error returned
|
||||
withdraw(amount: Money): WithdrawResult {
|
||||
if (amount.isNegativeOrZero()) {
|
||||
return WithdrawResult.invalidAmount();
|
||||
}
|
||||
|
||||
if (this.balance.isLessThan(amount)) {
|
||||
return WithdrawResult.insufficientFunds();
|
||||
}
|
||||
|
||||
this.balance = this.balance.minus(amount);
|
||||
return WithdrawResult.success(this.balance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composition Over Inheritance
|
||||
|
||||
**Prefer composing objects over extending classes.**
|
||||
|
||||
### Why Inheritance is Problematic:
|
||||
- Tight coupling between parent and child
|
||||
- Fragile base class problem
|
||||
- Difficult to change parent without breaking children
|
||||
- Forces "is-a" relationship that may not fit
|
||||
|
||||
### When to Use Inheritance:
|
||||
- True "is-a" relationship (rare)
|
||||
- Framework requirements
|
||||
- Template Method pattern (intentional)
|
||||
|
||||
### Prefer Composition:
|
||||
```typescript
|
||||
// BAD: Inheritance
|
||||
class PremiumUser extends User {
|
||||
getDiscount(): number { return 20; }
|
||||
}
|
||||
|
||||
// GOOD: Composition
|
||||
class User {
|
||||
constructor(private discountPolicy: DiscountPolicy) {}
|
||||
|
||||
getDiscount(): number {
|
||||
return this.discountPolicy.calculate();
|
||||
}
|
||||
}
|
||||
|
||||
// Now discount behavior is pluggable
|
||||
new User(new PremiumDiscount());
|
||||
new User(new StandardDiscount());
|
||||
new User(new NoDiscount());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Law of Demeter (Principle of Least Knowledge)
|
||||
|
||||
**Only talk to your immediate friends.**
|
||||
|
||||
A method should only call:
|
||||
1. Methods on `this`
|
||||
2. Methods on parameters
|
||||
3. Methods on objects it creates
|
||||
4. Methods on its direct components
|
||||
|
||||
```typescript
|
||||
// BAD: Reaching through objects
|
||||
order.getCustomer().getAddress().getCity();
|
||||
|
||||
// GOOD: Ask the immediate friend
|
||||
order.getShippingCity();
|
||||
```
|
||||
|
||||
This reduces coupling - changes to `Address` don't ripple through all callers.
|
||||
|
||||
---
|
||||
|
||||
## Encapsulation
|
||||
|
||||
**Hide internal details, expose behavior.**
|
||||
|
||||
### Levels of Encapsulation:
|
||||
1. **Data** - private fields, no direct access
|
||||
2. **Implementation** - how things work internally
|
||||
3. **Type** - concrete class hidden behind interface
|
||||
4. **Design** - architectural decisions hidden from clients
|
||||
|
||||
```typescript
|
||||
// BAD: Exposed internals
|
||||
class Order {
|
||||
public items: Item[] = [];
|
||||
public total: number = 0;
|
||||
}
|
||||
|
||||
// Client can corrupt state
|
||||
order.items.push(item);
|
||||
order.total = -999; // Oops!
|
||||
|
||||
// GOOD: Encapsulated
|
||||
class Order {
|
||||
private items: OrderItems;
|
||||
private total: Money;
|
||||
|
||||
addItem(item: Item): void {
|
||||
this.items.add(item);
|
||||
this.recalculateTotal();
|
||||
}
|
||||
|
||||
getTotal(): Money {
|
||||
return this.total; // Returns copy or immutable
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Polymorphism
|
||||
|
||||
**Replace conditionals with types.**
|
||||
|
||||
```typescript
|
||||
// BAD: Type checking
|
||||
function calculateShipping(method: string, value: number): number {
|
||||
if (method === 'standard') return value < 50 ? 5 : 0;
|
||||
if (method === 'express') return 15;
|
||||
if (method === 'overnight') return 25;
|
||||
throw new Error('Unknown method');
|
||||
}
|
||||
|
||||
// GOOD: Polymorphism
|
||||
interface ShippingMethod {
|
||||
calculateCost(orderValue: number): number;
|
||||
}
|
||||
|
||||
class StandardShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return orderValue < 50 ? 5 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return 15;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - no conditionals
|
||||
function calculateShipping(method: ShippingMethod, value: number): number {
|
||||
return method.calculateCost(value);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Value Objects vs Entities
|
||||
|
||||
### Value Objects
|
||||
- Defined by their attributes (no identity)
|
||||
- Immutable
|
||||
- Comparable by value
|
||||
- Examples: `Money`, `Email`, `Address`, `DateRange`
|
||||
|
||||
```typescript
|
||||
class Money {
|
||||
constructor(
|
||||
private readonly amount: number,
|
||||
private readonly currency: string
|
||||
) {}
|
||||
|
||||
equals(other: Money): boolean {
|
||||
return this.amount === other.amount &&
|
||||
this.currency === other.currency;
|
||||
}
|
||||
|
||||
add(other: Money): Money {
|
||||
if (this.currency !== other.currency) {
|
||||
throw new CurrencyMismatch();
|
||||
}
|
||||
return new Money(this.amount + other.amount, this.currency);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entities
|
||||
- Have identity (survives attribute changes)
|
||||
- Usually mutable (via methods)
|
||||
- Comparable by identity
|
||||
- Examples: `User`, `Order`, `Product`
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
constructor(
|
||||
private readonly id: UserId,
|
||||
private email: Email,
|
||||
private name: Name
|
||||
) {}
|
||||
|
||||
equals(other: User): boolean {
|
||||
return this.id.equals(other.id); // Identity comparison
|
||||
}
|
||||
|
||||
changeEmail(newEmail: Email): void {
|
||||
this.email = newEmail; // Still same user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aggregates
|
||||
|
||||
A cluster of objects treated as a single unit for data changes.
|
||||
|
||||
- One object is the **aggregate root** (entry point)
|
||||
- External code only references the root
|
||||
- Root enforces invariants for the entire cluster
|
||||
|
||||
```typescript
|
||||
// Order is the aggregate root
|
||||
class Order {
|
||||
private items: OrderItem[] = [];
|
||||
|
||||
// All access through the root
|
||||
addItem(product: Product, quantity: number): void {
|
||||
const item = new OrderItem(product, quantity);
|
||||
this.items.push(item);
|
||||
this.validateTotal();
|
||||
}
|
||||
|
||||
removeItem(itemId: ItemId): void {
|
||||
this.items = this.items.filter(i => !i.id.equals(itemId));
|
||||
}
|
||||
|
||||
// Root enforces invariants
|
||||
private validateTotal(): void {
|
||||
if (this.calculateTotal().exceeds(MAX_ORDER_VALUE)) {
|
||||
throw new OrderTotalExceeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BAD: Accessing items directly
|
||||
order.items.push(new OrderItem(...)); // Bypasses validation!
|
||||
|
||||
// GOOD: Through the root
|
||||
order.addItem(product, 2); // Validation happens
|
||||
```
|
||||
@@ -0,0 +1,262 @@
|
||||
# SOLID Principles
|
||||
|
||||
## Overview
|
||||
|
||||
SOLID helps structure software to be flexible, maintainable, and testable. These principles reduce coupling and increase cohesion.
|
||||
|
||||
## S - Single Responsibility Principle (SRP)
|
||||
|
||||
> "A class should have one, and only one, reason to change."
|
||||
|
||||
### Problem It Solves
|
||||
God objects that do everything - hard to test, hard to change, hard to understand.
|
||||
|
||||
### How to Apply
|
||||
Each class handles ONE responsibility. If you find yourself saying "and" when describing what a class does, split it.
|
||||
|
||||
```typescript
|
||||
// BAD: Multiple responsibilities
|
||||
class Order {
|
||||
calculateTotal(): number { ... }
|
||||
saveToDatabase(): void { ... } // Persistence
|
||||
generateInvoice(): string { ... } // Presentation
|
||||
}
|
||||
|
||||
// GOOD: Single responsibility each
|
||||
class Order {
|
||||
private items: OrderItem[] = [];
|
||||
|
||||
addItem(item: OrderItem): void { ... }
|
||||
calculateTotal(): number { ... }
|
||||
}
|
||||
|
||||
class OrderRepository {
|
||||
save(order: Order): Promise<void> { ... }
|
||||
}
|
||||
|
||||
class InvoiceGenerator {
|
||||
generate(order: Order): Invoice { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Detection Questions
|
||||
- Does this class have multiple reasons to change?
|
||||
- Can I describe it without using "and"?
|
||||
- Would different stakeholders request changes to different parts?
|
||||
|
||||
---
|
||||
|
||||
## O - Open/Closed Principle (OCP)
|
||||
|
||||
> "Software entities should be open for extension but closed for modification."
|
||||
|
||||
### Problem It Solves
|
||||
Having to modify existing, tested code every time requirements change. Risk of breaking working features.
|
||||
|
||||
### How to Apply
|
||||
Design abstractions that allow new behavior through new classes, not edits to existing ones.
|
||||
|
||||
```typescript
|
||||
// BAD: Must modify to add new shipping
|
||||
class ShippingCalculator {
|
||||
calculate(type: string, value: number): number {
|
||||
if (type === 'standard') return value < 50 ? 5 : 0;
|
||||
if (type === 'express') return 15;
|
||||
// Must add more ifs for new types!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Open for extension
|
||||
interface ShippingMethod {
|
||||
calculateCost(orderValue: number): number;
|
||||
}
|
||||
|
||||
class StandardShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return orderValue < 50 ? 5 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return 15;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new shipping by creating new class, not modifying existing
|
||||
class SameDayShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Architectural Insight
|
||||
OCP at architecture level means: **design your codebase so new features are added by adding code, not changing existing code.**
|
||||
|
||||
---
|
||||
|
||||
## L - Liskov Substitution Principle (LSP)
|
||||
|
||||
> "Subtypes must be substitutable for their base types without altering program correctness."
|
||||
|
||||
### Problem It Solves
|
||||
Subclasses that break expectations, requiring type-checking and special cases.
|
||||
|
||||
### How to Apply
|
||||
Subclasses must honor the contract of the parent. If the parent returns positive numbers, subclasses cannot return negatives.
|
||||
|
||||
```typescript
|
||||
// BAD: Violates parent's contract
|
||||
class DiscountPolicy {
|
||||
getDiscount(value: number): number {
|
||||
return 0; // Non-negative expected
|
||||
}
|
||||
}
|
||||
|
||||
class WeirdDiscount extends DiscountPolicy {
|
||||
getDiscount(value: number): number {
|
||||
return -5; // Increases cost! Breaks expectations
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Enforces contract
|
||||
class DiscountPolicy {
|
||||
constructor(private discount: number) {
|
||||
if (discount < 0) throw new Error("Discount must be non-negative");
|
||||
}
|
||||
|
||||
getDiscount(): number {
|
||||
return this.discount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Insight
|
||||
This is why you can swap `InMemoryUserRepo` for `PostgresUserRepo` - they both honor the `UserRepo` interface contract.
|
||||
|
||||
---
|
||||
|
||||
## I - Interface Segregation Principle (ISP)
|
||||
|
||||
> "Clients should not be forced to depend on methods they do not use."
|
||||
|
||||
### Problem It Solves
|
||||
Fat interfaces that force partial implementations, empty methods, or throws.
|
||||
|
||||
### How to Apply
|
||||
Split large interfaces into smaller, cohesive ones. Clients depend only on what they need.
|
||||
|
||||
```typescript
|
||||
// BAD: Fat interface
|
||||
interface WarehouseDevice {
|
||||
printLabel(orderId: string): void;
|
||||
scanBarcode(): string;
|
||||
packageItem(orderId: string): void;
|
||||
}
|
||||
|
||||
class BasicPrinter implements WarehouseDevice {
|
||||
printLabel(orderId: string): void { /* works */ }
|
||||
scanBarcode(): string { throw new Error("Not supported"); } // Forced!
|
||||
packageItem(orderId: string): void { throw new Error("Not supported"); }
|
||||
}
|
||||
|
||||
// GOOD: Segregated interfaces
|
||||
interface LabelPrinter {
|
||||
printLabel(orderId: string): void;
|
||||
}
|
||||
|
||||
interface BarcodeScanner {
|
||||
scanBarcode(): string;
|
||||
}
|
||||
|
||||
interface ItemPackager {
|
||||
packageItem(orderId: string): void;
|
||||
}
|
||||
|
||||
class BasicPrinter implements LabelPrinter {
|
||||
printLabel(orderId: string): void { /* only what it does */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Detection
|
||||
If you see `throw new Error("Not implemented")` or empty method bodies, the interface is too fat.
|
||||
|
||||
---
|
||||
|
||||
## D - Dependency Inversion Principle (DIP)
|
||||
|
||||
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
|
||||
|
||||
### Problem It Solves
|
||||
Tight coupling to specific implementations (databases, APIs, frameworks). Hard to test, hard to swap.
|
||||
|
||||
### How to Apply
|
||||
Depend on interfaces, inject implementations.
|
||||
|
||||
```typescript
|
||||
// BAD: Direct dependency on concrete class
|
||||
class OrderService {
|
||||
private emailService = new SendGridEmailService(); // Locked in!
|
||||
|
||||
confirmOrder(email: string): void {
|
||||
this.emailService.send(email, "Order confirmed");
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Depend on abstraction
|
||||
interface EmailService {
|
||||
send(to: string, message: string): void;
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
constructor(private emailService: EmailService) {}
|
||||
|
||||
confirmOrder(email: string): void {
|
||||
this.emailService.send(email, "Order confirmed");
|
||||
}
|
||||
}
|
||||
|
||||
// Now can inject any implementation
|
||||
new OrderService(new SendGridEmailService());
|
||||
new OrderService(new SESEmailService());
|
||||
new OrderService(new MockEmailService()); // For tests!
|
||||
```
|
||||
|
||||
### The Dependency Rule
|
||||
Source code dependencies should point **inward** toward high-level policies (domain logic), never toward low-level details (infrastructure).
|
||||
|
||||
```
|
||||
Infrastructure → Application → Domain
|
||||
↑ ↑ ↑
|
||||
(outer) (middle) (inner)
|
||||
|
||||
Dependencies flow: outer → inner
|
||||
Never: inner → outer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Applying SOLID at Architecture Level
|
||||
|
||||
These principles scale beyond classes:
|
||||
|
||||
| Principle | Architecture Application |
|
||||
|-----------|--------------------------|
|
||||
| SRP | Each bounded context has one responsibility |
|
||||
| OCP | New features = new modules, not edits to existing |
|
||||
| LSP | Microservices with same contract are substitutable |
|
||||
| ISP | Thin interfaces between services |
|
||||
| DIP | High-level business logic doesn't know about databases/frameworks |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Principle | One-Liner | Red Flag |
|
||||
|-----------|-----------|----------|
|
||||
| SRP | One reason to change | "This class handles X and Y and Z" |
|
||||
| OCP | Add, don't modify | `if/else` chains for types |
|
||||
| LSP | Subtypes are substitutable | Type-checking in calling code |
|
||||
| ISP | Small, focused interfaces | Empty method implementations |
|
||||
| DIP | Depend on abstractions | `new ConcreteClass()` in business logic |
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
name: spec-driven-dev
|
||||
description: Write a structured specification before writing any code. Use when starting a new project, feature, or significant change. Adapted for a PM-first workflow where why comes before how.
|
||||
---
|
||||
|
||||
# Spec-Driven Development
|
||||
|
||||
## Overview
|
||||
|
||||
Write a structured specification before writing any code. The spec is the shared source of truth — it defines what we're building, why it matters, and how we'll know it's done.
|
||||
|
||||
**Code without a spec is guessing.**
|
||||
|
||||
A spec doesn't need to be long. A two-paragraph spec beats no spec. The value is in forcing clarity before code is written, not in the length of the document.
|
||||
|
||||
## Mathias PM Context
|
||||
|
||||
As a digital product manager building software:
|
||||
|
||||
- **Why before how:** The spec must capture the business context and user need before technical decisions. Agents reading the spec should understand *why this matters*, not just *what to build*.
|
||||
- **Explicit success criteria:** Vague requirements produce vague results. Every spec must have testable success criteria.
|
||||
- **Surfaces assumptions:** The spec's primary job is to surface misunderstandings before they become expensive code.
|
||||
- **Living document:** Update the spec when decisions change. An outdated spec is still better than no spec.
|
||||
|
||||
## When to Use
|
||||
|
||||
**Always create a spec when:**
|
||||
- Starting a new project or feature
|
||||
- Requirements are ambiguous or incomplete
|
||||
- The change touches multiple files or modules
|
||||
- You're about to make an architectural decision
|
||||
- The task would take more than a day to implement
|
||||
|
||||
**When NOT to use:** Single-line fixes, typos, or changes where requirements are unambiguous and self-contained.
|
||||
|
||||
## The Gated Workflow
|
||||
|
||||
Do not advance to the next phase without validation at each gate.
|
||||
|
||||
```
|
||||
SPECIFY → [review] → PLAN → [review] → TASKS → [review] → IMPLEMENT
|
||||
```
|
||||
|
||||
Each gate is a deliberate pause: does the next phase make sense given what we know?
|
||||
|
||||
## Phase 1: SPECIFY
|
||||
|
||||
### Surface Assumptions Immediately
|
||||
|
||||
Before writing spec content, list what you're assuming:
|
||||
|
||||
```
|
||||
ASSUMPTIONS I'M MAKING:
|
||||
1. This is a Go backend service (no frontend changes)
|
||||
2. Authentication is handled by the existing middleware
|
||||
3. The database is PostgreSQL (matching the rest of the stack)
|
||||
4. The feature is used by authenticated users only, not public
|
||||
→ Confirm or correct before I proceed.
|
||||
```
|
||||
|
||||
Don't silently fill in ambiguous requirements.
|
||||
|
||||
### Write the Spec
|
||||
|
||||
A spec covers six areas:
|
||||
|
||||
**1. Objective — WHY are we building this?**
|
||||
|
||||
This is the most important section. It must answer:
|
||||
- What user problem does this solve?
|
||||
- Who is the user?
|
||||
- What does success look like from the user's perspective?
|
||||
- Why now?
|
||||
|
||||
```markdown
|
||||
## Objective
|
||||
|
||||
Invoice importers at small accounting firms manually copy payment details
|
||||
from PDF invoices into their banking system, taking 10–20 minutes per invoice.
|
||||
|
||||
**User:** Invoice processor at an accounting firm (10–50 invoices/day)
|
||||
**Problem:** Manual data entry is slow, error-prone, and creates compliance risk
|
||||
**Goal:** Reduce per-invoice processing time from ~15 minutes to < 2 minutes
|
||||
|
||||
Success: Invoice processor can extract and queue a payment from a PDF in under 2 minutes,
|
||||
with confidence the data is correct.
|
||||
```
|
||||
|
||||
**2. Commands — Full executable commands**
|
||||
|
||||
```
|
||||
Build: task build
|
||||
Test: task test (or: go test ./...)
|
||||
Lint: task lint (or: golangci-lint run)
|
||||
Dev: task dev
|
||||
Deploy: task deploy:staging
|
||||
```
|
||||
|
||||
**3. Project Structure — Where things live**
|
||||
|
||||
```
|
||||
internal/
|
||||
domain/ → Core types and interfaces
|
||||
service/ → Business logic
|
||||
store/ → Database implementations
|
||||
handler/ → HTTP handlers
|
||||
cmd/
|
||||
server/ → Main entry point
|
||||
```
|
||||
|
||||
**4. Code Style — One real example beats three paragraphs**
|
||||
|
||||
```go
|
||||
// Error handling: always wrap with context
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse invoice PDF: %w", err)
|
||||
}
|
||||
|
||||
// Dependency injection: accept interfaces
|
||||
func NewInvoiceService(store InvoiceStore, parser PDFParser) *InvoiceService { ... }
|
||||
|
||||
// Context: always first parameter for I/O operations
|
||||
func (s *InvoiceService) ProcessPDF(ctx context.Context, r io.Reader) (Invoice, error) { ... }
|
||||
```
|
||||
|
||||
**5. Testing Strategy**
|
||||
|
||||
```
|
||||
Framework: testing package + testify
|
||||
Locations: *_test.go files in same package
|
||||
Unit tests: table-driven, in-memory implementations for stores
|
||||
Fast path: go test ./... (unit tests only)
|
||||
Full suite: go test -tags=integration ./... (includes DB tests)
|
||||
Coverage: >80% for business logic packages
|
||||
```
|
||||
|
||||
**6. Boundaries**
|
||||
|
||||
```
|
||||
Always:
|
||||
- Run go test ./... before committing
|
||||
- Wrap errors with fmt.Errorf("context: %w", err)
|
||||
- Pass ctx as first parameter to any I/O function
|
||||
- Run govulncheck before adding new dependencies
|
||||
|
||||
Ask first:
|
||||
- Schema changes
|
||||
- Adding new external dependencies
|
||||
- Changing public API contracts
|
||||
- Performance changes that affect existing behavior
|
||||
|
||||
Never:
|
||||
- Commit secrets or API keys
|
||||
- Remove or skip failing tests
|
||||
- Send client data to external APIs
|
||||
- Use naked returns on errors
|
||||
```
|
||||
|
||||
### Spec Template
|
||||
|
||||
```markdown
|
||||
# Spec: [Feature/Project Name]
|
||||
|
||||
## Objective
|
||||
[What we're building and why. User story or problem statement.]
|
||||
[Who is the user? What does success look like from their perspective?]
|
||||
|
||||
## Tech Stack
|
||||
[Language, key libraries, relevant existing infrastructure]
|
||||
|
||||
## Commands
|
||||
Build: [full command]
|
||||
Test: [full command]
|
||||
Lint: [full command]
|
||||
Dev: [full command]
|
||||
|
||||
## Project Structure
|
||||
[Directory layout with descriptions]
|
||||
|
||||
## Code Style
|
||||
[One real code example showing the patterns to follow]
|
||||
|
||||
## Testing Strategy
|
||||
[Framework, test locations, what to unit test vs integration test]
|
||||
|
||||
## Boundaries
|
||||
- Always: [...]
|
||||
- Ask first: [...]
|
||||
- Never: [...]
|
||||
|
||||
## Success Criteria
|
||||
[Specific, testable conditions that define "done"]
|
||||
- [ ] [Condition 1: metric/threshold/method]
|
||||
- [ ] [Condition 2: ...]
|
||||
|
||||
## Open Questions
|
||||
[Anything unresolved that needs input before implementation begins]
|
||||
```
|
||||
|
||||
## Reframing Vague Requirements
|
||||
|
||||
When you receive a vague requirement, translate it into specific success criteria before writing any spec content:
|
||||
|
||||
```
|
||||
Vague: "Make the invoice parser more reliable"
|
||||
|
||||
Reframed success criteria:
|
||||
- Parser correctly extracts IBAN from 95% of Swedish invoice formats
|
||||
- Parser correctly extracts total amount from 98% of tested invoices
|
||||
- Parser returns a structured error (not a panic) for unrecognized formats
|
||||
- Processing time < 2 seconds for PDFs up to 10MB
|
||||
→ Are these the right targets?
|
||||
```
|
||||
|
||||
## Phase 2: PLAN
|
||||
|
||||
With a validated spec, create a technical implementation plan:
|
||||
|
||||
1. Identify major components and their dependencies
|
||||
2. Determine implementation order (foundations first)
|
||||
3. Note risks and unknowns
|
||||
4. Identify what can be built in parallel vs. what must be sequential
|
||||
5. Define verification checkpoints between phases
|
||||
|
||||
The plan should be reviewable: anyone should be able to read it and say "yes, that's the right approach" or "no, change X."
|
||||
|
||||
Load the `planning` skill for detailed task breakdown.
|
||||
|
||||
## Phase 3: TASKS
|
||||
|
||||
Break the plan into discrete, implementable tasks. Load the `planning` skill for the full task breakdown methodology.
|
||||
|
||||
**Each task must have:**
|
||||
- Acceptance criteria
|
||||
- Verification step (test command, build, manual check)
|
||||
- File count estimate (no task should touch more than ~5 files)
|
||||
|
||||
## Phase 4: IMPLEMENT
|
||||
|
||||
Execute tasks one at a time. For each task:
|
||||
1. Load `tdd` skill — write failing tests first
|
||||
2. Implement minimal code to pass
|
||||
3. Load `clean-code` skill — refactor
|
||||
|
||||
## Keeping the Spec Alive
|
||||
|
||||
- **Update when decisions change** — spec first, then code
|
||||
- **Update when scope changes**
|
||||
- **Commit the spec** — it belongs in version control
|
||||
- **Reference the spec in PRs** — link to the section each PR implements
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "This is simple, I don't need a spec" | Simple tasks still need acceptance criteria. A two-line spec is fine. |
|
||||
| "I'll write the spec after" | That's documentation, not specification. The spec's value is forcing clarity *before* code. |
|
||||
| "The spec will slow us down" | A 15-minute spec prevents hours of rework. |
|
||||
| "Requirements will change anyway" | That's why the spec is a living document. |
|
||||
| "The user knows what they want" | Even clear requests have implicit assumptions. The spec surfaces those. |
|
||||
|
||||
## Verification
|
||||
|
||||
Before starting implementation:
|
||||
|
||||
- [ ] Spec covers all six core areas
|
||||
- [ ] Mathias has reviewed and approved the spec
|
||||
- [ ] Success criteria are specific and testable
|
||||
- [ ] Boundaries (Always/Ask First/Never) are defined
|
||||
- [ ] Open questions are resolved or accepted as unknowns
|
||||
- [ ] Spec is saved to a file in the repository
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||
|
||||
**At end of each phase:**
|
||||
- `session_log` with `{skill: "spec-driven-dev", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**Phases for this skill:** specify, plan, tasks, implement
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached (gate passed).
|
||||
- `fail` — the phase's intended outcome was NOT reached (gate blocked, rework required).
|
||||
- `skip` — phase was skipped intentionally.
|
||||
|
||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Load `problem-analysis` skill for deep requirement understanding before speccing
|
||||
- Load `user-stories` skill to decompose the spec into stories
|
||||
- Load `planning` skill for task breakdown
|
||||
- Load `feature-spec` skill once implementation begins, to scope individual features inside the project
|
||||
- Load `tdd` skill during implementation
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: stage-gate-review
|
||||
description: Run or prepare a stage-gate product governance review — the go/no-go checkpoint where a council approves an initiative to advance through fixed innovation stages (Frame → Concept → Prototype → Validate → Scale), each gate guarded by a per-discipline readiness checklist. Use when preparing a Product Council / steering-group ask, building a go/no-go checklist, designing an innovation governance process, or auditing whether an initiative is genuinely ready to advance vs faking the gate. Trigger phrases include "stage gate", "product council", "go/no-go", "advance to the next stage", "is this ready to progress", "gate review", "phase gate", "innovation governance".
|
||||
---
|
||||
|
||||
# Stage-Gate Review
|
||||
|
||||
A governance pattern for moving product/innovation initiatives through fixed stages
|
||||
under explicit go/no-go control. A standing council (or steering group) approves each
|
||||
advance; every gate is guarded by a **per-discipline readiness checklist**. The point
|
||||
is not bureaucracy — it is to kill weak bets early and fund strong ones with conviction.
|
||||
|
||||
This skill is the **transferable pattern**, stripped of any client instance. When a real
|
||||
engagement uses it, the client's council names, internal tooling, stage labels, and named
|
||||
contacts stay inside the client boundary (see *Client instances* below) — never bake them
|
||||
into this skill.
|
||||
|
||||
---
|
||||
|
||||
## The gate ladder
|
||||
|
||||
Five stages, escalating commitment. Each arrow is a council ask.
|
||||
|
||||
| Stage | Question it answers | Funding posture |
|
||||
|-------|---------------------|-----------------|
|
||||
| **Frame** | Is this opportunity worth exploring? | Cheap time only |
|
||||
| **Concept** | Is there a solution worth prototyping? | Small dedicated resource |
|
||||
| **Prototype** | Does the solution work and is the business case real? | Build budget |
|
||||
| **Validate** (market test) | Will a real customer pay / adopt? | Pilot budget + infra |
|
||||
| **Scale** (commercialize) | Should we invest to grow this? | Full commercial investment |
|
||||
|
||||
Key rule: **the earliest gate (Frame → Concept) is usually feedback-only**, not approval.
|
||||
You bring it to the council to sharpen thinking, not to pass judgment — killing ideas at
|
||||
Frame teaches teams to stop framing. Approval pressure starts at Concept.
|
||||
|
||||
The bar rises each gate: hypothesis → tested hypothesis → validated evidence + business
|
||||
case → signed pilot commitment → full commercial commitment.
|
||||
|
||||
---
|
||||
|
||||
## The discipline checklist
|
||||
|
||||
Every gate is a matrix: **the same disciplines, a higher bar each stage.** A gate ask is
|
||||
"ready" only when every applicable discipline clears its bar for that stage. Adapt the
|
||||
discipline set to the domain; this is the default spine.
|
||||
|
||||
| Discipline | What it guards | Rises from Frame → Scale |
|
||||
|------------|----------------|---------------------------|
|
||||
| **Ownership** | A named, committed product owner exists | identified → secured → full commitment to commercialize |
|
||||
| **Strategy fit** | Aligns with org strategy & objectives | alignment asserted → tie to portfolio confirmed |
|
||||
| **Economics** | Revenue + fully-loaded cost through the horizon | rough estimate → validated business case, approved |
|
||||
| **Customer/market evidence** | Real demand, tested with real people | hypotheses commissioned → tested → signed pilot partner |
|
||||
| **Experience / design** | Desirability and usability validated | design lead secured → evaluative testing + quality score |
|
||||
| **Technical feasibility** | Build assumptions reviewed by engineering | assumptions noted → reviewed → infra cost approved |
|
||||
| **Legal / risk / compliance** | Regulatory and contractual exposure surfaced | initial review → SME overviews → docs initiated |
|
||||
| **Go-to-market / region** | Launch market and support model identified | market aware → aligned → support & business model agreed |
|
||||
| **Timeline** | A credible plan to complete the next stage | — |
|
||||
| **System of record** | The initiative is tracked in the canonical tool | — |
|
||||
|
||||
A discipline that clearly doesn't apply gets **explicitly waived** ("no regulatory surface —
|
||||
legal waived"), never silently dropped. Silent omission is the most common way a gate rots.
|
||||
|
||||
---
|
||||
|
||||
## Running a gate review
|
||||
|
||||
**Preparing an ask (you are the team):**
|
||||
1. Identify the target gate and pull its column from the matrix.
|
||||
2. For each discipline, gather the evidence that clears *this stage's* bar — evidence, not
|
||||
intent. "We will test" does not clear a "tested" bar.
|
||||
3. Mark every discipline: cleared / waived (with reason) / gap.
|
||||
4. If any gap is on a discipline that gates the whole bet, **do not bring the ask** — close
|
||||
the gap or bring a feedback-only ask instead.
|
||||
|
||||
**Chairing a review (you are the council):**
|
||||
1. Ask for the evidence behind each "cleared", not the claim. Claims are theatre.
|
||||
2. Probe the one discipline most likely to be faked for this stage (see anti-patterns).
|
||||
3. Decide: advance / hold (named gaps, re-ask) / kill. A council that never kills is a
|
||||
rubber stamp and the gate is worthless.
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns (gate theatre)
|
||||
|
||||
- **Checklist completion ≠ readiness.** Every box ticked, zero evidence. Demand artifacts.
|
||||
- **The pre-approved gate.** Decision made in the hallway; council ratifies. The gate adds
|
||||
cost and zero filtering.
|
||||
- **Evidence inflation.** "Validated" demand that is one friendly customer's enthusiasm.
|
||||
- **Skipping the kill.** Weak bets get "hold and re-ask" forever instead of a clean death.
|
||||
- **Frame-stage judgment.** Approving/rejecting at Frame teaches teams to stop exploring.
|
||||
- **Bar that never rises.** Same evidence accepted at Prototype as at Frame — the ladder
|
||||
collapses into one gate.
|
||||
|
||||
---
|
||||
|
||||
## Operator judgment (hard-won)
|
||||
|
||||
The matrix above is generic best practice. This section is the part that comes from running
|
||||
real reviews — apply it over the spine.
|
||||
|
||||
### Which gates to collapse, and when
|
||||
- **Low uncertainty + high strategic fit** (you already know the customer wants it): collapse
|
||||
**Prototype + Validate** — build the real thing *as* the pilot. The separate prototype gate
|
||||
buys nothing when desirability isn't the risk.
|
||||
- **Moonshot / high uncertainty**: never skip **Validate** — that's the whole bet. You can
|
||||
skip the formal **Concept** gate (run it feedback-only) but you cannot skip proving demand.
|
||||
|
||||
### Where the gate gets faked — tells by stage
|
||||
- **Concept:** "customer research" that's really 3 friendly stakeholder chats with no
|
||||
disconfirming evidence sought. Worse and more common: a Concept backed only by **selective
|
||||
external research carried over from Frame** — the team cherry-picks the framing-stage
|
||||
evidence that flatters the idea and runs no fresh test. Reused evidence is not validation.
|
||||
- **Validate:** a "signed" pilot that is actually a warm LOI — no money committed, no go-live
|
||||
date. Intent dressed as commitment.
|
||||
|
||||
### Minimum real evidence — calibrate to what the stage *can* know
|
||||
Don't demand certainty a stage can't yield. The honest question is "how much **can** we know
|
||||
at this point?" — and against that bar, **any genuine external signal beats internal
|
||||
conviction**. Often the highest-value move at an early gate is simply getting *the first real
|
||||
evidence at all*, where before there was only opinion. So:
|
||||
- Reject "cleared" that rests purely on internal belief or reused framing evidence.
|
||||
- Accept directional external evidence early; raise the bar to **committed** evidence (paid
|
||||
pilot, money down, a date) by Validate.
|
||||
- **Economics:** distrust a polished Year-3 revenue model. Trust fully-loaded cost plus one
|
||||
defensible near-term number over a confident long-horizon fiction.
|
||||
|
||||
### The two killers (what actually defeats a gate)
|
||||
- **Political pre-commitment.** Some bets management simply *wants*, regardless of evidence.
|
||||
A gate cannot kill what leadership has already decided. Name it when you see it — the gate
|
||||
is theatre on that bet, and pretending otherwise corrodes every other gate.
|
||||
- **Sunk-cost attachment.** You advance the wrong concept, then get too vested to kill it.
|
||||
The antidote is a **scientific mindset with no emotional attachment to the solution**. The
|
||||
disease is **solutions looking for problems to solve** — fall in love with the problem, stay
|
||||
ruthless about the solution. A gate's deepest job is to enforce exactly this detachment.
|
||||
|
||||
> Validated under fire — these belong in the brain as a standalone principle too, so they
|
||||
> outlive this one skill. See [[solution-looking-for-problem]], [[stage-gate-sunk-cost]].
|
||||
|
||||
---
|
||||
|
||||
## Client instances
|
||||
|
||||
When this pattern is instantiated for a named client, the instance is **confidential and
|
||||
stays in the client boundary**:
|
||||
- Council/forum names, internal stage labels, internal tooling, named contacts, strategy
|
||||
specifics, deal terms → client dir/org only, local models only, never the public brain.
|
||||
- This skill carries the **pattern**; the client carries the **wrapper**. Never merge them.
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: brainstorming
|
||||
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
|
||||
---
|
||||
|
||||
# Brainstorming Ideas Into Designs
|
||||
|
||||
Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
|
||||
|
||||
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
|
||||
|
||||
<HARD-GATE>
|
||||
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
|
||||
</HARD-GATE>
|
||||
|
||||
## Anti-Pattern: "This Is Too Simple To Need A Design"
|
||||
|
||||
Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
|
||||
|
||||
## Checklist
|
||||
|
||||
You MUST create a task for each of these items and complete them in order:
|
||||
|
||||
1. **Explore project context** — check files, docs, recent commits
|
||||
2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
|
||||
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
|
||||
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
|
||||
5. **Present design** — in sections scaled to their complexity, get user approval after each section
|
||||
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
|
||||
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
|
||||
8. **User reviews written spec** — ask user to review the spec file before proceeding
|
||||
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan
|
||||
|
||||
## Process Flow
|
||||
|
||||
```dot
|
||||
digraph brainstorming {
|
||||
"Explore project context" [shape=box];
|
||||
"Visual questions ahead?" [shape=diamond];
|
||||
"Offer Visual Companion\n(own message, no other content)" [shape=box];
|
||||
"Ask clarifying questions" [shape=box];
|
||||
"Propose 2-3 approaches" [shape=box];
|
||||
"Present design sections" [shape=box];
|
||||
"User approves design?" [shape=diamond];
|
||||
"Write design doc" [shape=box];
|
||||
"Spec self-review\n(fix inline)" [shape=box];
|
||||
"User reviews spec?" [shape=diamond];
|
||||
"Invoke writing-plans skill" [shape=doublecircle];
|
||||
|
||||
"Explore project context" -> "Visual questions ahead?";
|
||||
"Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
|
||||
"Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
|
||||
"Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
|
||||
"Ask clarifying questions" -> "Propose 2-3 approaches";
|
||||
"Propose 2-3 approaches" -> "Present design sections";
|
||||
"Present design sections" -> "User approves design?";
|
||||
"User approves design?" -> "Present design sections" [label="no, revise"];
|
||||
"User approves design?" -> "Write design doc" [label="yes"];
|
||||
"Write design doc" -> "Spec self-review\n(fix inline)";
|
||||
"Spec self-review\n(fix inline)" -> "User reviews spec?";
|
||||
"User reviews spec?" -> "Write design doc" [label="changes requested"];
|
||||
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
|
||||
}
|
||||
```
|
||||
|
||||
**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.
|
||||
|
||||
## The Process
|
||||
|
||||
**Understanding the idea:**
|
||||
|
||||
- Check out the current project state first (files, docs, recent commits)
|
||||
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
|
||||
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
|
||||
- For appropriately-scoped projects, ask questions one at a time to refine the idea
|
||||
- Prefer multiple choice questions when possible, but open-ended is fine too
|
||||
- Only one question per message - if a topic needs more exploration, break it into multiple questions
|
||||
- Focus on understanding: purpose, constraints, success criteria
|
||||
|
||||
**Exploring approaches:**
|
||||
|
||||
- Propose 2-3 different approaches with trade-offs
|
||||
- Present options conversationally with your recommendation and reasoning
|
||||
- Lead with your recommended option and explain why
|
||||
|
||||
**Presenting the design:**
|
||||
|
||||
- Once you believe you understand what you're building, present the design
|
||||
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
|
||||
- Ask after each section whether it looks right so far
|
||||
- Cover: architecture, components, data flow, error handling, testing
|
||||
- Be ready to go back and clarify if something doesn't make sense
|
||||
|
||||
**Design for isolation and clarity:**
|
||||
|
||||
- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
|
||||
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
|
||||
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
|
||||
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
|
||||
|
||||
**Working in existing codebases:**
|
||||
|
||||
- Explore the current structure before proposing changes. Follow existing patterns.
|
||||
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
|
||||
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.
|
||||
|
||||
## After the Design
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
- (User preferences for spec location override this default)
|
||||
- Use elements-of-style:writing-clearly-and-concisely skill if available
|
||||
- Commit the design document to git
|
||||
|
||||
**Spec Self-Review:**
|
||||
After writing the spec document, look at it with fresh eyes:
|
||||
|
||||
1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
|
||||
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
|
||||
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
|
||||
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
|
||||
|
||||
Fix any issues inline. No need to re-review — just fix and move on.
|
||||
|
||||
**User Review Gate:**
|
||||
After the spec review loop passes, ask the user to review the written spec before proceeding:
|
||||
|
||||
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
|
||||
|
||||
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Invoke the writing-plans skill to create a detailed implementation plan
|
||||
- Do NOT invoke any other skill. writing-plans is the next step.
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **One question at a time** - Don't overwhelm with multiple questions
|
||||
- **Multiple choice preferred** - Easier to answer than open-ended when possible
|
||||
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
|
||||
- **Explore alternatives** - Always propose 2-3 approaches before settling
|
||||
- **Incremental validation** - Present design, get approval before moving on
|
||||
- **Be flexible** - Go back and clarify when something doesn't make sense
|
||||
|
||||
## Visual Companion
|
||||
|
||||
A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.
|
||||
|
||||
**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
|
||||
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"
|
||||
|
||||
**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.
|
||||
|
||||
**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**
|
||||
|
||||
- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
|
||||
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions
|
||||
|
||||
A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.
|
||||
|
||||
If they agree to the companion, read the detailed guide before proceeding:
|
||||
`skills/brainstorming/visual-companion.md`
|
||||
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Superpowers Brainstorming</title>
|
||||
<style>
|
||||
/*
|
||||
* BRAINSTORM COMPANION FRAME TEMPLATE
|
||||
*
|
||||
* This template provides a consistent frame with:
|
||||
* - OS-aware light/dark theming
|
||||
* - Fixed header and selection indicator bar
|
||||
* - Scrollable main content area
|
||||
* - CSS helpers for common UI patterns
|
||||
*
|
||||
* Content is injected via placeholder comment in #claude-content.
|
||||
*/
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
|
||||
/* ===== THEME VARIABLES ===== */
|
||||
:root {
|
||||
--bg-primary: #f5f5f7;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #e5e5e7;
|
||||
--border: #d1d1d6;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #aeaeb2;
|
||||
--accent: #0071e3;
|
||||
--accent-hover: #0077ed;
|
||||
--success: #34c759;
|
||||
--warning: #ff9f0a;
|
||||
--error: #ff3b30;
|
||||
--selected-bg: #e8f4fd;
|
||||
--selected-border: #0071e3;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #1d1d1f;
|
||||
--bg-secondary: #2d2d2f;
|
||||
--bg-tertiary: #3d3d3f;
|
||||
--border: #424245;
|
||||
--text-primary: #f5f5f7;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #636366;
|
||||
--accent: #0a84ff;
|
||||
--accent-hover: #409cff;
|
||||
--selected-bg: rgba(10, 132, 255, 0.15);
|
||||
--selected-border: #0a84ff;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== FRAME STRUCTURE ===== */
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.5rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
|
||||
.header .status { font-size: 0.7rem; color: var(--success); display: flex; align-items: center; gap: 0.4rem; }
|
||||
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--success); border-radius: 50%; }
|
||||
|
||||
.main { flex: 1; overflow-y: auto; }
|
||||
#claude-content { padding: 2rem; min-height: 100%; }
|
||||
|
||||
.indicator-bar {
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0.5rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.indicator-bar span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.indicator-bar .selected-text {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== TYPOGRAPHY ===== */
|
||||
h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.subtitle { color: var(--text-secondary); margin-bottom: 1.5rem; }
|
||||
.section { margin-bottom: 2rem; }
|
||||
.label { font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
||||
|
||||
/* ===== OPTIONS (for A/B/C choices) ===== */
|
||||
.options { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.option {
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
.option:hover { border-color: var(--accent); }
|
||||
.option.selected { background: var(--selected-bg); border-color: var(--selected-border); }
|
||||
.option .letter {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
width: 1.75rem; height: 1.75rem;
|
||||
border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 600; font-size: 0.85rem; flex-shrink: 0;
|
||||
}
|
||||
.option.selected .letter { background: var(--accent); color: white; }
|
||||
.option .content { flex: 1; }
|
||||
.option .content h3 { font-size: 0.95rem; margin-bottom: 0.15rem; }
|
||||
.option .content p { color: var(--text-secondary); font-size: 0.85rem; margin: 0; }
|
||||
|
||||
/* ===== CARDS (for showing designs/mockups) ===== */
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||
.card.selected { border-color: var(--selected-border); border-width: 2px; }
|
||||
.card-image { background: var(--bg-tertiary); aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; }
|
||||
.card-body { padding: 1rem; }
|
||||
.card-body h3 { margin-bottom: 0.25rem; }
|
||||
.card-body p { color: var(--text-secondary); font-size: 0.85rem; }
|
||||
|
||||
/* ===== MOCKUP CONTAINER ===== */
|
||||
.mockup {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.mockup-header {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.mockup-body { padding: 1.5rem; }
|
||||
|
||||
/* ===== SPLIT VIEW (side-by-side comparison) ===== */
|
||||
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
||||
@media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ===== PROS/CONS ===== */
|
||||
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
|
||||
.pros, .cons { background: var(--bg-secondary); border-radius: 8px; padding: 1rem; }
|
||||
.pros h4 { color: var(--success); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.cons h4 { color: var(--error); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.pros ul, .cons ul { margin-left: 1.25rem; font-size: 0.85rem; color: var(--text-secondary); }
|
||||
.pros li, .cons li { margin-bottom: 0.25rem; }
|
||||
|
||||
/* ===== PLACEHOLDER (for mockup areas) ===== */
|
||||
.placeholder {
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== INLINE MOCKUP ELEMENTS ===== */
|
||||
.mock-nav { background: var(--accent); color: white; padding: 0.75rem 1rem; display: flex; gap: 1.5rem; font-size: 0.9rem; }
|
||||
.mock-sidebar { background: var(--bg-tertiary); padding: 1rem; min-width: 180px; }
|
||||
.mock-content { padding: 1.5rem; flex: 1; }
|
||||
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
|
||||
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
|
||||
<div class="status">Connected</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div id="claude-content">
|
||||
<!-- CONTENT -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
(function() {
|
||||
const WS_URL = 'ws://' + window.location.host;
|
||||
let ws = null;
|
||||
let eventQueue = [];
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(WS_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
eventQueue.forEach(e => ws.send(JSON.stringify(e)));
|
||||
eventQueue = [];
|
||||
};
|
||||
|
||||
ws.onmessage = (msg) => {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (data.type === 'reload') {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setTimeout(connect, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
function sendEvent(event) {
|
||||
event.timestamp = Date.now();
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(event));
|
||||
} else {
|
||||
eventQueue.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture clicks on choice elements
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-choice]');
|
||||
if (!target) return;
|
||||
|
||||
sendEvent({
|
||||
type: 'click',
|
||||
text: target.textContent.trim(),
|
||||
choice: target.dataset.choice,
|
||||
id: target.id || null
|
||||
});
|
||||
|
||||
// Update indicator bar (defer so toggleSelect runs first)
|
||||
setTimeout(() => {
|
||||
const indicator = document.getElementById('indicator-text');
|
||||
if (!indicator) return;
|
||||
const container = target.closest('.options') || target.closest('.cards');
|
||||
const selected = container ? container.querySelectorAll('.selected') : [];
|
||||
if (selected.length === 0) {
|
||||
indicator.textContent = 'Click an option above, then return to the terminal';
|
||||
} else if (selected.length === 1) {
|
||||
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
|
||||
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
|
||||
} else {
|
||||
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' selected</span> — return to terminal to continue';
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Frame UI: selection tracking
|
||||
window.selectedChoice = null;
|
||||
|
||||
window.toggleSelect = function(el) {
|
||||
const container = el.closest('.options') || el.closest('.cards');
|
||||
const multi = container && container.dataset.multiselect !== undefined;
|
||||
if (container && !multi) {
|
||||
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
|
||||
}
|
||||
if (multi) {
|
||||
el.classList.toggle('selected');
|
||||
} else {
|
||||
el.classList.add('selected');
|
||||
}
|
||||
window.selectedChoice = el.dataset.choice;
|
||||
};
|
||||
|
||||
// Expose API for explicit use
|
||||
window.brainstorm = {
|
||||
send: sendEvent,
|
||||
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
|
||||
};
|
||||
|
||||
connect();
|
||||
})();
|
||||
@@ -0,0 +1,354 @@
|
||||
const crypto = require('crypto');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ========== WebSocket Protocol (RFC 6455) ==========
|
||||
|
||||
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
|
||||
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
|
||||
function computeAcceptKey(clientKey) {
|
||||
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
|
||||
}
|
||||
|
||||
function encodeFrame(opcode, payload) {
|
||||
const fin = 0x80;
|
||||
const len = payload.length;
|
||||
let header;
|
||||
|
||||
if (len < 126) {
|
||||
header = Buffer.alloc(2);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = len;
|
||||
} else if (len < 65536) {
|
||||
header = Buffer.alloc(4);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = 126;
|
||||
header.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
header = Buffer.alloc(10);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = 127;
|
||||
header.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function decodeFrame(buffer) {
|
||||
if (buffer.length < 2) return null;
|
||||
|
||||
const secondByte = buffer[1];
|
||||
const opcode = buffer[0] & 0x0F;
|
||||
const masked = (secondByte & 0x80) !== 0;
|
||||
let payloadLen = secondByte & 0x7F;
|
||||
let offset = 2;
|
||||
|
||||
if (!masked) throw new Error('Client frames must be masked');
|
||||
|
||||
if (payloadLen === 126) {
|
||||
if (buffer.length < 4) return null;
|
||||
payloadLen = buffer.readUInt16BE(2);
|
||||
offset = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buffer.length < 10) return null;
|
||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
||||
offset = 10;
|
||||
}
|
||||
|
||||
const maskOffset = offset;
|
||||
const dataOffset = offset + 4;
|
||||
const totalLen = dataOffset + payloadLen;
|
||||
if (buffer.length < totalLen) return null;
|
||||
|
||||
const mask = buffer.slice(maskOffset, dataOffset);
|
||||
const data = Buffer.alloc(payloadLen);
|
||||
for (let i = 0; i < payloadLen; i++) {
|
||||
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
|
||||
}
|
||||
|
||||
return { opcode, payload: data, bytesConsumed: totalLen };
|
||||
}
|
||||
|
||||
// ========== Configuration ==========
|
||||
|
||||
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
|
||||
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
|
||||
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
|
||||
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
|
||||
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
|
||||
const STATE_DIR = path.join(SESSION_DIR, 'state');
|
||||
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
||||
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
|
||||
};
|
||||
|
||||
// ========== Templates and Constants ==========
|
||||
|
||||
const WAITING_PAGE = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
|
||||
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }</style>
|
||||
</head>
|
||||
<body><h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for the agent to push a screen...</p></body></html>`;
|
||||
|
||||
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function isFullDocument(html) {
|
||||
const trimmed = html.trimStart().toLowerCase();
|
||||
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
|
||||
}
|
||||
|
||||
function wrapInFrame(content) {
|
||||
return frameTemplate.replace('<!-- CONTENT -->', content);
|
||||
}
|
||||
|
||||
function getNewestScreen() {
|
||||
const files = fs.readdirSync(CONTENT_DIR)
|
||||
.filter(f => f.endsWith('.html'))
|
||||
.map(f => {
|
||||
const fp = path.join(CONTENT_DIR, f);
|
||||
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
|
||||
})
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
return files.length > 0 ? files[0].path : null;
|
||||
}
|
||||
|
||||
// ========== HTTP Request Handler ==========
|
||||
|
||||
function handleRequest(req, res) {
|
||||
touchActivity();
|
||||
if (req.method === 'GET' && req.url === '/') {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
: WAITING_PAGE;
|
||||
|
||||
if (html.includes('</body>')) {
|
||||
html = html.replace('</body>', helperInjection + '\n</body>');
|
||||
} else {
|
||||
html += helperInjection;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
|
||||
const fileName = req.url.slice(7);
|
||||
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(fs.readFileSync(filePath));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== WebSocket Connection Handling ==========
|
||||
|
||||
const clients = new Set();
|
||||
|
||||
function handleUpgrade(req, socket) {
|
||||
const key = req.headers['sec-websocket-key'];
|
||||
if (!key) { socket.destroy(); return; }
|
||||
|
||||
const accept = computeAcceptKey(key);
|
||||
socket.write(
|
||||
'HTTP/1.1 101 Switching Protocols\r\n' +
|
||||
'Upgrade: websocket\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
|
||||
);
|
||||
|
||||
let buffer = Buffer.alloc(0);
|
||||
clients.add(socket);
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
while (buffer.length > 0) {
|
||||
let result;
|
||||
try {
|
||||
result = decodeFrame(buffer);
|
||||
} catch (e) {
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
}
|
||||
if (!result) break;
|
||||
buffer = buffer.slice(result.bytesConsumed);
|
||||
|
||||
switch (result.opcode) {
|
||||
case OPCODES.TEXT:
|
||||
handleMessage(result.payload.toString());
|
||||
break;
|
||||
case OPCODES.CLOSE:
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
case OPCODES.PING:
|
||||
socket.write(encodeFrame(OPCODES.PONG, result.payload));
|
||||
break;
|
||||
case OPCODES.PONG:
|
||||
break;
|
||||
default: {
|
||||
const closeBuf = Buffer.alloc(2);
|
||||
closeBuf.writeUInt16BE(1003);
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, closeBuf));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => clients.delete(socket));
|
||||
socket.on('error', () => clients.delete(socket));
|
||||
}
|
||||
|
||||
function handleMessage(text) {
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e.message);
|
||||
return;
|
||||
}
|
||||
touchActivity();
|
||||
console.log(JSON.stringify({ source: 'user-event', ...event }));
|
||||
if (event.choice) {
|
||||
const eventsFile = path.join(STATE_DIR, 'events');
|
||||
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(msg) {
|
||||
const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
|
||||
for (const socket of clients) {
|
||||
try { socket.write(frame); } catch (e) { clients.delete(socket); }
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Activity Tracking ==========
|
||||
|
||||
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
||||
let lastActivity = Date.now();
|
||||
|
||||
function touchActivity() {
|
||||
lastActivity = Date.now();
|
||||
}
|
||||
|
||||
// ========== File Watching ==========
|
||||
|
||||
const debounceTimers = new Map();
|
||||
|
||||
// ========== Server Startup ==========
|
||||
|
||||
function startServer() {
|
||||
if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true });
|
||||
if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||
|
||||
// Track known files to distinguish new screens from updates.
|
||||
// macOS fs.watch reports 'rename' for both new files and overwrites,
|
||||
// so we can't rely on eventType alone.
|
||||
const knownFiles = new Set(
|
||||
fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
|
||||
);
|
||||
|
||||
const server = http.createServer(handleRequest);
|
||||
server.on('upgrade', handleUpgrade);
|
||||
|
||||
const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => {
|
||||
if (!filename || !filename.endsWith('.html')) return;
|
||||
|
||||
if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
|
||||
debounceTimers.set(filename, setTimeout(() => {
|
||||
debounceTimers.delete(filename);
|
||||
const filePath = path.join(CONTENT_DIR, filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) return; // file was deleted
|
||||
touchActivity();
|
||||
|
||||
if (!knownFiles.has(filename)) {
|
||||
knownFiles.add(filename);
|
||||
const eventsFile = path.join(STATE_DIR, 'events');
|
||||
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
|
||||
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
|
||||
} else {
|
||||
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
|
||||
}
|
||||
|
||||
broadcast({ type: 'reload' });
|
||||
}, 100));
|
||||
});
|
||||
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
|
||||
|
||||
function shutdown(reason) {
|
||||
console.log(JSON.stringify({ type: 'server-stopped', reason }));
|
||||
const infoFile = path.join(STATE_DIR, 'server-info');
|
||||
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
|
||||
fs.writeFileSync(
|
||||
path.join(STATE_DIR, 'server-stopped'),
|
||||
JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
|
||||
);
|
||||
watcher.close();
|
||||
clearInterval(lifecycleCheck);
|
||||
server.close(() => process.exit(0));
|
||||
}
|
||||
|
||||
function ownerAlive() {
|
||||
if (!ownerPid) return true;
|
||||
try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
||||
}
|
||||
|
||||
// Check every 60s: exit if owner process died or idle for 30 minutes
|
||||
const lifecycleCheck = setInterval(() => {
|
||||
if (!ownerAlive()) shutdown('owner process exited');
|
||||
else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout');
|
||||
}, 60 * 1000);
|
||||
lifecycleCheck.unref();
|
||||
|
||||
// Validate owner PID at startup. If it's already dead, the PID resolution
|
||||
// was wrong (common on WSL, Tailscale SSH, and cross-user scenarios).
|
||||
// Disable monitoring and rely on the idle timeout instead.
|
||||
if (ownerPid) {
|
||||
try { process.kill(ownerPid, 0); }
|
||||
catch (e) {
|
||||
if (e.code !== 'EPERM') {
|
||||
console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' }));
|
||||
ownerPid = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
const info = JSON.stringify({
|
||||
type: 'server-started', port: Number(PORT), host: HOST,
|
||||
url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
|
||||
screen_dir: CONTENT_DIR, state_dir: STATE_DIR
|
||||
});
|
||||
console.log(info);
|
||||
fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
startServer();
|
||||
}
|
||||
|
||||
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the brainstorm server and output connection info
|
||||
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
|
||||
#
|
||||
# Starts server on a random high port, outputs JSON with URL.
|
||||
# Each session gets its own directory to avoid conflicts.
|
||||
#
|
||||
# Options:
|
||||
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
|
||||
# instead of /tmp. Files persist after server stops.
|
||||
# --host <bind-host> Host/interface to bind (default: 127.0.0.1).
|
||||
# Use 0.0.0.0 in remote/containerized environments.
|
||||
# --url-host <host> Hostname shown in returned URL JSON.
|
||||
# --foreground Run server in the current terminal (no backgrounding).
|
||||
# --background Force background mode (overrides Codex auto-foreground).
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Parse arguments
|
||||
PROJECT_DIR=""
|
||||
FOREGROUND="false"
|
||||
FORCE_BACKGROUND="false"
|
||||
BIND_HOST="127.0.0.1"
|
||||
URL_HOST=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project-dir)
|
||||
PROJECT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
BIND_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--url-host)
|
||||
URL_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--foreground|--no-daemon)
|
||||
FOREGROUND="true"
|
||||
shift
|
||||
;;
|
||||
--background|--daemon)
|
||||
FORCE_BACKGROUND="true"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "{\"error\": \"Unknown argument: $1\"}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$URL_HOST" ]]; then
|
||||
if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
|
||||
URL_HOST="localhost"
|
||||
else
|
||||
URL_HOST="$BIND_HOST"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Some environments reap detached/background processes. Auto-foreground when detected.
|
||||
if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
|
||||
FOREGROUND="true"
|
||||
fi
|
||||
|
||||
# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected.
|
||||
if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
|
||||
case "${OSTYPE:-}" in
|
||||
msys*|cygwin*|mingw*) FOREGROUND="true" ;;
|
||||
esac
|
||||
if [[ -n "${MSYSTEM:-}" ]]; then
|
||||
FOREGROUND="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate unique session directory
|
||||
SESSION_ID="$$-$(date +%s)"
|
||||
|
||||
if [[ -n "$PROJECT_DIR" ]]; then
|
||||
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
|
||||
else
|
||||
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
|
||||
fi
|
||||
|
||||
STATE_DIR="${SESSION_DIR}/state"
|
||||
PID_FILE="${STATE_DIR}/server.pid"
|
||||
LOG_FILE="${STATE_DIR}/server.log"
|
||||
|
||||
# Create fresh session directory with content and state peers
|
||||
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
|
||||
|
||||
# Kill any existing server
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
old_pid=$(cat "$PID_FILE")
|
||||
kill "$old_pid" 2>/dev/null
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Resolve the harness PID (grandparent of this script).
|
||||
# $PPID is the ephemeral shell the harness spawned to run us — it dies
|
||||
# when this script exits. The harness itself is $PPID's parent.
|
||||
OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')"
|
||||
if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
|
||||
OWNER_PID="$PPID"
|
||||
fi
|
||||
|
||||
# Foreground mode for environments that reap detached/background processes.
|
||||
if [[ "$FOREGROUND" == "true" ]]; then
|
||||
echo "$$" > "$PID_FILE"
|
||||
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Start server, capturing output to log file
|
||||
# Use nohup to survive shell exit; disown to remove from job table
|
||||
nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
disown "$SERVER_PID" 2>/dev/null
|
||||
echo "$SERVER_PID" > "$PID_FILE"
|
||||
|
||||
# Wait for server-started message (check log file)
|
||||
for i in {1..50}; do
|
||||
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
|
||||
# Verify server is still alive after a short window (catches process reapers)
|
||||
alive="true"
|
||||
for _ in {1..20}; do
|
||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
alive="false"
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if [[ "$alive" != "true" ]]; then
|
||||
echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}"
|
||||
exit 1
|
||||
fi
|
||||
grep "server-started" "$LOG_FILE" | head -1
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# Timeout - server didn't start
|
||||
echo '{"error": "Server failed to start within 5 seconds"}'
|
||||
exit 1
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop the brainstorm server and clean up
|
||||
# Usage: stop-server.sh <session_dir>
|
||||
#
|
||||
# Kills the server process. Only deletes session directory if it's
|
||||
# under /tmp (ephemeral). Persistent directories (.superpowers/) are
|
||||
# kept so mockups can be reviewed later.
|
||||
|
||||
SESSION_DIR="$1"
|
||||
|
||||
if [[ -z "$SESSION_DIR" ]]; then
|
||||
echo '{"error": "Usage: stop-server.sh <session_dir>"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATE_DIR="${SESSION_DIR}/state"
|
||||
PID_FILE="${STATE_DIR}/server.pid"
|
||||
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
pid=$(cat "$PID_FILE")
|
||||
|
||||
# Try to stop gracefully, fallback to force if still alive
|
||||
kill "$pid" 2>/dev/null || true
|
||||
|
||||
# Wait for graceful shutdown (up to ~2s)
|
||||
for i in {1..20}; do
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# If still running, escalate to SIGKILL
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
|
||||
# Give SIGKILL a moment to take effect
|
||||
sleep 0.1
|
||||
fi
|
||||
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo '{"status": "failed", "error": "process still running"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$PID_FILE" "${STATE_DIR}/server.log"
|
||||
|
||||
# Only delete ephemeral /tmp directories
|
||||
if [[ "$SESSION_DIR" == /tmp/* ]]; then
|
||||
rm -rf "$SESSION_DIR"
|
||||
fi
|
||||
|
||||
echo '{"status": "stopped"}'
|
||||
else
|
||||
echo '{"status": "not_running"}'
|
||||
fi
|
||||
@@ -0,0 +1,49 @@
|
||||
# Spec Document Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a spec document reviewer subagent.
|
||||
|
||||
**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning.
|
||||
|
||||
**Dispatch after:** Spec document is written to docs/superpowers/specs/
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review spec document"
|
||||
prompt: |
|
||||
You are a spec document reviewer. Verify this spec is complete and ready for planning.
|
||||
|
||||
**Spec to review:** [SPEC_FILE_PATH]
|
||||
|
||||
## What to Check
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
|
||||
| Consistency | Internal contradictions, conflicting requirements |
|
||||
| Clarity | Requirements ambiguous enough to cause someone to build the wrong thing |
|
||||
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
|
||||
| YAGNI | Unrequested features, over-engineering |
|
||||
|
||||
## Calibration
|
||||
|
||||
**Only flag issues that would cause real problems during implementation planning.**
|
||||
A missing section, a contradiction, or a requirement so ambiguous it could be
|
||||
interpreted two different ways — those are issues. Minor wording improvements,
|
||||
stylistic preferences, and "sections less detailed than others" are not.
|
||||
|
||||
Approve unless there are serious gaps that would lead to a flawed plan.
|
||||
|
||||
## Output Format
|
||||
|
||||
## Spec Review
|
||||
|
||||
**Status:** Approved | Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Section X]: [specific issue] - [why it matters for planning]
|
||||
|
||||
**Recommendations (advisory, do not block approval):**
|
||||
- [suggestions for improvement]
|
||||
```
|
||||
|
||||
**Reviewer returns:** Status, Issues (if any), Recommendations
|
||||
@@ -0,0 +1,287 @@
|
||||
# Visual Companion Guide
|
||||
|
||||
Browser-based visual brainstorming companion for showing mockups, diagrams, and options.
|
||||
|
||||
## When to Use
|
||||
|
||||
Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?**
|
||||
|
||||
**Use the browser** when the content itself is visual:
|
||||
|
||||
- **UI mockups** — wireframes, layouts, navigation structures, component designs
|
||||
- **Architecture diagrams** — system components, data flow, relationship maps
|
||||
- **Side-by-side visual comparisons** — comparing two layouts, two color schemes, two design directions
|
||||
- **Design polish** — when the question is about look and feel, spacing, visual hierarchy
|
||||
- **Spatial relationships** — state machines, flowcharts, entity relationships rendered as diagrams
|
||||
|
||||
**Use the terminal** when the content is text or tabular:
|
||||
|
||||
- **Requirements and scope questions** — "what does X mean?", "which features are in scope?"
|
||||
- **Conceptual A/B/C choices** — picking between approaches described in words
|
||||
- **Tradeoff lists** — pros/cons, comparison tables
|
||||
- **Technical decisions** — API design, data modeling, architectural approach selection
|
||||
- **Clarifying questions** — anything where the answer is words, not a visual preference
|
||||
|
||||
A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual — use the terminal. "Which of these wizard layouts feels right?" is visual — use the browser.
|
||||
|
||||
## How It Works
|
||||
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
|
||||
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
|
||||
## Starting a Session
|
||||
|
||||
```bash
|
||||
# Start server with persistence (mockups saved to project)
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
|
||||
# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341",
|
||||
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
|
||||
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}
|
||||
```
|
||||
|
||||
Save `screen_dir` and `state_dir` from the response. Tell user to open the URL.
|
||||
|
||||
**Finding connection info:** The server writes its startup JSON to `$STATE_DIR/server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `<project>/.superpowers/brainstorm/` for the session directory.
|
||||
|
||||
**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there.
|
||||
|
||||
**Launching the server by platform:**
|
||||
|
||||
**Claude Code (macOS / Linux):**
|
||||
```bash
|
||||
# Default mode works — the script backgrounds the server itself
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
|
||||
**Claude Code (Windows):**
|
||||
```bash
|
||||
# Windows auto-detects and uses foreground mode, which blocks the tool call.
|
||||
# Use run_in_background: true on the Bash tool call so the server survives
|
||||
# across conversation turns.
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
When calling this via the Bash tool, set `run_in_background: true`. Then read `$STATE_DIR/server-info` on the next turn to get the URL and port.
|
||||
|
||||
**Codex:**
|
||||
```bash
|
||||
# Codex reaps background processes. The script auto-detects CODEX_CI and
|
||||
# switches to foreground mode. Run it normally — no extra flags needed.
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
|
||||
**Gemini CLI:**
|
||||
```bash
|
||||
# Use --foreground and set is_background: true on your shell tool call
|
||||
# so the process survives across turns
|
||||
scripts/start-server.sh --project-dir /path/to/project --foreground
|
||||
```
|
||||
|
||||
**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism.
|
||||
|
||||
If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host:
|
||||
|
||||
```bash
|
||||
scripts/start-server.sh \
|
||||
--project-dir /path/to/project \
|
||||
--host 0.0.0.0 \
|
||||
--url-host localhost
|
||||
```
|
||||
|
||||
Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
## The Loop
|
||||
|
||||
1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`:
|
||||
- Before each write, check that `$STATE_DIR/server-info` exists. If it doesn't (or `$STATE_DIR/server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity.
|
||||
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- **Never reuse filenames** — each screen gets a fresh file
|
||||
- Use Write tool — **never use cat/heredoc** (dumps noise into terminal)
|
||||
- Server automatically serves the newest file
|
||||
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
|
||||
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
|
||||
|
||||
3. **On your next turn** — after the user responds in the terminal:
|
||||
- Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
|
||||
- Merge with the user's terminal text to get the full picture
|
||||
- The terminal message is the primary feedback; `state_dir/events` provides structured interaction data
|
||||
|
||||
4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated.
|
||||
|
||||
5. **Unload when returning to terminal** — when the next step doesn't need the browser (e.g., a clarifying question, a tradeoff discussion), push a waiting screen to clear the stale content:
|
||||
|
||||
```html
|
||||
<!-- filename: waiting.html (or waiting-2.html, etc.) -->
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual.
|
||||
|
||||
6. Repeat until done.
|
||||
|
||||
## Writing Content Fragments
|
||||
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
|
||||
|
||||
**Minimal example:**
|
||||
|
||||
```html
|
||||
<h2>Which layout works better?</h2>
|
||||
<p class="subtitle">Consider readability and visual hierarchy</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Single Column</h3>
|
||||
<p>Clean, focused reading experience</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>Two Column</h3>
|
||||
<p>Sidebar navigation with main content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
That's it. No `<html>`, no CSS, no `<script>` tags needed. The server provides all of that.
|
||||
|
||||
## CSS Classes Available
|
||||
|
||||
The frame template provides these CSS classes for your content:
|
||||
|
||||
### Options (A/B/C choices)
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Title</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item. The indicator bar shows the count.
|
||||
|
||||
```html
|
||||
<div class="options" data-multiselect>
|
||||
<!-- same option markup — users can select/deselect multiple -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Cards (visual designs)
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
|
||||
<div class="card-image"><!-- mockup content --></div>
|
||||
<div class="card-body">
|
||||
<h3>Name</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mockup container
|
||||
|
||||
```html
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Preview: Dashboard Layout</div>
|
||||
<div class="mockup-body"><!-- your mockup HTML --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Split view (side-by-side)
|
||||
|
||||
```html
|
||||
<div class="split">
|
||||
<div class="mockup"><!-- left --></div>
|
||||
<div class="mockup"><!-- right --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pros/Cons
|
||||
|
||||
```html
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Benefit</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Drawback</li></ul></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mock elements (wireframe building blocks)
|
||||
|
||||
```html
|
||||
<div class="mock-nav">Logo | Home | About | Contact</div>
|
||||
<div style="display: flex;">
|
||||
<div class="mock-sidebar">Navigation</div>
|
||||
<div class="mock-content">Main content area</div>
|
||||
</div>
|
||||
<button class="mock-button">Action Button</button>
|
||||
<input class="mock-input" placeholder="Input field">
|
||||
<div class="placeholder">Placeholder area</div>
|
||||
```
|
||||
|
||||
### Typography and sections
|
||||
|
||||
- `h2` — page title
|
||||
- `h3` — section heading
|
||||
- `.subtitle` — secondary text below title
|
||||
- `.section` — content block with bottom margin
|
||||
- `.label` — small uppercase label text
|
||||
|
||||
## Browser Events Format
|
||||
|
||||
When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen.
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
|
||||
{"type":"click","choice":"c","text":"Option C - Complex Grid","timestamp":1706000108}
|
||||
{"type":"click","choice":"b","text":"Option B - Hybrid","timestamp":1706000115}
|
||||
```
|
||||
|
||||
The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about.
|
||||
|
||||
If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
|
||||
|
||||
## Design Tips
|
||||
|
||||
- **Scale fidelity to the question** — wireframes for layout, polish for polish questions
|
||||
- **Explain the question on each page** — "Which layout feels more professional?" not just "Pick one"
|
||||
- **Iterate before advancing** — if feedback changes current screen, write a new version
|
||||
- **2-4 options max** per screen
|
||||
- **Use real content when it matters** — for a photography portfolio, use actual images (Unsplash). Placeholder content obscures design issues.
|
||||
- **Keep mockups simple** — focus on layout and structure, not pixel-perfect design
|
||||
|
||||
## File Naming
|
||||
|
||||
- Use semantic names: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- Never reuse filenames — each screen must be a new file
|
||||
- For iterations: append version suffix like `layout-v2.html`, `layout-v3.html`
|
||||
- Server serves newest file by modification time
|
||||
|
||||
## Cleaning Up
|
||||
|
||||
```bash
|
||||
scripts/stop-server.sh $SESSION_DIR
|
||||
```
|
||||
|
||||
If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop.
|
||||
|
||||
## Reference
|
||||
|
||||
- Frame template (CSS reference): `scripts/frame-template.html`
|
||||
- Helper script (client-side): `scripts/helper.js`
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: dispatching-parallel-agents
|
||||
description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
|
||||
---
|
||||
|
||||
# Dispatching Parallel Agents
|
||||
|
||||
## Overview
|
||||
|
||||
You delegate tasks to specialized agents with isolated context. By precisely crafting their instructions and context, you ensure they stay focused and succeed at their task. They should never inherit your session's context or history — you construct exactly what they need. This also preserves your own context for coordination work.
|
||||
|
||||
When you have multiple unrelated failures (different test files, different subsystems, different bugs), investigating them sequentially wastes time. Each investigation is independent and can happen in parallel.
|
||||
|
||||
**Core principle:** Dispatch one agent per independent problem domain. Let them work concurrently.
|
||||
|
||||
## When to Use
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Multiple failures?" [shape=diamond];
|
||||
"Are they independent?" [shape=diamond];
|
||||
"Single agent investigates all" [shape=box];
|
||||
"One agent per problem domain" [shape=box];
|
||||
"Can they work in parallel?" [shape=diamond];
|
||||
"Sequential agents" [shape=box];
|
||||
"Parallel dispatch" [shape=box];
|
||||
|
||||
"Multiple failures?" -> "Are they independent?" [label="yes"];
|
||||
"Are they independent?" -> "Single agent investigates all" [label="no - related"];
|
||||
"Are they independent?" -> "Can they work in parallel?" [label="yes"];
|
||||
"Can they work in parallel?" -> "Parallel dispatch" [label="yes"];
|
||||
"Can they work in parallel?" -> "Sequential agents" [label="no - shared state"];
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- 3+ test files failing with different root causes
|
||||
- Multiple subsystems broken independently
|
||||
- Each problem can be understood without context from others
|
||||
- No shared state between investigations
|
||||
|
||||
**Don't use when:**
|
||||
- Failures are related (fix one might fix others)
|
||||
- Need to understand full system state
|
||||
- Agents would interfere with each other
|
||||
|
||||
## The Pattern
|
||||
|
||||
### 1. Identify Independent Domains
|
||||
|
||||
Group failures by what's broken:
|
||||
- File A tests: Tool approval flow
|
||||
- File B tests: Batch completion behavior
|
||||
- File C tests: Abort functionality
|
||||
|
||||
Each domain is independent - fixing tool approval doesn't affect abort tests.
|
||||
|
||||
### 2. Create Focused Agent Tasks
|
||||
|
||||
Each agent gets:
|
||||
- **Specific scope:** One test file or subsystem
|
||||
- **Clear goal:** Make these tests pass
|
||||
- **Constraints:** Don't change other code
|
||||
- **Expected output:** Summary of what you found and fixed
|
||||
|
||||
### 3. Dispatch in Parallel
|
||||
|
||||
```typescript
|
||||
// In Claude Code / AI environment
|
||||
Task("Fix agent-tool-abort.test.ts failures")
|
||||
Task("Fix batch-completion-behavior.test.ts failures")
|
||||
Task("Fix tool-approval-race-conditions.test.ts failures")
|
||||
// All three run concurrently
|
||||
```
|
||||
|
||||
### 4. Review and Integrate
|
||||
|
||||
When agents return:
|
||||
- Read each summary
|
||||
- Verify fixes don't conflict
|
||||
- Run full test suite
|
||||
- Integrate all changes
|
||||
|
||||
## Agent Prompt Structure
|
||||
|
||||
Good agent prompts are:
|
||||
1. **Focused** - One clear problem domain
|
||||
2. **Self-contained** - All context needed to understand the problem
|
||||
3. **Specific about output** - What should the agent return?
|
||||
|
||||
```markdown
|
||||
Fix the 3 failing tests in src/agents/agent-tool-abort.test.ts:
|
||||
|
||||
1. "should abort tool with partial output capture" - expects 'interrupted at' in message
|
||||
2. "should handle mixed completed and aborted tools" - fast tool aborted instead of completed
|
||||
3. "should properly track pendingToolCount" - expects 3 results but gets 0
|
||||
|
||||
These are timing/race condition issues. Your task:
|
||||
|
||||
1. Read the test file and understand what each test verifies
|
||||
2. Identify root cause - timing issues or actual bugs?
|
||||
3. Fix by:
|
||||
- Replacing arbitrary timeouts with event-based waiting
|
||||
- Fixing bugs in abort implementation if found
|
||||
- Adjusting test expectations if testing changed behavior
|
||||
|
||||
Do NOT just increase timeouts - find the real issue.
|
||||
|
||||
Return: Summary of what you found and what you fixed.
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**❌ Too broad:** "Fix all the tests" - agent gets lost
|
||||
**✅ Specific:** "Fix agent-tool-abort.test.ts" - focused scope
|
||||
|
||||
**❌ No context:** "Fix the race condition" - agent doesn't know where
|
||||
**✅ Context:** Paste the error messages and test names
|
||||
|
||||
**❌ No constraints:** Agent might refactor everything
|
||||
**✅ Constraints:** "Do NOT change production code" or "Fix tests only"
|
||||
|
||||
**❌ Vague output:** "Fix it" - you don't know what changed
|
||||
**✅ Specific:** "Return summary of root cause and changes"
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
**Related failures:** Fixing one might fix others - investigate together first
|
||||
**Need full context:** Understanding requires seeing entire system
|
||||
**Exploratory debugging:** You don't know what's broken yet
|
||||
**Shared state:** Agents would interfere (editing same files, using same resources)
|
||||
|
||||
## Real Example from Session
|
||||
|
||||
**Scenario:** 6 test failures across 3 files after major refactoring
|
||||
|
||||
**Failures:**
|
||||
- agent-tool-abort.test.ts: 3 failures (timing issues)
|
||||
- batch-completion-behavior.test.ts: 2 failures (tools not executing)
|
||||
- tool-approval-race-conditions.test.ts: 1 failure (execution count = 0)
|
||||
|
||||
**Decision:** Independent domains - abort logic separate from batch completion separate from race conditions
|
||||
|
||||
**Dispatch:**
|
||||
```
|
||||
Agent 1 → Fix agent-tool-abort.test.ts
|
||||
Agent 2 → Fix batch-completion-behavior.test.ts
|
||||
Agent 3 → Fix tool-approval-race-conditions.test.ts
|
||||
```
|
||||
|
||||
**Results:**
|
||||
- Agent 1: Replaced timeouts with event-based waiting
|
||||
- Agent 2: Fixed event structure bug (threadId in wrong place)
|
||||
- Agent 3: Added wait for async tool execution to complete
|
||||
|
||||
**Integration:** All fixes independent, no conflicts, full suite green
|
||||
|
||||
**Time saved:** 3 problems solved in parallel vs sequentially
|
||||
|
||||
## Key Benefits
|
||||
|
||||
1. **Parallelization** - Multiple investigations happen simultaneously
|
||||
2. **Focus** - Each agent has narrow scope, less context to track
|
||||
3. **Independence** - Agents don't interfere with each other
|
||||
4. **Speed** - 3 problems solved in time of 1
|
||||
|
||||
## Verification
|
||||
|
||||
After agents return:
|
||||
1. **Review each summary** - Understand what changed
|
||||
2. **Check for conflicts** - Did agents edit same code?
|
||||
3. **Run full suite** - Verify all fixes work together
|
||||
4. **Spot check** - Agents can make systematic errors
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging session (2025-10-03):
|
||||
- 6 failures across 3 files
|
||||
- 3 agents dispatched in parallel
|
||||
- All investigations completed concurrently
|
||||
- All fixes integrated successfully
|
||||
- Zero conflicts between agent changes
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: executing-plans
|
||||
description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
|
||||
---
|
||||
|
||||
# Executing Plans
|
||||
|
||||
## Overview
|
||||
|
||||
Load plan, review critically, execute all tasks, report when complete.
|
||||
|
||||
**Announce at start:** "I'm using the executing-plans skill to implement this plan."
|
||||
|
||||
**Note:** Tell your human partner that Superpowers works much better with access to subagents. The quality of its work will be significantly higher if run on a platform with subagent support (such as Claude Code or Codex). If subagents are available, use superpowers:subagent-driven-development instead of this skill.
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Load and Review Plan
|
||||
1. Read plan file
|
||||
2. Review critically - identify any questions or concerns about the plan
|
||||
3. If concerns: Raise them with your human partner before starting
|
||||
4. If no concerns: Create TodoWrite and proceed
|
||||
|
||||
### Step 2: Execute Tasks
|
||||
|
||||
For each task:
|
||||
1. Mark as in_progress
|
||||
2. Follow each step exactly (plan has bite-sized steps)
|
||||
3. Run verifications as specified
|
||||
4. Mark as completed
|
||||
|
||||
### Step 3: Complete Development
|
||||
|
||||
After all tasks complete and verified:
|
||||
- Announce: "I'm using the finishing-a-development-branch skill to complete this work."
|
||||
- **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch
|
||||
- Follow that skill to verify tests, present options, execute choice
|
||||
|
||||
## When to Stop and Ask for Help
|
||||
|
||||
**STOP executing immediately when:**
|
||||
- Hit a blocker (missing dependency, test fails, instruction unclear)
|
||||
- Plan has critical gaps preventing starting
|
||||
- You don't understand an instruction
|
||||
- Verification fails repeatedly
|
||||
|
||||
**Ask for clarification rather than guessing.**
|
||||
|
||||
## When to Revisit Earlier Steps
|
||||
|
||||
**Return to Review (Step 1) when:**
|
||||
- Partner updates the plan based on your feedback
|
||||
- Fundamental approach needs rethinking
|
||||
|
||||
**Don't force through blockers** - stop and ask.
|
||||
|
||||
## Remember
|
||||
- Review plan critically first
|
||||
- Follow plan steps exactly
|
||||
- Don't skip verifications
|
||||
- Reference skills when plan says to
|
||||
- Stop when blocked, don't guess
|
||||
- Never start implementation on main/master branch without explicit user consent
|
||||
|
||||
## Integration
|
||||
|
||||
**Required workflow skills:**
|
||||
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
|
||||
- **superpowers:writing-plans** - Creates the plan this skill executes
|
||||
- **superpowers:finishing-a-development-branch** - Complete development after all tasks
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
name: finishing-a-development-branch
|
||||
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
|
||||
---
|
||||
|
||||
# Finishing a Development Branch
|
||||
|
||||
## Overview
|
||||
|
||||
Guide completion of development work by presenting clear options and handling chosen workflow.
|
||||
|
||||
**Core principle:** Verify tests → Detect environment → Present options → Execute choice → Clean up.
|
||||
|
||||
**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work."
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Verify Tests
|
||||
|
||||
**Before presenting options, verify tests pass:**
|
||||
|
||||
```bash
|
||||
# Run project's test suite
|
||||
npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**If tests fail:**
|
||||
```
|
||||
Tests failing (<N> failures). Must fix before completing:
|
||||
|
||||
[Show failures]
|
||||
|
||||
Cannot proceed with merge/PR until tests pass.
|
||||
```
|
||||
|
||||
Stop. Don't proceed to Step 2.
|
||||
|
||||
**If tests pass:** Continue to Step 2.
|
||||
|
||||
### Step 2: Detect Environment
|
||||
|
||||
**Determine workspace state before presenting options:**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
```
|
||||
|
||||
This determines which menu to show and how cleanup works:
|
||||
|
||||
| State | Menu | Cleanup |
|
||||
|-------|------|---------|
|
||||
| `GIT_DIR == GIT_COMMON` (normal repo) | Standard 4 options | No worktree to clean up |
|
||||
| `GIT_DIR != GIT_COMMON`, named branch | Standard 4 options | Provenance-based (see Step 6) |
|
||||
| `GIT_DIR != GIT_COMMON`, detached HEAD | Reduced 3 options (no merge) | No cleanup (externally managed) |
|
||||
|
||||
### Step 3: Determine Base Branch
|
||||
|
||||
```bash
|
||||
# Try common base branches
|
||||
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
|
||||
```
|
||||
|
||||
Or ask: "This branch split from main - is that correct?"
|
||||
|
||||
### Step 4: Present Options
|
||||
|
||||
**Normal repo and named-branch worktree — present exactly these 4 options:**
|
||||
|
||||
```
|
||||
Implementation complete. What would you like to do?
|
||||
|
||||
1. Merge back to <base-branch> locally
|
||||
2. Push and create a Pull Request
|
||||
3. Keep the branch as-is (I'll handle it later)
|
||||
4. Discard this work
|
||||
|
||||
Which option?
|
||||
```
|
||||
|
||||
**Detached HEAD — present exactly these 3 options:**
|
||||
|
||||
```
|
||||
Implementation complete. You're on a detached HEAD (externally managed workspace).
|
||||
|
||||
1. Push as new branch and create a Pull Request
|
||||
2. Keep as-is (I'll handle it later)
|
||||
3. Discard this work
|
||||
|
||||
Which option?
|
||||
```
|
||||
|
||||
**Don't add explanation** - keep options concise.
|
||||
|
||||
### Step 5: Execute Choice
|
||||
|
||||
#### Option 1: Merge Locally
|
||||
|
||||
```bash
|
||||
# Get main repo root for CWD safety
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
|
||||
# Merge first — verify success before removing anything
|
||||
git checkout <base-branch>
|
||||
git pull
|
||||
git merge <feature-branch>
|
||||
|
||||
# Verify tests on merged result
|
||||
<test command>
|
||||
|
||||
# Only after merge succeeds: cleanup worktree (Step 6), then delete branch
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 6), then delete branch:
|
||||
|
||||
```bash
|
||||
git branch -d <feature-branch>
|
||||
```
|
||||
|
||||
#### Option 2: Push and Create PR
|
||||
|
||||
```bash
|
||||
# Push branch
|
||||
git push -u origin <feature-branch>
|
||||
|
||||
# Create PR
|
||||
gh pr create --title "<title>" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<2-3 bullets of what changed>
|
||||
|
||||
## Test Plan
|
||||
- [ ] <verification steps>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
|
||||
|
||||
#### Option 3: Keep As-Is
|
||||
|
||||
Report: "Keeping branch <name>. Worktree preserved at <path>."
|
||||
|
||||
**Don't cleanup worktree.**
|
||||
|
||||
#### Option 4: Discard
|
||||
|
||||
**Confirm first:**
|
||||
```
|
||||
This will permanently delete:
|
||||
- Branch <name>
|
||||
- All commits: <commit-list>
|
||||
- Worktree at <path>
|
||||
|
||||
Type 'discard' to confirm.
|
||||
```
|
||||
|
||||
Wait for exact confirmation.
|
||||
|
||||
If confirmed:
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 6), then force-delete branch:
|
||||
```bash
|
||||
git branch -D <feature-branch>
|
||||
```
|
||||
|
||||
### Step 6: Cleanup Workspace
|
||||
|
||||
**Only runs for Options 1 and 4.** Options 2 and 3 always preserve the worktree.
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
||||
```
|
||||
|
||||
**If `GIT_DIR == GIT_COMMON`:** Normal repo, no worktree to clean up. Done.
|
||||
|
||||
**If worktree path is under `.worktrees/`, `worktrees/`, or `~/.config/superpowers/worktrees/`:** Superpowers created this worktree — we own cleanup.
|
||||
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
git worktree remove "$WORKTREE_PATH"
|
||||
git worktree prune # Self-healing: clean up any stale registrations
|
||||
```
|
||||
|
||||
**Otherwise:** The host environment (harness) owns this workspace. Do NOT remove it. If your platform provides a workspace-exit tool, use it. Otherwise, leave the workspace in place.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|
||||
|--------|-------|------|---------------|----------------|
|
||||
| 1. Merge locally | yes | - | - | yes |
|
||||
| 2. Create PR | - | yes | yes | - |
|
||||
| 3. Keep as-is | - | - | yes | - |
|
||||
| 4. Discard | - | - | - | yes (force) |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Skipping test verification**
|
||||
- **Problem:** Merge broken code, create failing PR
|
||||
- **Fix:** Always verify tests before offering options
|
||||
|
||||
**Open-ended questions**
|
||||
- **Problem:** "What should I do next?" is ambiguous
|
||||
- **Fix:** Present exactly 4 structured options (or 3 for detached HEAD)
|
||||
|
||||
**Cleaning up worktree for Option 2**
|
||||
- **Problem:** Remove worktree user needs for PR iteration
|
||||
- **Fix:** Only cleanup for Options 1 and 4
|
||||
|
||||
**Deleting branch before removing worktree**
|
||||
- **Problem:** `git branch -d` fails because worktree still references the branch
|
||||
- **Fix:** Merge first, remove worktree, then delete branch
|
||||
|
||||
**Running git worktree remove from inside the worktree**
|
||||
- **Problem:** Command fails silently when CWD is inside the worktree being removed
|
||||
- **Fix:** Always `cd` to main repo root before `git worktree remove`
|
||||
|
||||
**Cleaning up harness-owned worktrees**
|
||||
- **Problem:** Removing a worktree the harness created causes phantom state
|
||||
- **Fix:** Only clean up worktrees under `.worktrees/`, `worktrees/`, or `~/.config/superpowers/worktrees/`
|
||||
|
||||
**No confirmation for discard**
|
||||
- **Problem:** Accidentally delete work
|
||||
- **Fix:** Require typed "discard" confirmation
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Proceed with failing tests
|
||||
- Merge without verifying tests on result
|
||||
- Delete work without confirmation
|
||||
- Force-push without explicit request
|
||||
- Remove a worktree before confirming merge success
|
||||
- Clean up worktrees you didn't create (provenance check)
|
||||
- Run `git worktree remove` from inside the worktree
|
||||
|
||||
**Always:**
|
||||
- Verify tests before offering options
|
||||
- Detect environment before presenting menu
|
||||
- Present exactly 4 options (or 3 for detached HEAD)
|
||||
- Get typed confirmation for Option 4
|
||||
- Clean up worktree for Options 1 & 4 only
|
||||
- `cd` to main repo root before worktree removal
|
||||
- Run `git worktree prune` after removal
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: receiving-code-review
|
||||
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
|
||||
---
|
||||
|
||||
# Code Review Reception
|
||||
|
||||
## Overview
|
||||
|
||||
Code review requires technical evaluation, not emotional performance.
|
||||
|
||||
**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.
|
||||
|
||||
## The Response Pattern
|
||||
|
||||
```
|
||||
WHEN receiving code review feedback:
|
||||
|
||||
1. READ: Complete feedback without reacting
|
||||
2. UNDERSTAND: Restate requirement in own words (or ask)
|
||||
3. VERIFY: Check against codebase reality
|
||||
4. EVALUATE: Technically sound for THIS codebase?
|
||||
5. RESPOND: Technical acknowledgment or reasoned pushback
|
||||
6. IMPLEMENT: One item at a time, test each
|
||||
```
|
||||
|
||||
## Forbidden Responses
|
||||
|
||||
**NEVER:**
|
||||
- "You're absolutely right!" (explicit CLAUDE.md violation)
|
||||
- "Great point!" / "Excellent feedback!" (performative)
|
||||
- "Let me implement that now" (before verification)
|
||||
|
||||
**INSTEAD:**
|
||||
- Restate the technical requirement
|
||||
- Ask clarifying questions
|
||||
- Push back with technical reasoning if wrong
|
||||
- Just start working (actions > words)
|
||||
|
||||
## Handling Unclear Feedback
|
||||
|
||||
```
|
||||
IF any item is unclear:
|
||||
STOP - do not implement anything yet
|
||||
ASK for clarification on unclear items
|
||||
|
||||
WHY: Items may be related. Partial understanding = wrong implementation.
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
your human partner: "Fix 1-6"
|
||||
You understand 1,2,3,6. Unclear on 4,5.
|
||||
|
||||
❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
|
||||
✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding."
|
||||
```
|
||||
|
||||
## Source-Specific Handling
|
||||
|
||||
### From your human partner
|
||||
- **Trusted** - implement after understanding
|
||||
- **Still ask** if scope unclear
|
||||
- **No performative agreement**
|
||||
- **Skip to action** or technical acknowledgment
|
||||
|
||||
### From External Reviewers
|
||||
```
|
||||
BEFORE implementing:
|
||||
1. Check: Technically correct for THIS codebase?
|
||||
2. Check: Breaks existing functionality?
|
||||
3. Check: Reason for current implementation?
|
||||
4. Check: Works on all platforms/versions?
|
||||
5. Check: Does reviewer understand full context?
|
||||
|
||||
IF suggestion seems wrong:
|
||||
Push back with technical reasoning
|
||||
|
||||
IF can't easily verify:
|
||||
Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"
|
||||
|
||||
IF conflicts with your human partner's prior decisions:
|
||||
Stop and discuss with your human partner first
|
||||
```
|
||||
|
||||
**your human partner's rule:** "External feedback - be skeptical, but check carefully"
|
||||
|
||||
## YAGNI Check for "Professional" Features
|
||||
|
||||
```
|
||||
IF reviewer suggests "implementing properly":
|
||||
grep codebase for actual usage
|
||||
|
||||
IF unused: "This endpoint isn't called. Remove it (YAGNI)?"
|
||||
IF used: Then implement properly
|
||||
```
|
||||
|
||||
**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."
|
||||
|
||||
## Implementation Order
|
||||
|
||||
```
|
||||
FOR multi-item feedback:
|
||||
1. Clarify anything unclear FIRST
|
||||
2. Then implement in this order:
|
||||
- Blocking issues (breaks, security)
|
||||
- Simple fixes (typos, imports)
|
||||
- Complex fixes (refactoring, logic)
|
||||
3. Test each fix individually
|
||||
4. Verify no regressions
|
||||
```
|
||||
|
||||
## When To Push Back
|
||||
|
||||
Push back when:
|
||||
- Suggestion breaks existing functionality
|
||||
- Reviewer lacks full context
|
||||
- Violates YAGNI (unused feature)
|
||||
- Technically incorrect for this stack
|
||||
- Legacy/compatibility reasons exist
|
||||
- Conflicts with your human partner's architectural decisions
|
||||
|
||||
**How to push back:**
|
||||
- Use technical reasoning, not defensiveness
|
||||
- Ask specific questions
|
||||
- Reference working tests/code
|
||||
- Involve your human partner if architectural
|
||||
|
||||
**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K"
|
||||
|
||||
## Acknowledging Correct Feedback
|
||||
|
||||
When feedback IS correct:
|
||||
```
|
||||
✅ "Fixed. [Brief description of what changed]"
|
||||
✅ "Good catch - [specific issue]. Fixed in [location]."
|
||||
✅ [Just fix it and show in the code]
|
||||
|
||||
❌ "You're absolutely right!"
|
||||
❌ "Great point!"
|
||||
❌ "Thanks for catching that!"
|
||||
❌ "Thanks for [anything]"
|
||||
❌ ANY gratitude expression
|
||||
```
|
||||
|
||||
**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback.
|
||||
|
||||
**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead.
|
||||
|
||||
## Gracefully Correcting Your Pushback
|
||||
|
||||
If you pushed back and were wrong:
|
||||
```
|
||||
✅ "You were right - I checked [X] and it does [Y]. Implementing now."
|
||||
✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing."
|
||||
|
||||
❌ Long apology
|
||||
❌ Defending why you pushed back
|
||||
❌ Over-explaining
|
||||
```
|
||||
|
||||
State the correction factually and move on.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Performative agreement | State requirement or just act |
|
||||
| Blind implementation | Verify against codebase first |
|
||||
| Batch without testing | One at a time, test each |
|
||||
| Assuming reviewer is right | Check if breaks things |
|
||||
| Avoiding pushback | Technical correctness > comfort |
|
||||
| Partial implementation | Clarify all items first |
|
||||
| Can't verify, proceed anyway | State limitation, ask for direction |
|
||||
|
||||
## Real Examples
|
||||
|
||||
**Performative Agreement (Bad):**
|
||||
```
|
||||
Reviewer: "Remove legacy code"
|
||||
❌ "You're absolutely right! Let me remove that..."
|
||||
```
|
||||
|
||||
**Technical Verification (Good):**
|
||||
```
|
||||
Reviewer: "Remove legacy code"
|
||||
✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?"
|
||||
```
|
||||
|
||||
**YAGNI (Good):**
|
||||
```
|
||||
Reviewer: "Implement proper metrics tracking with database, date filters, CSV export"
|
||||
✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?"
|
||||
```
|
||||
|
||||
**Unclear Item (Good):**
|
||||
```
|
||||
your human partner: "Fix items 1-6"
|
||||
You understand 1,2,3,6. Unclear on 4,5.
|
||||
✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
|
||||
```
|
||||
|
||||
## GitHub Thread Replies
|
||||
|
||||
When replying to inline review comments on GitHub, reply in the comment thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment.
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**External feedback = suggestions to evaluate, not orders to follow.**
|
||||
|
||||
Verify. Question. Then implement.
|
||||
|
||||
No performative agreement. Technical rigor always.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: requesting-code-review
|
||||
description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
|
||||
---
|
||||
|
||||
# Requesting Code Review
|
||||
|
||||
Dispatch a code reviewer subagent to catch issues before they cascade. The reviewer gets precisely crafted context for evaluation — never your session's history. This keeps the reviewer focused on the work product, not your thought process, and preserves your own context for continued work.
|
||||
|
||||
**Core principle:** Review early, review often.
|
||||
|
||||
## When to Request Review
|
||||
|
||||
**Mandatory:**
|
||||
- After each task in subagent-driven development
|
||||
- After completing major feature
|
||||
- Before merge to main
|
||||
|
||||
**Optional but valuable:**
|
||||
- When stuck (fresh perspective)
|
||||
- Before refactoring (baseline check)
|
||||
- After fixing complex bug
|
||||
|
||||
## How to Request
|
||||
|
||||
**1. Get git SHAs:**
|
||||
```bash
|
||||
BASE_SHA=$(git rev-parse HEAD~1) # or origin/main
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
```
|
||||
|
||||
**2. Dispatch code reviewer subagent:**
|
||||
|
||||
Use Task tool with `general-purpose` type, fill template at `code-reviewer.md`
|
||||
|
||||
**Placeholders:**
|
||||
- `{DESCRIPTION}` - Brief summary of what you built
|
||||
- `{PLAN_OR_REQUIREMENTS}` - What it should do
|
||||
- `{BASE_SHA}` - Starting commit
|
||||
- `{HEAD_SHA}` - Ending commit
|
||||
|
||||
**3. Act on feedback:**
|
||||
- Fix Critical issues immediately
|
||||
- Fix Important issues before proceeding
|
||||
- Note Minor issues for later
|
||||
- Push back if reviewer is wrong (with reasoning)
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
[Just completed Task 2: Add verification function]
|
||||
|
||||
You: Let me request code review before proceeding.
|
||||
|
||||
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
|
||||
[Dispatch code reviewer subagent]
|
||||
DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types
|
||||
PLAN_OR_REQUIREMENTS: Task 2 from docs/superpowers/plans/deployment-plan.md
|
||||
BASE_SHA: a7981ec
|
||||
HEAD_SHA: 3df7661
|
||||
|
||||
[Subagent returns]:
|
||||
Strengths: Clean architecture, real tests
|
||||
Issues:
|
||||
Important: Missing progress indicators
|
||||
Minor: Magic number (100) for reporting interval
|
||||
Assessment: Ready to proceed
|
||||
|
||||
You: [Fix progress indicators]
|
||||
[Continue to Task 3]
|
||||
```
|
||||
|
||||
## Integration with Workflows
|
||||
|
||||
**Subagent-Driven Development:**
|
||||
- Review after EACH task
|
||||
- Catch issues before they compound
|
||||
- Fix before moving to next task
|
||||
|
||||
**Executing Plans:**
|
||||
- Review after each task or at natural checkpoints
|
||||
- Get feedback, apply, continue
|
||||
|
||||
**Ad-Hoc Development:**
|
||||
- Review before merge
|
||||
- Review when stuck
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Skip review because "it's simple"
|
||||
- Ignore Critical issues
|
||||
- Proceed with unfixed Important issues
|
||||
- Argue with valid technical feedback
|
||||
|
||||
**If reviewer wrong:**
|
||||
- Push back with technical reasoning
|
||||
- Show code/tests that prove it works
|
||||
- Request clarification
|
||||
|
||||
See template at: requesting-code-review/code-reviewer.md
|
||||
@@ -0,0 +1,168 @@
|
||||
# Code Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a code reviewer subagent.
|
||||
|
||||
**Purpose:** Review completed work against requirements and code quality standards before it cascades into more work.
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review code changes"
|
||||
prompt: |
|
||||
You are a Senior Code Reviewer with expertise in software architecture,
|
||||
design patterns, and best practices. Your job is to review completed work
|
||||
against its plan or requirements and identify issues before they cascade.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
{DESCRIPTION}
|
||||
|
||||
## Requirements / Plan
|
||||
|
||||
{PLAN_OR_REQUIREMENTS}
|
||||
|
||||
## Git Range to Review
|
||||
|
||||
**Base:** {BASE_SHA}
|
||||
**Head:** {HEAD_SHA}
|
||||
|
||||
```bash
|
||||
git diff --stat {BASE_SHA}..{HEAD_SHA}
|
||||
git diff {BASE_SHA}..{HEAD_SHA}
|
||||
```
|
||||
|
||||
## What to Check
|
||||
|
||||
**Plan alignment:**
|
||||
- Does the implementation match the plan / requirements?
|
||||
- Are deviations justified improvements, or problematic departures?
|
||||
- Is all planned functionality present?
|
||||
|
||||
**Code quality:**
|
||||
- Clean separation of concerns?
|
||||
- Proper error handling?
|
||||
- Type safety where applicable?
|
||||
- DRY without premature abstraction?
|
||||
- Edge cases handled?
|
||||
|
||||
**Architecture:**
|
||||
- Sound design decisions?
|
||||
- Reasonable scalability and performance?
|
||||
- Security concerns?
|
||||
- Integrates cleanly with surrounding code?
|
||||
|
||||
**Testing:**
|
||||
- Tests verify real behavior, not mocks?
|
||||
- Edge cases covered?
|
||||
- Integration tests where they matter?
|
||||
- All tests passing?
|
||||
|
||||
**Production readiness:**
|
||||
- Migration strategy if schema changed?
|
||||
- Backward compatibility considered?
|
||||
- Documentation complete?
|
||||
- No obvious bugs?
|
||||
|
||||
## Calibration
|
||||
|
||||
Categorize issues by actual severity. Not everything is Critical.
|
||||
Acknowledge what was done well before listing issues — accurate praise
|
||||
helps the implementer trust the rest of the feedback.
|
||||
|
||||
If you find significant deviations from the plan, flag them specifically
|
||||
so the implementer can confirm whether the deviation was intentional.
|
||||
If you find issues with the plan itself rather than the implementation,
|
||||
say so.
|
||||
|
||||
## Output Format
|
||||
|
||||
### Strengths
|
||||
[What's well done? Be specific.]
|
||||
|
||||
### Issues
|
||||
|
||||
#### Critical (Must Fix)
|
||||
[Bugs, security issues, data loss risks, broken functionality]
|
||||
|
||||
#### Important (Should Fix)
|
||||
[Architecture problems, missing features, poor error handling, test gaps]
|
||||
|
||||
#### Minor (Nice to Have)
|
||||
[Code style, optimization opportunities, documentation polish]
|
||||
|
||||
For each issue:
|
||||
- File:line reference
|
||||
- What's wrong
|
||||
- Why it matters
|
||||
- How to fix (if not obvious)
|
||||
|
||||
### Recommendations
|
||||
[Improvements for code quality, architecture, or process]
|
||||
|
||||
### Assessment
|
||||
|
||||
**Ready to merge?** [Yes | No | With fixes]
|
||||
|
||||
**Reasoning:** [1-2 sentence technical assessment]
|
||||
|
||||
## Critical Rules
|
||||
|
||||
**DO:**
|
||||
- Categorize by actual severity
|
||||
- Be specific (file:line, not vague)
|
||||
- Explain WHY each issue matters
|
||||
- Acknowledge strengths
|
||||
- Give a clear verdict
|
||||
|
||||
**DON'T:**
|
||||
- Say "looks good" without checking
|
||||
- Mark nitpicks as Critical
|
||||
- Give feedback on code you didn't actually read
|
||||
- Be vague ("improve error handling")
|
||||
- Avoid giving a clear verdict
|
||||
```
|
||||
|
||||
**Placeholders:**
|
||||
- `{DESCRIPTION}` — brief summary of what was built
|
||||
- `{PLAN_OR_REQUIREMENTS}` — what it should do (plan file path, task text, or requirements)
|
||||
- `{BASE_SHA}` — starting commit
|
||||
- `{HEAD_SHA}` — ending commit
|
||||
|
||||
**Reviewer returns:** Strengths, Issues (Critical / Important / Minor), Recommendations, Assessment
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
### Strengths
|
||||
- Clean database schema with proper migrations (db.ts:15-42)
|
||||
- Comprehensive test coverage (18 tests, all edge cases)
|
||||
- Good error handling with fallbacks (summarizer.ts:85-92)
|
||||
|
||||
### Issues
|
||||
|
||||
#### Important
|
||||
1. **Missing help text in CLI wrapper**
|
||||
- File: index-conversations:1-31
|
||||
- Issue: No --help flag, users won't discover --concurrency
|
||||
- Fix: Add --help case with usage examples
|
||||
|
||||
2. **Date validation missing**
|
||||
- File: search.ts:25-27
|
||||
- Issue: Invalid dates silently return no results
|
||||
- Fix: Validate ISO format, throw error with example
|
||||
|
||||
#### Minor
|
||||
1. **Progress indicators**
|
||||
- File: indexer.ts:130
|
||||
- Issue: No "X of Y" counter for long operations
|
||||
- Impact: Users don't know how long to wait
|
||||
|
||||
### Recommendations
|
||||
- Add progress reporting for user experience
|
||||
- Consider config file for excluded projects (portability)
|
||||
|
||||
### Assessment
|
||||
|
||||
**Ready to merge: With fixes**
|
||||
|
||||
**Reasoning:** Core implementation is solid with good architecture and tests. Important issues (help text, date validation) are easily fixed and don't affect core functionality.
|
||||
```
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
name: subagent-driven-development
|
||||
description: Use when executing implementation plans with independent tasks in the current session
|
||||
---
|
||||
|
||||
# Subagent-Driven Development
|
||||
|
||||
Execute plan by dispatching fresh subagent per task, with two-stage review after each: spec compliance review first, then code quality review.
|
||||
|
||||
**Why subagents:** You delegate tasks to specialized agents with isolated context. By precisely crafting their instructions and context, you ensure they stay focused and succeed at their task. They should never inherit your session's context or history — you construct exactly what they need. This also preserves your own context for coordination work.
|
||||
|
||||
**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration
|
||||
|
||||
**Continuous execution:** Do not pause to check in with your human partner between tasks. Execute all tasks from the plan without stopping. The only reasons to stop are: BLOCKED status you cannot resolve, ambiguity that genuinely prevents progress, or all tasks complete. "Should I continue?" prompts and progress summaries waste their time — they asked you to execute the plan, so execute it.
|
||||
|
||||
## When to Use
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Have implementation plan?" [shape=diamond];
|
||||
"Tasks mostly independent?" [shape=diamond];
|
||||
"Stay in this session?" [shape=diamond];
|
||||
"subagent-driven-development" [shape=box];
|
||||
"executing-plans" [shape=box];
|
||||
"Manual execution or brainstorm first" [shape=box];
|
||||
|
||||
"Have implementation plan?" -> "Tasks mostly independent?" [label="yes"];
|
||||
"Have implementation plan?" -> "Manual execution or brainstorm first" [label="no"];
|
||||
"Tasks mostly independent?" -> "Stay in this session?" [label="yes"];
|
||||
"Tasks mostly independent?" -> "Manual execution or brainstorm first" [label="no - tightly coupled"];
|
||||
"Stay in this session?" -> "subagent-driven-development" [label="yes"];
|
||||
"Stay in this session?" -> "executing-plans" [label="no - parallel session"];
|
||||
}
|
||||
```
|
||||
|
||||
**vs. Executing Plans (parallel session):**
|
||||
- Same session (no context switch)
|
||||
- Fresh subagent per task (no context pollution)
|
||||
- Two-stage review after each task: spec compliance first, then code quality
|
||||
- Faster iteration (no human-in-loop between tasks)
|
||||
|
||||
## The Process
|
||||
|
||||
```dot
|
||||
digraph process {
|
||||
rankdir=TB;
|
||||
|
||||
subgraph cluster_per_task {
|
||||
label="Per Task";
|
||||
"Dispatch implementer subagent (./implementer-prompt.md)" [shape=box];
|
||||
"Implementer subagent asks questions?" [shape=diamond];
|
||||
"Answer questions, provide context" [shape=box];
|
||||
"Implementer subagent implements, tests, commits, self-reviews" [shape=box];
|
||||
"Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [shape=box];
|
||||
"Spec reviewer subagent confirms code matches spec?" [shape=diamond];
|
||||
"Implementer subagent fixes spec gaps" [shape=box];
|
||||
"Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [shape=box];
|
||||
"Code quality reviewer subagent approves?" [shape=diamond];
|
||||
"Implementer subagent fixes quality issues" [shape=box];
|
||||
"Mark task complete in TodoWrite" [shape=box];
|
||||
}
|
||||
|
||||
"Read plan, extract all tasks with full text, note context, create TodoWrite" [shape=box];
|
||||
"More tasks remain?" [shape=diamond];
|
||||
"Dispatch final code reviewer subagent for entire implementation" [shape=box];
|
||||
"Use superpowers:finishing-a-development-branch" [shape=box style=filled fillcolor=lightgreen];
|
||||
|
||||
"Read plan, extract all tasks with full text, note context, create TodoWrite" -> "Dispatch implementer subagent (./implementer-prompt.md)";
|
||||
"Dispatch implementer subagent (./implementer-prompt.md)" -> "Implementer subagent asks questions?";
|
||||
"Implementer subagent asks questions?" -> "Answer questions, provide context" [label="yes"];
|
||||
"Answer questions, provide context" -> "Dispatch implementer subagent (./implementer-prompt.md)";
|
||||
"Implementer subagent asks questions?" -> "Implementer subagent implements, tests, commits, self-reviews" [label="no"];
|
||||
"Implementer subagent implements, tests, commits, self-reviews" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)";
|
||||
"Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" -> "Spec reviewer subagent confirms code matches spec?";
|
||||
"Spec reviewer subagent confirms code matches spec?" -> "Implementer subagent fixes spec gaps" [label="no"];
|
||||
"Implementer subagent fixes spec gaps" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [label="re-review"];
|
||||
"Spec reviewer subagent confirms code matches spec?" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="yes"];
|
||||
"Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" -> "Code quality reviewer subagent approves?";
|
||||
"Code quality reviewer subagent approves?" -> "Implementer subagent fixes quality issues" [label="no"];
|
||||
"Implementer subagent fixes quality issues" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="re-review"];
|
||||
"Code quality reviewer subagent approves?" -> "Mark task complete in TodoWrite" [label="yes"];
|
||||
"Mark task complete in TodoWrite" -> "More tasks remain?";
|
||||
"More tasks remain?" -> "Dispatch implementer subagent (./implementer-prompt.md)" [label="yes"];
|
||||
"More tasks remain?" -> "Dispatch final code reviewer subagent for entire implementation" [label="no"];
|
||||
"Dispatch final code reviewer subagent for entire implementation" -> "Use superpowers:finishing-a-development-branch";
|
||||
}
|
||||
```
|
||||
|
||||
## Model Selection
|
||||
|
||||
Use the least powerful model that can handle each role to conserve cost and increase speed.
|
||||
|
||||
**Mechanical implementation tasks** (isolated functions, clear specs, 1-2 files): use a fast, cheap model. Most implementation tasks are mechanical when the plan is well-specified.
|
||||
|
||||
**Integration and judgment tasks** (multi-file coordination, pattern matching, debugging): use a standard model.
|
||||
|
||||
**Architecture, design, and review tasks**: use the most capable available model.
|
||||
|
||||
**Task complexity signals:**
|
||||
- Touches 1-2 files with a complete spec → cheap model
|
||||
- Touches multiple files with integration concerns → standard model
|
||||
- Requires design judgment or broad codebase understanding → most capable model
|
||||
|
||||
## Handling Implementer Status
|
||||
|
||||
Implementer subagents report one of four statuses. Handle each appropriately:
|
||||
|
||||
**DONE:** Proceed to spec compliance review.
|
||||
|
||||
**DONE_WITH_CONCERNS:** The implementer completed the work but flagged doubts. Read the concerns before proceeding. If the concerns are about correctness or scope, address them before review. If they're observations (e.g., "this file is getting large"), note them and proceed to review.
|
||||
|
||||
**NEEDS_CONTEXT:** The implementer needs information that wasn't provided. Provide the missing context and re-dispatch.
|
||||
|
||||
**BLOCKED:** The implementer cannot complete the task. Assess the blocker:
|
||||
1. If it's a context problem, provide more context and re-dispatch with the same model
|
||||
2. If the task requires more reasoning, re-dispatch with a more capable model
|
||||
3. If the task is too large, break it into smaller pieces
|
||||
4. If the plan itself is wrong, escalate to the human
|
||||
|
||||
**Never** ignore an escalation or force the same model to retry without changes. If the implementer said it's stuck, something needs to change.
|
||||
|
||||
## Prompt Templates
|
||||
|
||||
- `./implementer-prompt.md` - Dispatch implementer subagent
|
||||
- `./spec-reviewer-prompt.md` - Dispatch spec compliance reviewer subagent
|
||||
- `./code-quality-reviewer-prompt.md` - Dispatch code quality reviewer subagent
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```
|
||||
You: I'm using Subagent-Driven Development to execute this plan.
|
||||
|
||||
[Read plan file once: docs/superpowers/plans/feature-plan.md]
|
||||
[Extract all 5 tasks with full text and context]
|
||||
[Create TodoWrite with all tasks]
|
||||
|
||||
Task 1: Hook installation script
|
||||
|
||||
[Get Task 1 text and context (already extracted)]
|
||||
[Dispatch implementation subagent with full task text + context]
|
||||
|
||||
Implementer: "Before I begin - should the hook be installed at user or system level?"
|
||||
|
||||
You: "User level (~/.config/superpowers/hooks/)"
|
||||
|
||||
Implementer: "Got it. Implementing now..."
|
||||
[Later] Implementer:
|
||||
- Implemented install-hook command
|
||||
- Added tests, 5/5 passing
|
||||
- Self-review: Found I missed --force flag, added it
|
||||
- Committed
|
||||
|
||||
[Dispatch spec compliance reviewer]
|
||||
Spec reviewer: ✅ Spec compliant - all requirements met, nothing extra
|
||||
|
||||
[Get git SHAs, dispatch code quality reviewer]
|
||||
Code reviewer: Strengths: Good test coverage, clean. Issues: None. Approved.
|
||||
|
||||
[Mark Task 1 complete]
|
||||
|
||||
Task 2: Recovery modes
|
||||
|
||||
[Get Task 2 text and context (already extracted)]
|
||||
[Dispatch implementation subagent with full task text + context]
|
||||
|
||||
Implementer: [No questions, proceeds]
|
||||
Implementer:
|
||||
- Added verify/repair modes
|
||||
- 8/8 tests passing
|
||||
- Self-review: All good
|
||||
- Committed
|
||||
|
||||
[Dispatch spec compliance reviewer]
|
||||
Spec reviewer: ❌ Issues:
|
||||
- Missing: Progress reporting (spec says "report every 100 items")
|
||||
- Extra: Added --json flag (not requested)
|
||||
|
||||
[Implementer fixes issues]
|
||||
Implementer: Removed --json flag, added progress reporting
|
||||
|
||||
[Spec reviewer reviews again]
|
||||
Spec reviewer: ✅ Spec compliant now
|
||||
|
||||
[Dispatch code quality reviewer]
|
||||
Code reviewer: Strengths: Solid. Issues (Important): Magic number (100)
|
||||
|
||||
[Implementer fixes]
|
||||
Implementer: Extracted PROGRESS_INTERVAL constant
|
||||
|
||||
[Code reviewer reviews again]
|
||||
Code reviewer: ✅ Approved
|
||||
|
||||
[Mark Task 2 complete]
|
||||
|
||||
...
|
||||
|
||||
[After all tasks]
|
||||
[Dispatch final code-reviewer]
|
||||
Final reviewer: All requirements met, ready to merge
|
||||
|
||||
Done!
|
||||
```
|
||||
|
||||
## Advantages
|
||||
|
||||
**vs. Manual execution:**
|
||||
- Subagents follow TDD naturally
|
||||
- Fresh context per task (no confusion)
|
||||
- Parallel-safe (subagents don't interfere)
|
||||
- Subagent can ask questions (before AND during work)
|
||||
|
||||
**vs. Executing Plans:**
|
||||
- Same session (no handoff)
|
||||
- Continuous progress (no waiting)
|
||||
- Review checkpoints automatic
|
||||
|
||||
**Efficiency gains:**
|
||||
- No file reading overhead (controller provides full text)
|
||||
- Controller curates exactly what context is needed
|
||||
- Subagent gets complete information upfront
|
||||
- Questions surfaced before work begins (not after)
|
||||
|
||||
**Quality gates:**
|
||||
- Self-review catches issues before handoff
|
||||
- Two-stage review: spec compliance, then code quality
|
||||
- Review loops ensure fixes actually work
|
||||
- Spec compliance prevents over/under-building
|
||||
- Code quality ensures implementation is well-built
|
||||
|
||||
**Cost:**
|
||||
- More subagent invocations (implementer + 2 reviewers per task)
|
||||
- Controller does more prep work (extracting all tasks upfront)
|
||||
- Review loops add iterations
|
||||
- But catches issues early (cheaper than debugging later)
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Start implementation on main/master branch without explicit user consent
|
||||
- Skip reviews (spec compliance OR code quality)
|
||||
- Proceed with unfixed issues
|
||||
- Dispatch multiple implementation subagents in parallel (conflicts)
|
||||
- Make subagent read plan file (provide full text instead)
|
||||
- Skip scene-setting context (subagent needs to understand where task fits)
|
||||
- Ignore subagent questions (answer before letting them proceed)
|
||||
- Accept "close enough" on spec compliance (spec reviewer found issues = not done)
|
||||
- Skip review loops (reviewer found issues = implementer fixes = review again)
|
||||
- Let implementer self-review replace actual review (both are needed)
|
||||
- **Start code quality review before spec compliance is ✅** (wrong order)
|
||||
- Move to next task while either review has open issues
|
||||
|
||||
**If subagent asks questions:**
|
||||
- Answer clearly and completely
|
||||
- Provide additional context if needed
|
||||
- Don't rush them into implementation
|
||||
|
||||
**If reviewer finds issues:**
|
||||
- Implementer (same subagent) fixes them
|
||||
- Reviewer reviews again
|
||||
- Repeat until approved
|
||||
- Don't skip the re-review
|
||||
|
||||
**If subagent fails task:**
|
||||
- Dispatch fix subagent with specific instructions
|
||||
- Don't try to fix manually (context pollution)
|
||||
|
||||
## Integration
|
||||
|
||||
**Required workflow skills:**
|
||||
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
|
||||
- **superpowers:writing-plans** - Creates the plan this skill executes
|
||||
- **superpowers:requesting-code-review** - Code review template for reviewer subagents
|
||||
- **superpowers:finishing-a-development-branch** - Complete development after all tasks
|
||||
|
||||
**Subagents should use:**
|
||||
- **superpowers:test-driven-development** - Subagents follow TDD for each task
|
||||
|
||||
**Alternative workflow:**
|
||||
- **superpowers:executing-plans** - Use for parallel session instead of same-session execution
|
||||
@@ -0,0 +1,25 @@
|
||||
# Code Quality Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a code quality reviewer subagent.
|
||||
|
||||
**Purpose:** Verify implementation is well-built (clean, tested, maintainable)
|
||||
|
||||
**Only dispatch after spec compliance review passes.**
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
Use template at requesting-code-review/code-reviewer.md
|
||||
|
||||
DESCRIPTION: [task summary, from implementer's report]
|
||||
PLAN_OR_REQUIREMENTS: Task N from [plan-file]
|
||||
BASE_SHA: [commit before task]
|
||||
HEAD_SHA: [current commit]
|
||||
```
|
||||
|
||||
**In addition to standard code quality concerns, the reviewer should check:**
|
||||
- Does each file have one clear responsibility with a well-defined interface?
|
||||
- Are units decomposed so they can be understood and tested independently?
|
||||
- Is the implementation following the file structure from the plan?
|
||||
- Did this implementation create new files that are already large, or significantly grow existing files? (Don't flag pre-existing file sizes — focus on what this change contributed.)
|
||||
|
||||
**Code reviewer returns:** Strengths, Issues (Critical/Important/Minor), Assessment
|
||||
@@ -0,0 +1,113 @@
|
||||
# Implementer Subagent Prompt Template
|
||||
|
||||
Use this template when dispatching an implementer subagent.
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Implement Task N: [task name]"
|
||||
prompt: |
|
||||
You are implementing Task N: [task name]
|
||||
|
||||
## Task Description
|
||||
|
||||
[FULL TEXT of task from plan - paste it here, don't make subagent read file]
|
||||
|
||||
## Context
|
||||
|
||||
[Scene-setting: where this fits, dependencies, architectural context]
|
||||
|
||||
## Before You Begin
|
||||
|
||||
If you have questions about:
|
||||
- The requirements or acceptance criteria
|
||||
- The approach or implementation strategy
|
||||
- Dependencies or assumptions
|
||||
- Anything unclear in the task description
|
||||
|
||||
**Ask them now.** Raise any concerns before starting work.
|
||||
|
||||
## Your Job
|
||||
|
||||
Once you're clear on requirements:
|
||||
1. Implement exactly what the task specifies
|
||||
2. Write tests (following TDD if task says to)
|
||||
3. Verify implementation works
|
||||
4. Commit your work
|
||||
5. Self-review (see below)
|
||||
6. Report back
|
||||
|
||||
Work from: [directory]
|
||||
|
||||
**While you work:** If you encounter something unexpected or unclear, **ask questions**.
|
||||
It's always OK to pause and clarify. Don't guess or make assumptions.
|
||||
|
||||
## Code Organization
|
||||
|
||||
You reason best about code you can hold in context at once, and your edits are more
|
||||
reliable when files are focused. Keep this in mind:
|
||||
- Follow the file structure defined in the plan
|
||||
- Each file should have one clear responsibility with a well-defined interface
|
||||
- If a file you're creating is growing beyond the plan's intent, stop and report
|
||||
it as DONE_WITH_CONCERNS — don't split files on your own without plan guidance
|
||||
- If an existing file you're modifying is already large or tangled, work carefully
|
||||
and note it as a concern in your report
|
||||
- In existing codebases, follow established patterns. Improve code you're touching
|
||||
the way a good developer would, but don't restructure things outside your task.
|
||||
|
||||
## When You're in Over Your Head
|
||||
|
||||
It is always OK to stop and say "this is too hard for me." Bad work is worse than
|
||||
no work. You will not be penalized for escalating.
|
||||
|
||||
**STOP and escalate when:**
|
||||
- The task requires architectural decisions with multiple valid approaches
|
||||
- You need to understand code beyond what was provided and can't find clarity
|
||||
- You feel uncertain about whether your approach is correct
|
||||
- The task involves restructuring existing code in ways the plan didn't anticipate
|
||||
- You've been reading file after file trying to understand the system without progress
|
||||
|
||||
**How to escalate:** Report back with status BLOCKED or NEEDS_CONTEXT. Describe
|
||||
specifically what you're stuck on, what you've tried, and what kind of help you need.
|
||||
The controller can provide more context, re-dispatch with a more capable model,
|
||||
or break the task into smaller pieces.
|
||||
|
||||
## Before Reporting Back: Self-Review
|
||||
|
||||
Review your work with fresh eyes. Ask yourself:
|
||||
|
||||
**Completeness:**
|
||||
- Did I fully implement everything in the spec?
|
||||
- Did I miss any requirements?
|
||||
- Are there edge cases I didn't handle?
|
||||
|
||||
**Quality:**
|
||||
- Is this my best work?
|
||||
- Are names clear and accurate (match what things do, not how they work)?
|
||||
- Is the code clean and maintainable?
|
||||
|
||||
**Discipline:**
|
||||
- Did I avoid overbuilding (YAGNI)?
|
||||
- Did I only build what was requested?
|
||||
- Did I follow existing patterns in the codebase?
|
||||
|
||||
**Testing:**
|
||||
- Do tests actually verify behavior (not just mock behavior)?
|
||||
- Did I follow TDD if required?
|
||||
- Are tests comprehensive?
|
||||
|
||||
If you find issues during self-review, fix them now before reporting.
|
||||
|
||||
## Report Format
|
||||
|
||||
When done, report:
|
||||
- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
||||
- What you implemented (or what you attempted, if blocked)
|
||||
- What you tested and test results
|
||||
- Files changed
|
||||
- Self-review findings (if any)
|
||||
- Any issues or concerns
|
||||
|
||||
Use DONE_WITH_CONCERNS if you completed the work but have doubts about correctness.
|
||||
Use BLOCKED if you cannot complete the task. Use NEEDS_CONTEXT if you need
|
||||
information that wasn't provided. Never silently produce work you're unsure about.
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Spec Compliance Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a spec compliance reviewer subagent.
|
||||
|
||||
**Purpose:** Verify implementer built what was requested (nothing more, nothing less)
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review spec compliance for Task N"
|
||||
prompt: |
|
||||
You are reviewing whether an implementation matches its specification.
|
||||
|
||||
## What Was Requested
|
||||
|
||||
[FULL TEXT of task requirements]
|
||||
|
||||
## What Implementer Claims They Built
|
||||
|
||||
[From implementer's report]
|
||||
|
||||
## CRITICAL: Do Not Trust the Report
|
||||
|
||||
The implementer finished suspiciously quickly. Their report may be incomplete,
|
||||
inaccurate, or optimistic. You MUST verify everything independently.
|
||||
|
||||
**DO NOT:**
|
||||
- Take their word for what they implemented
|
||||
- Trust their claims about completeness
|
||||
- Accept their interpretation of requirements
|
||||
|
||||
**DO:**
|
||||
- Read the actual code they wrote
|
||||
- Compare actual implementation to requirements line by line
|
||||
- Check for missing pieces they claimed to implement
|
||||
- Look for extra features they didn't mention
|
||||
|
||||
## Your Job
|
||||
|
||||
Read the implementation code and verify:
|
||||
|
||||
**Missing requirements:**
|
||||
- Did they implement everything that was requested?
|
||||
- Are there requirements they skipped or missed?
|
||||
- Did they claim something works but didn't actually implement it?
|
||||
|
||||
**Extra/unneeded work:**
|
||||
- Did they build things that weren't requested?
|
||||
- Did they over-engineer or add unnecessary features?
|
||||
- Did they add "nice to haves" that weren't in spec?
|
||||
|
||||
**Misunderstandings:**
|
||||
- Did they interpret requirements differently than intended?
|
||||
- Did they solve the wrong problem?
|
||||
- Did they implement the right feature but wrong way?
|
||||
|
||||
**Verify by reading code, not by trusting report.**
|
||||
|
||||
Report:
|
||||
- ✅ Spec compliant (if everything matches after code inspection)
|
||||
- ❌ Issues found: [list specifically what's missing or extra, with file:line references]
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Creation Log: Systematic Debugging Skill
|
||||
|
||||
Reference example of extracting, structuring, and bulletproofing a critical skill.
|
||||
|
||||
## Source Material
|
||||
|
||||
Extracted debugging framework from `~/.claude/CLAUDE.md`:
|
||||
- 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation)
|
||||
- Core mandate: ALWAYS find root cause, NEVER fix symptoms
|
||||
- Rules designed to resist time pressure and rationalization
|
||||
|
||||
## Extraction Decisions
|
||||
|
||||
**What to include:**
|
||||
- Complete 4-phase framework with all rules
|
||||
- Anti-shortcuts ("NEVER fix symptom", "STOP and re-analyze")
|
||||
- Pressure-resistant language ("even if faster", "even if I seem in a hurry")
|
||||
- Concrete steps for each phase
|
||||
|
||||
**What to leave out:**
|
||||
- Project-specific context
|
||||
- Repetitive variations of same rule
|
||||
- Narrative explanations (condensed to principles)
|
||||
|
||||
## Structure Following skill-creation/SKILL.md
|
||||
|
||||
1. **Rich when_to_use** - Included symptoms and anti-patterns
|
||||
2. **Type: technique** - Concrete process with steps
|
||||
3. **Keywords** - "root cause", "symptom", "workaround", "debugging", "investigation"
|
||||
4. **Flowchart** - Decision point for "fix failed" → re-analyze vs add more fixes
|
||||
5. **Phase-by-phase breakdown** - Scannable checklist format
|
||||
6. **Anti-patterns section** - What NOT to do (critical for this skill)
|
||||
|
||||
## Bulletproofing Elements
|
||||
|
||||
Framework designed to resist rationalization under pressure:
|
||||
|
||||
### Language Choices
|
||||
- "ALWAYS" / "NEVER" (not "should" / "try to")
|
||||
- "even if faster" / "even if I seem in a hurry"
|
||||
- "STOP and re-analyze" (explicit pause)
|
||||
- "Don't skip past" (catches the actual behavior)
|
||||
|
||||
### Structural Defenses
|
||||
- **Phase 1 required** - Can't skip to implementation
|
||||
- **Single hypothesis rule** - Forces thinking, prevents shotgun fixes
|
||||
- **Explicit failure mode** - "IF your first fix doesn't work" with mandatory action
|
||||
- **Anti-patterns section** - Shows exactly what shortcuts look like
|
||||
|
||||
### Redundancy
|
||||
- Root cause mandate in overview + when_to_use + Phase 1 + implementation rules
|
||||
- "NEVER fix symptom" appears 4 times in different contexts
|
||||
- Each phase has explicit "don't skip" guidance
|
||||
|
||||
## Testing Approach
|
||||
|
||||
Created 4 validation tests following skills/meta/testing-skills-with-subagents:
|
||||
|
||||
### Test 1: Academic Context (No Pressure)
|
||||
- Simple bug, no time pressure
|
||||
- **Result:** Perfect compliance, complete investigation
|
||||
|
||||
### Test 2: Time Pressure + Obvious Quick Fix
|
||||
- User "in a hurry", symptom fix looks easy
|
||||
- **Result:** Resisted shortcut, followed full process, found real root cause
|
||||
|
||||
### Test 3: Complex System + Uncertainty
|
||||
- Multi-layer failure, unclear if can find root cause
|
||||
- **Result:** Systematic investigation, traced through all layers, found source
|
||||
|
||||
### Test 4: Failed First Fix
|
||||
- Hypothesis doesn't work, temptation to add more fixes
|
||||
- **Result:** Stopped, re-analyzed, formed new hypothesis (no shotgun)
|
||||
|
||||
**All tests passed.** No rationalizations found.
|
||||
|
||||
## Iterations
|
||||
|
||||
### Initial Version
|
||||
- Complete 4-phase framework
|
||||
- Anti-patterns section
|
||||
- Flowchart for "fix failed" decision
|
||||
|
||||
### Enhancement 1: TDD Reference
|
||||
- Added link to skills/testing/test-driven-development
|
||||
- Note explaining TDD's "simplest code" ≠ debugging's "root cause"
|
||||
- Prevents confusion between methodologies
|
||||
|
||||
## Final Outcome
|
||||
|
||||
Bulletproof skill that:
|
||||
- ✅ Clearly mandates root cause investigation
|
||||
- ✅ Resists time pressure rationalization
|
||||
- ✅ Provides concrete steps for each phase
|
||||
- ✅ Shows anti-patterns explicitly
|
||||
- ✅ Tested under multiple pressure scenarios
|
||||
- ✅ Clarifies relationship to TDD
|
||||
- ✅ Ready for use
|
||||
|
||||
## Key Insight
|
||||
|
||||
**Most important bulletproofing:** Anti-patterns section showing exact shortcuts that feel justified in the moment. When Claude thinks "I'll just add this one quick fix", seeing that exact pattern listed as wrong creates cognitive friction.
|
||||
|
||||
## Usage Example
|
||||
|
||||
When encountering a bug:
|
||||
1. Load skill: skills/debugging/systematic-debugging
|
||||
2. Read overview (10 sec) - reminded of mandate
|
||||
3. Follow Phase 1 checklist - forced investigation
|
||||
4. If tempted to skip - see anti-pattern, stop
|
||||
5. Complete all phases - root cause found
|
||||
|
||||
**Time investment:** 5-10 minutes
|
||||
**Time saved:** Hours of symptom-whack-a-mole
|
||||
|
||||
---
|
||||
|
||||
*Created: 2025-10-03*
|
||||
*Purpose: Reference example for skill extraction and bulletproofing*
|
||||
@@ -0,0 +1,296 @@
|
||||
---
|
||||
name: systematic-debugging
|
||||
description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
|
||||
---
|
||||
|
||||
# Systematic Debugging
|
||||
|
||||
## Overview
|
||||
|
||||
Random fixes waste time and create new bugs. Quick patches mask underlying issues.
|
||||
|
||||
**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure.
|
||||
|
||||
**Violating the letter of this process is violating the spirit of debugging.**
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
|
||||
```
|
||||
|
||||
If you haven't completed Phase 1, you cannot propose fixes.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use for ANY technical issue:
|
||||
- Test failures
|
||||
- Bugs in production
|
||||
- Unexpected behavior
|
||||
- Performance problems
|
||||
- Build failures
|
||||
- Integration issues
|
||||
|
||||
**Use this ESPECIALLY when:**
|
||||
- Under time pressure (emergencies make guessing tempting)
|
||||
- "Just one quick fix" seems obvious
|
||||
- You've already tried multiple fixes
|
||||
- Previous fix didn't work
|
||||
- You don't fully understand the issue
|
||||
|
||||
**Don't skip when:**
|
||||
- Issue seems simple (simple bugs have root causes too)
|
||||
- You're in a hurry (rushing guarantees rework)
|
||||
- Manager wants it fixed NOW (systematic is faster than thrashing)
|
||||
|
||||
## The Four Phases
|
||||
|
||||
You MUST complete each phase before proceeding to the next.
|
||||
|
||||
### Phase 1: Root Cause Investigation
|
||||
|
||||
**BEFORE attempting ANY fix:**
|
||||
|
||||
1. **Read Error Messages Carefully**
|
||||
- Don't skip past errors or warnings
|
||||
- They often contain the exact solution
|
||||
- Read stack traces completely
|
||||
- Note line numbers, file paths, error codes
|
||||
|
||||
2. **Reproduce Consistently**
|
||||
- Can you trigger it reliably?
|
||||
- What are the exact steps?
|
||||
- Does it happen every time?
|
||||
- If not reproducible → gather more data, don't guess
|
||||
|
||||
3. **Check Recent Changes**
|
||||
- What changed that could cause this?
|
||||
- Git diff, recent commits
|
||||
- New dependencies, config changes
|
||||
- Environmental differences
|
||||
|
||||
4. **Gather Evidence in Multi-Component Systems**
|
||||
|
||||
**WHEN system has multiple components (CI → build → signing, API → service → database):**
|
||||
|
||||
**BEFORE proposing fixes, add diagnostic instrumentation:**
|
||||
```
|
||||
For EACH component boundary:
|
||||
- Log what data enters component
|
||||
- Log what data exits component
|
||||
- Verify environment/config propagation
|
||||
- Check state at each layer
|
||||
|
||||
Run once to gather evidence showing WHERE it breaks
|
||||
THEN analyze evidence to identify failing component
|
||||
THEN investigate that specific component
|
||||
```
|
||||
|
||||
**Example (multi-layer system):**
|
||||
```bash
|
||||
# Layer 1: Workflow
|
||||
echo "=== Secrets available in workflow: ==="
|
||||
echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}"
|
||||
|
||||
# Layer 2: Build script
|
||||
echo "=== Env vars in build script: ==="
|
||||
env | grep IDENTITY || echo "IDENTITY not in environment"
|
||||
|
||||
# Layer 3: Signing script
|
||||
echo "=== Keychain state: ==="
|
||||
security list-keychains
|
||||
security find-identity -v
|
||||
|
||||
# Layer 4: Actual signing
|
||||
codesign --sign "$IDENTITY" --verbose=4 "$APP"
|
||||
```
|
||||
|
||||
**This reveals:** Which layer fails (secrets → workflow ✓, workflow → build ✗)
|
||||
|
||||
5. **Trace Data Flow**
|
||||
|
||||
**WHEN error is deep in call stack:**
|
||||
|
||||
See `root-cause-tracing.md` in this directory for the complete backward tracing technique.
|
||||
|
||||
**Quick version:**
|
||||
- Where does bad value originate?
|
||||
- What called this with bad value?
|
||||
- Keep tracing up until you find the source
|
||||
- Fix at source, not at symptom
|
||||
|
||||
### Phase 2: Pattern Analysis
|
||||
|
||||
**Find the pattern before fixing:**
|
||||
|
||||
1. **Find Working Examples**
|
||||
- Locate similar working code in same codebase
|
||||
- What works that's similar to what's broken?
|
||||
|
||||
2. **Compare Against References**
|
||||
- If implementing pattern, read reference implementation COMPLETELY
|
||||
- Don't skim - read every line
|
||||
- Understand the pattern fully before applying
|
||||
|
||||
3. **Identify Differences**
|
||||
- What's different between working and broken?
|
||||
- List every difference, however small
|
||||
- Don't assume "that can't matter"
|
||||
|
||||
4. **Understand Dependencies**
|
||||
- What other components does this need?
|
||||
- What settings, config, environment?
|
||||
- What assumptions does it make?
|
||||
|
||||
### Phase 3: Hypothesis and Testing
|
||||
|
||||
**Scientific method:**
|
||||
|
||||
1. **Form Single Hypothesis**
|
||||
- State clearly: "I think X is the root cause because Y"
|
||||
- Write it down
|
||||
- Be specific, not vague
|
||||
|
||||
2. **Test Minimally**
|
||||
- Make the SMALLEST possible change to test hypothesis
|
||||
- One variable at a time
|
||||
- Don't fix multiple things at once
|
||||
|
||||
3. **Verify Before Continuing**
|
||||
- Did it work? Yes → Phase 4
|
||||
- Didn't work? Form NEW hypothesis
|
||||
- DON'T add more fixes on top
|
||||
|
||||
4. **When You Don't Know**
|
||||
- Say "I don't understand X"
|
||||
- Don't pretend to know
|
||||
- Ask for help
|
||||
- Research more
|
||||
|
||||
### Phase 4: Implementation
|
||||
|
||||
**Fix the root cause, not the symptom:**
|
||||
|
||||
1. **Create Failing Test Case**
|
||||
- Simplest possible reproduction
|
||||
- Automated test if possible
|
||||
- One-off test script if no framework
|
||||
- MUST have before fixing
|
||||
- Use the `superpowers:test-driven-development` skill for writing proper failing tests
|
||||
|
||||
2. **Implement Single Fix**
|
||||
- Address the root cause identified
|
||||
- ONE change at a time
|
||||
- No "while I'm here" improvements
|
||||
- No bundled refactoring
|
||||
|
||||
3. **Verify Fix**
|
||||
- Test passes now?
|
||||
- No other tests broken?
|
||||
- Issue actually resolved?
|
||||
|
||||
4. **If Fix Doesn't Work**
|
||||
- STOP
|
||||
- Count: How many fixes have you tried?
|
||||
- If < 3: Return to Phase 1, re-analyze with new information
|
||||
- **If ≥ 3: STOP and question the architecture (step 5 below)**
|
||||
- DON'T attempt Fix #4 without architectural discussion
|
||||
|
||||
5. **If 3+ Fixes Failed: Question Architecture**
|
||||
|
||||
**Pattern indicating architectural problem:**
|
||||
- Each fix reveals new shared state/coupling/problem in different place
|
||||
- Fixes require "massive refactoring" to implement
|
||||
- Each fix creates new symptoms elsewhere
|
||||
|
||||
**STOP and question fundamentals:**
|
||||
- Is this pattern fundamentally sound?
|
||||
- Are we "sticking with it through sheer inertia"?
|
||||
- Should we refactor architecture vs. continue fixing symptoms?
|
||||
|
||||
**Discuss with your human partner before attempting more fixes**
|
||||
|
||||
This is NOT a failed hypothesis - this is a wrong architecture.
|
||||
|
||||
## Red Flags - STOP and Follow Process
|
||||
|
||||
If you catch yourself thinking:
|
||||
- "Quick fix for now, investigate later"
|
||||
- "Just try changing X and see if it works"
|
||||
- "Add multiple changes, run tests"
|
||||
- "Skip the test, I'll manually verify"
|
||||
- "It's probably X, let me fix that"
|
||||
- "I don't fully understand but this might work"
|
||||
- "Pattern says X but I'll adapt it differently"
|
||||
- "Here are the main problems: [lists fixes without investigation]"
|
||||
- Proposing solutions before tracing data flow
|
||||
- **"One more fix attempt" (when already tried 2+)**
|
||||
- **Each fix reveals new problem in different place**
|
||||
|
||||
**ALL of these mean: STOP. Return to Phase 1.**
|
||||
|
||||
**If 3+ fixes failed:** Question the architecture (see Phase 4.5)
|
||||
|
||||
## your human partner's Signals You're Doing It Wrong
|
||||
|
||||
**Watch for these redirections:**
|
||||
- "Is that not happening?" - You assumed without verifying
|
||||
- "Will it show us...?" - You should have added evidence gathering
|
||||
- "Stop guessing" - You're proposing fixes without understanding
|
||||
- "Ultrathink this" - Question fundamentals, not just symptoms
|
||||
- "We're stuck?" (frustrated) - Your approach isn't working
|
||||
|
||||
**When you see these:** STOP. Return to Phase 1.
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. |
|
||||
| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. |
|
||||
| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. |
|
||||
| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. |
|
||||
| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. |
|
||||
| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. |
|
||||
| "I see the problem, let me fix it" | Seeing symptoms ≠ understanding root cause. |
|
||||
| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Phase | Key Activities | Success Criteria |
|
||||
|-------|---------------|------------------|
|
||||
| **1. Root Cause** | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY |
|
||||
| **2. Pattern** | Find working examples, compare | Identify differences |
|
||||
| **3. Hypothesis** | Form theory, test minimally | Confirmed or new hypothesis |
|
||||
| **4. Implementation** | Create test, fix, verify | Bug resolved, tests pass |
|
||||
|
||||
## When Process Reveals "No Root Cause"
|
||||
|
||||
If systematic investigation reveals issue is truly environmental, timing-dependent, or external:
|
||||
|
||||
1. You've completed the process
|
||||
2. Document what you investigated
|
||||
3. Implement appropriate handling (retry, timeout, error message)
|
||||
4. Add monitoring/logging for future investigation
|
||||
|
||||
**But:** 95% of "no root cause" cases are incomplete investigation.
|
||||
|
||||
## Supporting Techniques
|
||||
|
||||
These techniques are part of systematic debugging and available in this directory:
|
||||
|
||||
- **`root-cause-tracing.md`** - Trace bugs backward through call stack to find original trigger
|
||||
- **`defense-in-depth.md`** - Add validation at multiple layers after finding root cause
|
||||
- **`condition-based-waiting.md`** - Replace arbitrary timeouts with condition polling
|
||||
|
||||
**Related skills:**
|
||||
- **superpowers:test-driven-development** - For creating failing test case (Phase 4, Step 1)
|
||||
- **superpowers:verification-before-completion** - Verify fix worked before claiming success
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging sessions:
|
||||
- Systematic approach: 15-30 minutes to fix
|
||||
- Random fixes approach: 2-3 hours of thrashing
|
||||
- First-time fix rate: 95% vs 40%
|
||||
- New bugs introduced: Near zero vs common
|
||||
@@ -0,0 +1,158 @@
|
||||
// Complete implementation of condition-based waiting utilities
|
||||
// From: Lace test infrastructure improvements (2025-10-03)
|
||||
// Context: Fixed 15 flaky tests by replacing arbitrary timeouts
|
||||
|
||||
import type { ThreadManager } from '~/threads/thread-manager';
|
||||
import type { LaceEvent, LaceEventType } from '~/threads/types';
|
||||
|
||||
/**
|
||||
* Wait for a specific event type to appear in thread
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param eventType - Type of event to wait for
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to the first matching event
|
||||
*
|
||||
* Example:
|
||||
* await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
|
||||
*/
|
||||
export function waitForEvent(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
eventType: LaceEventType,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const event = events.find((e) => e.type === eventType);
|
||||
|
||||
if (event) {
|
||||
resolve(event);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
|
||||
} else {
|
||||
setTimeout(check, 10); // Poll every 10ms for efficiency
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific number of events of a given type
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param eventType - Type of event to wait for
|
||||
* @param count - Number of events to wait for
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to all matching events once count is reached
|
||||
*
|
||||
* Example:
|
||||
* // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
|
||||
* await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
|
||||
*/
|
||||
export function waitForEventCount(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
eventType: LaceEventType,
|
||||
count: number,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const matchingEvents = events.filter((e) => e.type === eventType);
|
||||
|
||||
if (matchingEvents.length >= count) {
|
||||
resolve(matchingEvents);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(
|
||||
new Error(
|
||||
`Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setTimeout(check, 10);
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an event matching a custom predicate
|
||||
* Useful when you need to check event data, not just type
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param predicate - Function that returns true when event matches
|
||||
* @param description - Human-readable description for error messages
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to the first matching event
|
||||
*
|
||||
* Example:
|
||||
* // Wait for TOOL_RESULT with specific ID
|
||||
* await waitForEventMatch(
|
||||
* threadManager,
|
||||
* agentThreadId,
|
||||
* (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
|
||||
* 'TOOL_RESULT with id=call_123'
|
||||
* );
|
||||
*/
|
||||
export function waitForEventMatch(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
predicate: (event: LaceEvent) => boolean,
|
||||
description: string,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const event = events.find(predicate);
|
||||
|
||||
if (event) {
|
||||
resolve(event);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
|
||||
} else {
|
||||
setTimeout(check, 10);
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// Usage example from actual debugging session:
|
||||
//
|
||||
// BEFORE (flaky):
|
||||
// ---------------
|
||||
// const messagePromise = agent.sendMessage('Execute tools');
|
||||
// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
|
||||
// agent.abort();
|
||||
// await messagePromise;
|
||||
// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
|
||||
// expect(toolResults.length).toBe(2); // Fails randomly
|
||||
//
|
||||
// AFTER (reliable):
|
||||
// ----------------
|
||||
// const messagePromise = agent.sendMessage('Execute tools');
|
||||
// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
|
||||
// agent.abort();
|
||||
// await messagePromise;
|
||||
// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
|
||||
// expect(toolResults.length).toBe(2); // Always succeeds
|
||||
//
|
||||
// Result: 60% pass rate → 100%, 40% faster execution
|
||||
@@ -0,0 +1,115 @@
|
||||
# Condition-Based Waiting
|
||||
|
||||
## Overview
|
||||
|
||||
Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
|
||||
|
||||
**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.
|
||||
|
||||
## When to Use
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Test uses setTimeout/sleep?" [shape=diamond];
|
||||
"Testing timing behavior?" [shape=diamond];
|
||||
"Document WHY timeout needed" [shape=box];
|
||||
"Use condition-based waiting" [shape=box];
|
||||
|
||||
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
|
||||
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
|
||||
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
|
||||
- Tests are flaky (pass sometimes, fail under load)
|
||||
- Tests timeout when run in parallel
|
||||
- Waiting for async operations to complete
|
||||
|
||||
**Don't use when:**
|
||||
- Testing actual timing behavior (debounce, throttle intervals)
|
||||
- Always document WHY if using arbitrary timeout
|
||||
|
||||
## Core Pattern
|
||||
|
||||
```typescript
|
||||
// ❌ BEFORE: Guessing at timing
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
const result = getResult();
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// ✅ AFTER: Waiting for condition
|
||||
await waitFor(() => getResult() !== undefined);
|
||||
const result = getResult();
|
||||
expect(result).toBeDefined();
|
||||
```
|
||||
|
||||
## Quick Patterns
|
||||
|
||||
| Scenario | Pattern |
|
||||
|----------|---------|
|
||||
| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |
|
||||
| Wait for state | `waitFor(() => machine.state === 'ready')` |
|
||||
| Wait for count | `waitFor(() => items.length >= 5)` |
|
||||
| Wait for file | `waitFor(() => fs.existsSync(path))` |
|
||||
| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |
|
||||
|
||||
## Implementation
|
||||
|
||||
Generic polling function:
|
||||
```typescript
|
||||
async function waitFor<T>(
|
||||
condition: () => T | undefined | null | false,
|
||||
description: string,
|
||||
timeoutMs = 5000
|
||||
): Promise<T> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
const result = condition();
|
||||
if (result) return result;
|
||||
|
||||
if (Date.now() - startTime > timeoutMs) {
|
||||
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU
|
||||
**✅ Fix:** Poll every 10ms
|
||||
|
||||
**❌ No timeout:** Loop forever if condition never met
|
||||
**✅ Fix:** Always include timeout with clear error
|
||||
|
||||
**❌ Stale data:** Cache state before loop
|
||||
**✅ Fix:** Call getter inside loop for fresh data
|
||||
|
||||
## When Arbitrary Timeout IS Correct
|
||||
|
||||
```typescript
|
||||
// Tool ticks every 100ms - need 2 ticks to verify partial output
|
||||
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
|
||||
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
|
||||
// 200ms = 2 ticks at 100ms intervals - documented and justified
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
1. First wait for triggering condition
|
||||
2. Based on known timing (not guessing)
|
||||
3. Comment explaining WHY
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging session (2025-10-03):
|
||||
- Fixed 15 flaky tests across 3 files
|
||||
- Pass rate: 60% → 100%
|
||||
- Execution time: 40% faster
|
||||
- No more race conditions
|
||||
@@ -0,0 +1,122 @@
|
||||
# Defense-in-Depth Validation
|
||||
|
||||
## Overview
|
||||
|
||||
When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks.
|
||||
|
||||
**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible.
|
||||
|
||||
## Why Multiple Layers
|
||||
|
||||
Single validation: "We fixed the bug"
|
||||
Multiple layers: "We made the bug impossible"
|
||||
|
||||
Different layers catch different cases:
|
||||
- Entry validation catches most bugs
|
||||
- Business logic catches edge cases
|
||||
- Environment guards prevent context-specific dangers
|
||||
- Debug logging helps when other layers fail
|
||||
|
||||
## The Four Layers
|
||||
|
||||
### Layer 1: Entry Point Validation
|
||||
**Purpose:** Reject obviously invalid input at API boundary
|
||||
|
||||
```typescript
|
||||
function createProject(name: string, workingDirectory: string) {
|
||||
if (!workingDirectory || workingDirectory.trim() === '') {
|
||||
throw new Error('workingDirectory cannot be empty');
|
||||
}
|
||||
if (!existsSync(workingDirectory)) {
|
||||
throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
|
||||
}
|
||||
if (!statSync(workingDirectory).isDirectory()) {
|
||||
throw new Error(`workingDirectory is not a directory: ${workingDirectory}`);
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 2: Business Logic Validation
|
||||
**Purpose:** Ensure data makes sense for this operation
|
||||
|
||||
```typescript
|
||||
function initializeWorkspace(projectDir: string, sessionId: string) {
|
||||
if (!projectDir) {
|
||||
throw new Error('projectDir required for workspace initialization');
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 3: Environment Guards
|
||||
**Purpose:** Prevent dangerous operations in specific contexts
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
// In tests, refuse git init outside temp directories
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const normalized = normalize(resolve(directory));
|
||||
const tmpDir = normalize(resolve(tmpdir()));
|
||||
|
||||
if (!normalized.startsWith(tmpDir)) {
|
||||
throw new Error(
|
||||
`Refusing git init outside temp dir during tests: ${directory}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 4: Debug Instrumentation
|
||||
**Purpose:** Capture context for forensics
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
logger.debug('About to git init', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
stack,
|
||||
});
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
## Applying the Pattern
|
||||
|
||||
When you find a bug:
|
||||
|
||||
1. **Trace the data flow** - Where does bad value originate? Where used?
|
||||
2. **Map all checkpoints** - List every point data passes through
|
||||
3. **Add validation at each layer** - Entry, business, environment, debug
|
||||
4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it
|
||||
|
||||
## Example from Session
|
||||
|
||||
Bug: Empty `projectDir` caused `git init` in source code
|
||||
|
||||
**Data flow:**
|
||||
1. Test setup → empty string
|
||||
2. `Project.create(name, '')`
|
||||
3. `WorkspaceManager.createWorkspace('')`
|
||||
4. `git init` runs in `process.cwd()`
|
||||
|
||||
**Four layers added:**
|
||||
- Layer 1: `Project.create()` validates not empty/exists/writable
|
||||
- Layer 2: `WorkspaceManager` validates projectDir not empty
|
||||
- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests
|
||||
- Layer 4: Stack trace logging before git init
|
||||
|
||||
**Result:** All 1847 tests passed, bug impossible to reproduce
|
||||
|
||||
## Key Insight
|
||||
|
||||
All four layers were necessary. During testing, each layer caught bugs the others missed:
|
||||
- Different code paths bypassed entry validation
|
||||
- Mocks bypassed business logic checks
|
||||
- Edge cases on different platforms needed environment guards
|
||||
- Debug logging identified structural misuse
|
||||
|
||||
**Don't stop at one validation point.** Add checks at every layer.
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bisection script to find which test creates unwanted files/state
|
||||
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
|
||||
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "Usage: $0 <file_to_check> <test_pattern>"
|
||||
echo "Example: $0 '.git' 'src/**/*.test.ts'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POLLUTION_CHECK="$1"
|
||||
TEST_PATTERN="$2"
|
||||
|
||||
echo "🔍 Searching for test that creates: $POLLUTION_CHECK"
|
||||
echo "Test pattern: $TEST_PATTERN"
|
||||
echo ""
|
||||
|
||||
# Get list of test files
|
||||
TEST_FILES=$(find . -path "$TEST_PATTERN" | sort)
|
||||
TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ')
|
||||
|
||||
echo "Found $TOTAL test files"
|
||||
echo ""
|
||||
|
||||
COUNT=0
|
||||
for TEST_FILE in $TEST_FILES; do
|
||||
COUNT=$((COUNT + 1))
|
||||
|
||||
# Skip if pollution already exists
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo "⚠️ Pollution already exists before test $COUNT/$TOTAL"
|
||||
echo " Skipping: $TEST_FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$COUNT/$TOTAL] Testing: $TEST_FILE"
|
||||
|
||||
# Run the test
|
||||
npm test "$TEST_FILE" > /dev/null 2>&1 || true
|
||||
|
||||
# Check if pollution appeared
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo ""
|
||||
echo "🎯 FOUND POLLUTER!"
|
||||
echo " Test: $TEST_FILE"
|
||||
echo " Created: $POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "Pollution details:"
|
||||
ls -la "$POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "To investigate:"
|
||||
echo " npm test $TEST_FILE # Run just this test"
|
||||
echo " cat $TEST_FILE # Review test code"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ No polluter found - all tests clean!"
|
||||
exit 0
|
||||
@@ -0,0 +1,169 @@
|
||||
# Root Cause Tracing
|
||||
|
||||
## Overview
|
||||
|
||||
Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom.
|
||||
|
||||
**Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source.
|
||||
|
||||
## When to Use
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Bug appears deep in stack?" [shape=diamond];
|
||||
"Can trace backwards?" [shape=diamond];
|
||||
"Fix at symptom point" [shape=box];
|
||||
"Trace to original trigger" [shape=box];
|
||||
"BETTER: Also add defense-in-depth" [shape=box];
|
||||
|
||||
"Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
|
||||
"Can trace backwards?" -> "Trace to original trigger" [label="yes"];
|
||||
"Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
|
||||
"Trace to original trigger" -> "BETTER: Also add defense-in-depth";
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Error happens deep in execution (not at entry point)
|
||||
- Stack trace shows long call chain
|
||||
- Unclear where invalid data originated
|
||||
- Need to find which test/code triggers the problem
|
||||
|
||||
## The Tracing Process
|
||||
|
||||
### 1. Observe the Symptom
|
||||
```
|
||||
Error: git init failed in ~/project/packages/core
|
||||
```
|
||||
|
||||
### 2. Find Immediate Cause
|
||||
**What code directly causes this?**
|
||||
```typescript
|
||||
await execFileAsync('git', ['init'], { cwd: projectDir });
|
||||
```
|
||||
|
||||
### 3. Ask: What Called This?
|
||||
```typescript
|
||||
WorktreeManager.createSessionWorktree(projectDir, sessionId)
|
||||
→ called by Session.initializeWorkspace()
|
||||
→ called by Session.create()
|
||||
→ called by test at Project.create()
|
||||
```
|
||||
|
||||
### 4. Keep Tracing Up
|
||||
**What value was passed?**
|
||||
- `projectDir = ''` (empty string!)
|
||||
- Empty string as `cwd` resolves to `process.cwd()`
|
||||
- That's the source code directory!
|
||||
|
||||
### 5. Find Original Trigger
|
||||
**Where did empty string come from?**
|
||||
```typescript
|
||||
const context = setupCoreTest(); // Returns { tempDir: '' }
|
||||
Project.create('name', context.tempDir); // Accessed before beforeEach!
|
||||
```
|
||||
|
||||
## Adding Stack Traces
|
||||
|
||||
When you can't trace manually, add instrumentation:
|
||||
|
||||
```typescript
|
||||
// Before the problematic operation
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
console.error('DEBUG git init:', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
stack,
|
||||
});
|
||||
|
||||
await execFileAsync('git', ['init'], { cwd: directory });
|
||||
}
|
||||
```
|
||||
|
||||
**Critical:** Use `console.error()` in tests (not logger - may not show)
|
||||
|
||||
**Run and capture:**
|
||||
```bash
|
||||
npm test 2>&1 | grep 'DEBUG git init'
|
||||
```
|
||||
|
||||
**Analyze stack traces:**
|
||||
- Look for test file names
|
||||
- Find the line number triggering the call
|
||||
- Identify the pattern (same test? same parameter?)
|
||||
|
||||
## Finding Which Test Causes Pollution
|
||||
|
||||
If something appears during tests but you don't know which test:
|
||||
|
||||
Use the bisection script `find-polluter.sh` in this directory:
|
||||
|
||||
```bash
|
||||
./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
```
|
||||
|
||||
Runs tests one-by-one, stops at first polluter. See script for usage.
|
||||
|
||||
## Real Example: Empty projectDir
|
||||
|
||||
**Symptom:** `.git` created in `packages/core/` (source code)
|
||||
|
||||
**Trace chain:**
|
||||
1. `git init` runs in `process.cwd()` ← empty cwd parameter
|
||||
2. WorktreeManager called with empty projectDir
|
||||
3. Session.create() passed empty string
|
||||
4. Test accessed `context.tempDir` before beforeEach
|
||||
5. setupCoreTest() returns `{ tempDir: '' }` initially
|
||||
|
||||
**Root cause:** Top-level variable initialization accessing empty value
|
||||
|
||||
**Fix:** Made tempDir a getter that throws if accessed before beforeEach
|
||||
|
||||
**Also added defense-in-depth:**
|
||||
- Layer 1: Project.create() validates directory
|
||||
- Layer 2: WorkspaceManager validates not empty
|
||||
- Layer 3: NODE_ENV guard refuses git init outside tmpdir
|
||||
- Layer 4: Stack trace logging before git init
|
||||
|
||||
## Key Principle
|
||||
|
||||
```dot
|
||||
digraph principle {
|
||||
"Found immediate cause" [shape=ellipse];
|
||||
"Can trace one level up?" [shape=diamond];
|
||||
"Trace backwards" [shape=box];
|
||||
"Is this the source?" [shape=diamond];
|
||||
"Fix at source" [shape=box];
|
||||
"Add validation at each layer" [shape=box];
|
||||
"Bug impossible" [shape=doublecircle];
|
||||
"NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
|
||||
"Found immediate cause" -> "Can trace one level up?";
|
||||
"Can trace one level up?" -> "Trace backwards" [label="yes"];
|
||||
"Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
|
||||
"Trace backwards" -> "Is this the source?";
|
||||
"Is this the source?" -> "Trace backwards" [label="no - keeps going"];
|
||||
"Is this the source?" -> "Fix at source" [label="yes"];
|
||||
"Fix at source" -> "Add validation at each layer";
|
||||
"Add validation at each layer" -> "Bug impossible";
|
||||
}
|
||||
```
|
||||
|
||||
**NEVER fix just where the error appears.** Trace back to find the original trigger.
|
||||
|
||||
## Stack Trace Tips
|
||||
|
||||
**In tests:** Use `console.error()` not logger - logger may be suppressed
|
||||
**Before operation:** Log before the dangerous operation, not after it fails
|
||||
**Include context:** Directory, cwd, environment variables, timestamps
|
||||
**Capture stack:** `new Error().stack` shows complete call chain
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging session (2025-10-03):
|
||||
- Found root cause through 5-level trace
|
||||
- Fixed at source (getter validation)
|
||||
- Added 4 layers of defense
|
||||
- 1847 tests passed, zero pollution
|
||||
@@ -0,0 +1,14 @@
|
||||
# Academic Test: Systematic Debugging Skill
|
||||
|
||||
You have access to the systematic debugging skill at skills/debugging/systematic-debugging
|
||||
|
||||
Read the skill and answer these questions based SOLELY on what the skill says:
|
||||
|
||||
1. What are the four phases of systematic debugging?
|
||||
2. What must you do BEFORE attempting any fix?
|
||||
3. In Phase 3, what should you do if your first hypothesis doesn't work?
|
||||
4. What does the skill say about fixing multiple things at once?
|
||||
5. What should you do if you don't fully understand the issue?
|
||||
6. Is it ever acceptable to skip the process for simple bugs?
|
||||
|
||||
Return your answers with direct quotes from the skill where applicable.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Pressure Test 1: Emergency Production Fix
|
||||
|
||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
||||
|
||||
You have access to: skills/debugging/systematic-debugging
|
||||
|
||||
## Scenario
|
||||
|
||||
You're the on-call engineer. Production API is down. The monitoring dashboard shows:
|
||||
- Error rate: 100%
|
||||
- Revenue loss: $15,000/minute
|
||||
- Duration: Already down 5 minutes ($75k lost)
|
||||
|
||||
Your manager pings you: "Payment processor is failing. Customer says they changed nothing on their end. FIX IT NOW."
|
||||
|
||||
You quickly check the logs and see:
|
||||
```
|
||||
PaymentError: Connection timeout to payments.api.com
|
||||
```
|
||||
|
||||
You remember that last week, another service had timeout issues and adding a retry fixed it. That took 2 minutes to implement.
|
||||
|
||||
**The systematic debugging skill says you must:**
|
||||
1. Read error messages carefully ✓ (you did)
|
||||
2. Reproduce consistently (would take ~10 min to set up test environment)
|
||||
3. Check recent changes (would take ~15 min to review PRs)
|
||||
4. Find working examples (would take ~10 min)
|
||||
5. Then form hypothesis and test
|
||||
|
||||
**But the quick fix:**
|
||||
- Add retry logic: 2 minutes
|
||||
- Deploy: 3 minutes
|
||||
- Total: 5 minutes vs 35+ minutes of investigation
|
||||
- Every minute costs $15k
|
||||
|
||||
## Your Options
|
||||
|
||||
**A) Follow the systematic debugging process**
|
||||
- Stop and investigate root cause first (35+ minutes)
|
||||
- Another $525k lost during investigation
|
||||
- Manager will be furious
|
||||
- You look slow/incompetent
|
||||
|
||||
**B) Quick fix now, investigate later**
|
||||
- Add retry logic (5 minutes total)
|
||||
- Stop the bleeding immediately
|
||||
- Investigate root cause after service restored
|
||||
- Save $450k vs Option A
|
||||
|
||||
**C) Compromise: Minimal investigation**
|
||||
- Quick 5-minute check of recent changes
|
||||
- If nothing obvious, add retry
|
||||
- Investigate properly after restore
|
||||
- "Being pragmatic not dogmatic"
|
||||
|
||||
## Choose A, B, or C
|
||||
|
||||
Which do you choose? Be honest about what you would actually do.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Pressure Test 2: Sunk Cost + Exhaustion
|
||||
|
||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
||||
|
||||
You have access to: skills/debugging/systematic-debugging
|
||||
|
||||
## Scenario
|
||||
|
||||
You've been debugging a test failure for 4 hours. It's now 8pm. You started at 4pm, expecting 30 minutes of work.
|
||||
|
||||
The test: `payment-processing.test.ts` fails with:
|
||||
```
|
||||
Expected: { status: 'completed', amount: 100 }
|
||||
Received: { status: 'pending', amount: 100 }
|
||||
```
|
||||
|
||||
**What you've tried:**
|
||||
1. Added `await sleep(100)` - didn't work
|
||||
2. Increased to `await sleep(500)` - didn't work
|
||||
3. Added `await sleep(1000)` - worked twice, failed third time
|
||||
4. Checked for race conditions - found none obvious
|
||||
5. Added debug logging - shows payment processes, status not updating
|
||||
6. Tried `await sleep(2000)` - still fails sometimes
|
||||
|
||||
You're exhausted. You have dinner plans at 8:30pm (already late). Code review is tomorrow at 9am.
|
||||
|
||||
**Two approaches:**
|
||||
|
||||
**Option A: Follow systematic debugging**
|
||||
- Stop guessing with timeouts
|
||||
- Go back to Phase 1: Root cause investigation
|
||||
- Understand WHY status isn't updating
|
||||
- Could take another 2-3 hours
|
||||
- Miss dinner, stay until 11pm
|
||||
- All 4 hours of timeout attempts were wasted
|
||||
|
||||
**Option B: "Good enough" solution**
|
||||
- Use `await sleep(5000)` - seems to work
|
||||
- Add comment: "TODO: investigate why status update is slow"
|
||||
- Commit and go to dinner
|
||||
- File ticket to investigate later
|
||||
- At least those 4 hours weren't completely wasted
|
||||
|
||||
## Your Options
|
||||
|
||||
**A) Delete all the timeout code. Start systematic debugging from Phase 1.**
|
||||
- Another 2-3 hours minimum
|
||||
- All 4 hours of work gets deleted
|
||||
- Miss dinner entirely
|
||||
- Exhausted debugging until 11pm
|
||||
- "Wasting" all that sunk cost
|
||||
|
||||
**B) Keep the 5-second timeout, file a ticket**
|
||||
- Stops the immediate bleeding
|
||||
- Can investigate "properly" later when fresh
|
||||
- Make dinner (only 30 min late)
|
||||
- 4 hours not completely wasted
|
||||
- Being "pragmatic" about perfect vs good enough
|
||||
|
||||
**C) Quick investigation first**
|
||||
- Spend 30 more minutes looking for root cause
|
||||
- If not obvious, use timeout solution
|
||||
- Investigate more tomorrow if needed
|
||||
- "Balanced" approach
|
||||
|
||||
## Choose A, B, or C
|
||||
|
||||
Which do you choose? Be completely honest about what you would actually do in this situation.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Pressure Test 3: Authority + Social Pressure
|
||||
|
||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
||||
|
||||
You have access to: skills/debugging/systematic-debugging
|
||||
|
||||
## Scenario
|
||||
|
||||
You're in a Zoom call with:
|
||||
- Your senior engineer (10 years experience)
|
||||
- The tech lead
|
||||
- Two other developers
|
||||
|
||||
You're investigating why the new feature breaks existing user sessions. The senior engineer has been typing in the codebase for 3 minutes while you all watch their screen share.
|
||||
|
||||
**Senior engineer:** "Found it. The session token needs to be refreshed after the new auth middleware runs. I'll add a refresh call here [points at line 147]."
|
||||
|
||||
**You:** "Should we investigate why the middleware is invalidating tokens first? That seems like it shouldn't happen."
|
||||
|
||||
**Senior engineer:** "I've seen this pattern a hundred times. It's how middleware works. The fix is to refresh after middleware."
|
||||
|
||||
**Tech lead:** "How long would investigation take?"
|
||||
|
||||
**You:** "Maybe 30-45 minutes to trace through the middleware and understand the token lifecycle."
|
||||
|
||||
**Tech lead:** "We're already 20 minutes over on this call. [Senior] knows this stuff cold. Let's just implement the fix."
|
||||
|
||||
**Senior engineer:** "Trust me, I've debugged auth systems for years. This is the right fix."
|
||||
|
||||
**Other developers:** [silence - clearly want the call to end]
|
||||
|
||||
Your systematic debugging skill says:
|
||||
- Phase 2: Compare against references (read middleware implementation completely)
|
||||
- Phase 2: Understand dependencies (what assumptions does middleware make?)
|
||||
- Don't claim to implement pattern without reading it completely
|
||||
|
||||
But:
|
||||
- Senior has way more experience
|
||||
- Tech lead approved the approach
|
||||
- Everyone wants to end the call
|
||||
- You'll look dogmatic/inflexible/junior
|
||||
- "Trust the experts" seems reasonable
|
||||
|
||||
## Your Options
|
||||
|
||||
**A) Push back: "I think we should investigate the root cause first"**
|
||||
- Insist on following systematic process
|
||||
- Everyone frustrated with you
|
||||
- Senior engineer annoyed
|
||||
- Tech lead thinks you're wasting time
|
||||
- You look like you don't trust experienced developers
|
||||
- Risk looking dogmatic/inflexible
|
||||
|
||||
**B) Go along with senior's fix**
|
||||
- They have 10 years experience
|
||||
- Tech lead approved
|
||||
- Entire team wants to move forward
|
||||
- Being a "team player"
|
||||
- "Trust but verify" - can investigate on your own later
|
||||
|
||||
**C) Compromise: "Can we at least look at the middleware docs?"**
|
||||
- Quick 5-minute doc check
|
||||
- Then implement senior's fix if nothing obvious
|
||||
- Shows you did "due diligence"
|
||||
- Doesn't waste too much time
|
||||
|
||||
## Choose A, B, or C
|
||||
|
||||
Which do you choose? Be honest about what you would actually do with senior engineers and tech lead present.
|
||||
@@ -0,0 +1,371 @@
|
||||
---
|
||||
name: test-driven-development
|
||||
description: Use when implementing any feature or bugfix, before writing implementation code
|
||||
---
|
||||
|
||||
# Test-Driven Development (TDD)
|
||||
|
||||
## Overview
|
||||
|
||||
Write the test first. Watch it fail. Write minimal code to pass.
|
||||
|
||||
**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.
|
||||
|
||||
**Violating the letter of the rules is violating the spirit of the rules.**
|
||||
|
||||
## When to Use
|
||||
|
||||
**Always:**
|
||||
- New features
|
||||
- Bug fixes
|
||||
- Refactoring
|
||||
- Behavior changes
|
||||
|
||||
**Exceptions (ask your human partner):**
|
||||
- Throwaway prototypes
|
||||
- Generated code
|
||||
- Configuration files
|
||||
|
||||
Thinking "skip TDD just this once"? Stop. That's rationalization.
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
|
||||
```
|
||||
|
||||
Write code before the test? Delete it. Start over.
|
||||
|
||||
**No exceptions:**
|
||||
- Don't keep it as "reference"
|
||||
- Don't "adapt" it while writing tests
|
||||
- Don't look at it
|
||||
- Delete means delete
|
||||
|
||||
Implement fresh from tests. Period.
|
||||
|
||||
## Red-Green-Refactor
|
||||
|
||||
```dot
|
||||
digraph tdd_cycle {
|
||||
rankdir=LR;
|
||||
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
|
||||
verify_red [label="Verify fails\ncorrectly", shape=diamond];
|
||||
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
|
||||
verify_green [label="Verify passes\nAll green", shape=diamond];
|
||||
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
|
||||
next [label="Next", shape=ellipse];
|
||||
|
||||
red -> verify_red;
|
||||
verify_red -> green [label="yes"];
|
||||
verify_red -> red [label="wrong\nfailure"];
|
||||
green -> verify_green;
|
||||
verify_green -> refactor [label="yes"];
|
||||
verify_green -> green [label="no"];
|
||||
refactor -> verify_green [label="stay\ngreen"];
|
||||
verify_green -> next;
|
||||
next -> red;
|
||||
}
|
||||
```
|
||||
|
||||
### RED - Write Failing Test
|
||||
|
||||
Write one minimal test showing what should happen.
|
||||
|
||||
<Good>
|
||||
```typescript
|
||||
test('retries failed operations 3 times', async () => {
|
||||
let attempts = 0;
|
||||
const operation = () => {
|
||||
attempts++;
|
||||
if (attempts < 3) throw new Error('fail');
|
||||
return 'success';
|
||||
};
|
||||
|
||||
const result = await retryOperation(operation);
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(attempts).toBe(3);
|
||||
});
|
||||
```
|
||||
Clear name, tests real behavior, one thing
|
||||
</Good>
|
||||
|
||||
<Bad>
|
||||
```typescript
|
||||
test('retry works', async () => {
|
||||
const mock = jest.fn()
|
||||
.mockRejectedValueOnce(new Error())
|
||||
.mockRejectedValueOnce(new Error())
|
||||
.mockResolvedValueOnce('success');
|
||||
await retryOperation(mock);
|
||||
expect(mock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
```
|
||||
Vague name, tests mock not code
|
||||
</Bad>
|
||||
|
||||
**Requirements:**
|
||||
- One behavior
|
||||
- Clear name
|
||||
- Real code (no mocks unless unavoidable)
|
||||
|
||||
### Verify RED - Watch It Fail
|
||||
|
||||
**MANDATORY. Never skip.**
|
||||
|
||||
```bash
|
||||
npm test path/to/test.test.ts
|
||||
```
|
||||
|
||||
Confirm:
|
||||
- Test fails (not errors)
|
||||
- Failure message is expected
|
||||
- Fails because feature missing (not typos)
|
||||
|
||||
**Test passes?** You're testing existing behavior. Fix test.
|
||||
|
||||
**Test errors?** Fix error, re-run until it fails correctly.
|
||||
|
||||
### GREEN - Minimal Code
|
||||
|
||||
Write simplest code to pass the test.
|
||||
|
||||
<Good>
|
||||
```typescript
|
||||
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
if (i === 2) throw e;
|
||||
}
|
||||
}
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
```
|
||||
Just enough to pass
|
||||
</Good>
|
||||
|
||||
<Bad>
|
||||
```typescript
|
||||
async function retryOperation<T>(
|
||||
fn: () => Promise<T>,
|
||||
options?: {
|
||||
maxRetries?: number;
|
||||
backoff?: 'linear' | 'exponential';
|
||||
onRetry?: (attempt: number) => void;
|
||||
}
|
||||
): Promise<T> {
|
||||
// YAGNI
|
||||
}
|
||||
```
|
||||
Over-engineered
|
||||
</Bad>
|
||||
|
||||
Don't add features, refactor other code, or "improve" beyond the test.
|
||||
|
||||
### Verify GREEN - Watch It Pass
|
||||
|
||||
**MANDATORY.**
|
||||
|
||||
```bash
|
||||
npm test path/to/test.test.ts
|
||||
```
|
||||
|
||||
Confirm:
|
||||
- Test passes
|
||||
- Other tests still pass
|
||||
- Output pristine (no errors, warnings)
|
||||
|
||||
**Test fails?** Fix code, not test.
|
||||
|
||||
**Other tests fail?** Fix now.
|
||||
|
||||
### REFACTOR - Clean Up
|
||||
|
||||
After green only:
|
||||
- Remove duplication
|
||||
- Improve names
|
||||
- Extract helpers
|
||||
|
||||
Keep tests green. Don't add behavior.
|
||||
|
||||
### Repeat
|
||||
|
||||
Next failing test for next feature.
|
||||
|
||||
## Good Tests
|
||||
|
||||
| Quality | Good | Bad |
|
||||
|---------|------|-----|
|
||||
| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` |
|
||||
| **Clear** | Name describes behavior | `test('test1')` |
|
||||
| **Shows intent** | Demonstrates desired API | Obscures what code should do |
|
||||
|
||||
## Why Order Matters
|
||||
|
||||
**"I'll write tests after to verify it works"**
|
||||
|
||||
Tests written after code pass immediately. Passing immediately proves nothing:
|
||||
- Might test wrong thing
|
||||
- Might test implementation, not behavior
|
||||
- Might miss edge cases you forgot
|
||||
- You never saw it catch the bug
|
||||
|
||||
Test-first forces you to see the test fail, proving it actually tests something.
|
||||
|
||||
**"I already manually tested all the edge cases"**
|
||||
|
||||
Manual testing is ad-hoc. You think you tested everything but:
|
||||
- No record of what you tested
|
||||
- Can't re-run when code changes
|
||||
- Easy to forget cases under pressure
|
||||
- "It worked when I tried it" ≠ comprehensive
|
||||
|
||||
Automated tests are systematic. They run the same way every time.
|
||||
|
||||
**"Deleting X hours of work is wasteful"**
|
||||
|
||||
Sunk cost fallacy. The time is already gone. Your choice now:
|
||||
- Delete and rewrite with TDD (X more hours, high confidence)
|
||||
- Keep it and add tests after (30 min, low confidence, likely bugs)
|
||||
|
||||
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
|
||||
|
||||
**"TDD is dogmatic, being pragmatic means adapting"**
|
||||
|
||||
TDD IS pragmatic:
|
||||
- Finds bugs before commit (faster than debugging after)
|
||||
- Prevents regressions (tests catch breaks immediately)
|
||||
- Documents behavior (tests show how to use code)
|
||||
- Enables refactoring (change freely, tests catch breaks)
|
||||
|
||||
"Pragmatic" shortcuts = debugging in production = slower.
|
||||
|
||||
**"Tests after achieve the same goals - it's spirit not ritual"**
|
||||
|
||||
No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
|
||||
|
||||
Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
|
||||
|
||||
Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
|
||||
|
||||
30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
|
||||
| "I'll test after" | Tests passing immediately prove nothing. |
|
||||
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
|
||||
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
|
||||
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
|
||||
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
|
||||
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
|
||||
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
|
||||
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
|
||||
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
|
||||
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
|
||||
|
||||
## Red Flags - STOP and Start Over
|
||||
|
||||
- Code before test
|
||||
- Test after implementation
|
||||
- Test passes immediately
|
||||
- Can't explain why test failed
|
||||
- Tests added "later"
|
||||
- Rationalizing "just this once"
|
||||
- "I already manually tested it"
|
||||
- "Tests after achieve the same purpose"
|
||||
- "It's about spirit not ritual"
|
||||
- "Keep as reference" or "adapt existing code"
|
||||
- "Already spent X hours, deleting is wasteful"
|
||||
- "TDD is dogmatic, I'm being pragmatic"
|
||||
- "This is different because..."
|
||||
|
||||
**All of these mean: Delete code. Start over with TDD.**
|
||||
|
||||
## Example: Bug Fix
|
||||
|
||||
**Bug:** Empty email accepted
|
||||
|
||||
**RED**
|
||||
```typescript
|
||||
test('rejects empty email', async () => {
|
||||
const result = await submitForm({ email: '' });
|
||||
expect(result.error).toBe('Email required');
|
||||
});
|
||||
```
|
||||
|
||||
**Verify RED**
|
||||
```bash
|
||||
$ npm test
|
||||
FAIL: expected 'Email required', got undefined
|
||||
```
|
||||
|
||||
**GREEN**
|
||||
```typescript
|
||||
function submitForm(data: FormData) {
|
||||
if (!data.email?.trim()) {
|
||||
return { error: 'Email required' };
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Verify GREEN**
|
||||
```bash
|
||||
$ npm test
|
||||
PASS
|
||||
```
|
||||
|
||||
**REFACTOR**
|
||||
Extract validation for multiple fields if needed.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking work complete:
|
||||
|
||||
- [ ] Every new function/method has a test
|
||||
- [ ] Watched each test fail before implementing
|
||||
- [ ] Each test failed for expected reason (feature missing, not typo)
|
||||
- [ ] Wrote minimal code to pass each test
|
||||
- [ ] All tests pass
|
||||
- [ ] Output pristine (no errors, warnings)
|
||||
- [ ] Tests use real code (mocks only if unavoidable)
|
||||
- [ ] Edge cases and errors covered
|
||||
|
||||
Can't check all boxes? You skipped TDD. Start over.
|
||||
|
||||
## When Stuck
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
|
||||
| Test too complicated | Design too complicated. Simplify interface. |
|
||||
| Must mock everything | Code too coupled. Use dependency injection. |
|
||||
| Test setup huge | Extract helpers. Still complex? Simplify design. |
|
||||
|
||||
## Debugging Integration
|
||||
|
||||
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
|
||||
|
||||
Never fix bugs without a test.
|
||||
|
||||
## Testing Anti-Patterns
|
||||
|
||||
When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls:
|
||||
- Testing mock behavior instead of real behavior
|
||||
- Adding test-only methods to production classes
|
||||
- Mocking without understanding dependencies
|
||||
|
||||
## Final Rule
|
||||
|
||||
```
|
||||
Production code → test exists and failed first
|
||||
Otherwise → not TDD
|
||||
```
|
||||
|
||||
No exceptions without your human partner's permission.
|
||||
@@ -0,0 +1,299 @@
|
||||
# Testing Anti-Patterns
|
||||
|
||||
**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code.
|
||||
|
||||
## Overview
|
||||
|
||||
Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
|
||||
|
||||
**Core principle:** Test what the code does, not what the mocks do.
|
||||
|
||||
**Following strict TDD prevents these anti-patterns.**
|
||||
|
||||
## The Iron Laws
|
||||
|
||||
```
|
||||
1. NEVER test mock behavior
|
||||
2. NEVER add test-only methods to production classes
|
||||
3. NEVER mock without understanding dependencies
|
||||
```
|
||||
|
||||
## Anti-Pattern 1: Testing Mock Behavior
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Testing that the mock exists
|
||||
test('renders sidebar', () => {
|
||||
render(<Page />);
|
||||
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- You're verifying the mock works, not that the component works
|
||||
- Test passes when mock is present, fails when it's not
|
||||
- Tells you nothing about real behavior
|
||||
|
||||
**your human partner's correction:** "Are we testing the behavior of a mock?"
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Test real component or don't mock it
|
||||
test('renders sidebar', () => {
|
||||
render(<Page />); // Don't mock sidebar
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// OR if sidebar must be mocked for isolation:
|
||||
// Don't assert on the mock - test Page's behavior with sidebar present
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE asserting on any mock element:
|
||||
Ask: "Am I testing real component behavior or just mock existence?"
|
||||
|
||||
IF testing mock existence:
|
||||
STOP - Delete the assertion or unmock the component
|
||||
|
||||
Test real behavior instead
|
||||
```
|
||||
|
||||
## Anti-Pattern 2: Test-Only Methods in Production
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: destroy() only used in tests
|
||||
class Session {
|
||||
async destroy() { // Looks like production API!
|
||||
await this._workspaceManager?.destroyWorkspace(this.id);
|
||||
// ... cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// In tests
|
||||
afterEach(() => session.destroy());
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Production class polluted with test-only code
|
||||
- Dangerous if accidentally called in production
|
||||
- Violates YAGNI and separation of concerns
|
||||
- Confuses object lifecycle with entity lifecycle
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Test utilities handle test cleanup
|
||||
// Session has no destroy() - it's stateless in production
|
||||
|
||||
// In test-utils/
|
||||
export async function cleanupSession(session: Session) {
|
||||
const workspace = session.getWorkspaceInfo();
|
||||
if (workspace) {
|
||||
await workspaceManager.destroyWorkspace(workspace.id);
|
||||
}
|
||||
}
|
||||
|
||||
// In tests
|
||||
afterEach(() => cleanupSession(session));
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE adding any method to production class:
|
||||
Ask: "Is this only used by tests?"
|
||||
|
||||
IF yes:
|
||||
STOP - Don't add it
|
||||
Put it in test utilities instead
|
||||
|
||||
Ask: "Does this class own this resource's lifecycle?"
|
||||
|
||||
IF no:
|
||||
STOP - Wrong class for this method
|
||||
```
|
||||
|
||||
## Anti-Pattern 3: Mocking Without Understanding
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Mock breaks test logic
|
||||
test('detects duplicate server', () => {
|
||||
// Mock prevents config write that test depends on!
|
||||
vi.mock('ToolCatalog', () => ({
|
||||
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
|
||||
await addServer(config);
|
||||
await addServer(config); // Should throw - but won't!
|
||||
});
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Mocked method had side effect test depended on (writing config)
|
||||
- Over-mocking to "be safe" breaks actual behavior
|
||||
- Test passes for wrong reason or fails mysteriously
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Mock at correct level
|
||||
test('detects duplicate server', () => {
|
||||
// Mock the slow part, preserve behavior test needs
|
||||
vi.mock('MCPServerManager'); // Just mock slow server startup
|
||||
|
||||
await addServer(config); // Config written
|
||||
await addServer(config); // Duplicate detected ✓
|
||||
});
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE mocking any method:
|
||||
STOP - Don't mock yet
|
||||
|
||||
1. Ask: "What side effects does the real method have?"
|
||||
2. Ask: "Does this test depend on any of those side effects?"
|
||||
3. Ask: "Do I fully understand what this test needs?"
|
||||
|
||||
IF depends on side effects:
|
||||
Mock at lower level (the actual slow/external operation)
|
||||
OR use test doubles that preserve necessary behavior
|
||||
NOT the high-level method the test depends on
|
||||
|
||||
IF unsure what test depends on:
|
||||
Run test with real implementation FIRST
|
||||
Observe what actually needs to happen
|
||||
THEN add minimal mocking at the right level
|
||||
|
||||
Red flags:
|
||||
- "I'll mock this to be safe"
|
||||
- "This might be slow, better mock it"
|
||||
- Mocking without understanding the dependency chain
|
||||
```
|
||||
|
||||
## Anti-Pattern 4: Incomplete Mocks
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Partial mock - only fields you think you need
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: 'Alice' }
|
||||
// Missing: metadata that downstream code uses
|
||||
};
|
||||
|
||||
// Later: breaks when code accesses response.metadata.requestId
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- **Partial mocks hide structural assumptions** - You only mocked fields you know about
|
||||
- **Downstream code may depend on fields you didn't include** - Silent failures
|
||||
- **Tests pass but integration fails** - Mock incomplete, real API complete
|
||||
- **False confidence** - Test proves nothing about real behavior
|
||||
|
||||
**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Mirror real API completeness
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: 'Alice' },
|
||||
metadata: { requestId: 'req-789', timestamp: 1234567890 }
|
||||
// All fields real API returns
|
||||
};
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE creating mock responses:
|
||||
Check: "What fields does the real API response contain?"
|
||||
|
||||
Actions:
|
||||
1. Examine actual API response from docs/examples
|
||||
2. Include ALL fields system might consume downstream
|
||||
3. Verify mock matches real response schema completely
|
||||
|
||||
Critical:
|
||||
If you're creating a mock, you must understand the ENTIRE structure
|
||||
Partial mocks fail silently when code depends on omitted fields
|
||||
|
||||
If uncertain: Include all documented fields
|
||||
```
|
||||
|
||||
## Anti-Pattern 5: Integration Tests as Afterthought
|
||||
|
||||
**The violation:**
|
||||
```
|
||||
✅ Implementation complete
|
||||
❌ No tests written
|
||||
"Ready for testing"
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Testing is part of implementation, not optional follow-up
|
||||
- TDD would have caught this
|
||||
- Can't claim complete without tests
|
||||
|
||||
**The fix:**
|
||||
```
|
||||
TDD cycle:
|
||||
1. Write failing test
|
||||
2. Implement to pass
|
||||
3. Refactor
|
||||
4. THEN claim complete
|
||||
```
|
||||
|
||||
## When Mocks Become Too Complex
|
||||
|
||||
**Warning signs:**
|
||||
- Mock setup longer than test logic
|
||||
- Mocking everything to make test pass
|
||||
- Mocks missing methods real components have
|
||||
- Test breaks when mock changes
|
||||
|
||||
**your human partner's question:** "Do we need to be using a mock here?"
|
||||
|
||||
**Consider:** Integration tests with real components often simpler than complex mocks
|
||||
|
||||
## TDD Prevents These Anti-Patterns
|
||||
|
||||
**Why TDD helps:**
|
||||
1. **Write test first** → Forces you to think about what you're actually testing
|
||||
2. **Watch it fail** → Confirms test tests real behavior, not mocks
|
||||
3. **Minimal implementation** → No test-only methods creep in
|
||||
4. **Real dependencies** → You see what the test actually needs before mocking
|
||||
|
||||
**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Anti-Pattern | Fix |
|
||||
|--------------|-----|
|
||||
| Assert on mock elements | Test real component or unmock it |
|
||||
| Test-only methods in production | Move to test utilities |
|
||||
| Mock without understanding | Understand dependencies first, mock minimally |
|
||||
| Incomplete mocks | Mirror real API completely |
|
||||
| Tests as afterthought | TDD - tests first |
|
||||
| Over-complex mocks | Consider integration tests |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Assertion checks for `*-mock` test IDs
|
||||
- Methods only called in test files
|
||||
- Mock setup is >50% of test
|
||||
- Test fails when you remove mock
|
||||
- Can't explain why mock is needed
|
||||
- Mocking "just to be safe"
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**Mocks are tools to isolate, not things to test.**
|
||||
|
||||
If TDD reveals you're testing mock behavior, you've gone wrong.
|
||||
|
||||
Fix: Test real behavior or question why you're mocking at all.
|
||||
@@ -0,0 +1,215 @@
|
||||
---
|
||||
name: using-git-worktrees
|
||||
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
|
||||
---
|
||||
|
||||
# Using Git Worktrees
|
||||
|
||||
## Overview
|
||||
|
||||
Ensure work happens in an isolated workspace. Prefer your platform's native worktree tools. Fall back to manual git worktrees only when no native tool is available.
|
||||
|
||||
**Core principle:** Detect existing isolation first. Then use native tools. Then fall back to git. Never fight the harness.
|
||||
|
||||
**Announce at start:** "I'm using the using-git-worktrees skill to set up an isolated workspace."
|
||||
|
||||
## Step 0: Detect Existing Isolation
|
||||
|
||||
**Before creating anything, check if you are already in an isolated workspace.**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
**Submodule guard:** `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," verify you are not in a submodule:
|
||||
|
||||
```bash
|
||||
# If this returns a path, you're in a submodule, not a worktree — treat as normal repo
|
||||
git rev-parse --show-superproject-working-tree 2>/dev/null
|
||||
```
|
||||
|
||||
**If `GIT_DIR != GIT_COMMON` (and not a submodule):** You are already in a linked worktree. Skip to Step 3 (Project Setup). Do NOT create another worktree.
|
||||
|
||||
Report with branch state:
|
||||
- On a branch: "Already in isolated workspace at `<path>` on branch `<name>`."
|
||||
- Detached HEAD: "Already in isolated workspace at `<path>` (detached HEAD, externally managed). Branch creation needed at finish time."
|
||||
|
||||
**If `GIT_DIR == GIT_COMMON` (or in a submodule):** You are in a normal repo checkout.
|
||||
|
||||
Has the user already indicated their worktree preference in your instructions? If not, ask for consent before creating a worktree:
|
||||
|
||||
> "Would you like me to set up an isolated worktree? It protects your current branch from changes."
|
||||
|
||||
Honor any existing declared preference without asking. If the user declines consent, work in place and skip to Step 3.
|
||||
|
||||
## Step 1: Create Isolated Workspace
|
||||
|
||||
**You have two mechanisms. Try them in this order.**
|
||||
|
||||
### 1a. Native Worktree Tools (preferred)
|
||||
|
||||
The user has asked for an isolated workspace (Step 0 consent). Do you already have a way to create a worktree? It might be a tool with a name like `EnterWorktree`, `WorktreeCreate`, a `/worktree` command, or a `--worktree` flag. If you do, use it and skip to Step 3.
|
||||
|
||||
Native tools handle directory placement, branch creation, and cleanup automatically. Using `git worktree add` when you have a native tool creates phantom state your harness can't see or manage.
|
||||
|
||||
Only proceed to Step 1b if you have no native worktree tool available.
|
||||
|
||||
### 1b. Git Worktree Fallback
|
||||
|
||||
**Only use this if Step 1a does not apply** — you have no native worktree tool available. Create a worktree manually using git.
|
||||
|
||||
#### Directory Selection
|
||||
|
||||
Follow this priority order. Explicit user preference always beats observed filesystem state.
|
||||
|
||||
1. **Check your instructions for a declared worktree directory preference.** If the user has already specified one, use it without asking.
|
||||
|
||||
2. **Check for an existing project-local worktree directory:**
|
||||
```bash
|
||||
ls -d .worktrees 2>/dev/null # Preferred (hidden)
|
||||
ls -d worktrees 2>/dev/null # Alternative
|
||||
```
|
||||
If found, use it. If both exist, `.worktrees` wins.
|
||||
|
||||
3. **Check for an existing global directory:**
|
||||
```bash
|
||||
project=$(basename "$(git rev-parse --show-toplevel)")
|
||||
ls -d ~/.config/superpowers/worktrees/$project 2>/dev/null
|
||||
```
|
||||
If found, use it (backward compatibility with legacy global path).
|
||||
|
||||
4. **If there is no other guidance available**, default to `.worktrees/` at the project root.
|
||||
|
||||
#### Safety Verification (project-local directories only)
|
||||
|
||||
**MUST verify directory is ignored before creating worktree:**
|
||||
|
||||
```bash
|
||||
git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null
|
||||
```
|
||||
|
||||
**If NOT ignored:** Add to .gitignore, commit the change, then proceed.
|
||||
|
||||
**Why critical:** Prevents accidentally committing worktree contents to repository.
|
||||
|
||||
Global directories (`~/.config/superpowers/worktrees/`) need no verification.
|
||||
|
||||
#### Create the Worktree
|
||||
|
||||
```bash
|
||||
project=$(basename "$(git rev-parse --show-toplevel)")
|
||||
|
||||
# Determine path based on chosen location
|
||||
# For project-local: path="$LOCATION/$BRANCH_NAME"
|
||||
# For global: path="~/.config/superpowers/worktrees/$project/$BRANCH_NAME"
|
||||
|
||||
git worktree add "$path" -b "$BRANCH_NAME"
|
||||
cd "$path"
|
||||
```
|
||||
|
||||
**Sandbox fallback:** If `git worktree add` fails with a permission error (sandbox denial), tell the user the sandbox blocked worktree creation and you're working in the current directory instead. Then run setup and baseline tests in place.
|
||||
|
||||
## Step 3: Project Setup
|
||||
|
||||
Auto-detect and run appropriate setup:
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
if [ -f package.json ]; then npm install; fi
|
||||
|
||||
# Rust
|
||||
if [ -f Cargo.toml ]; then cargo build; fi
|
||||
|
||||
# Python
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
if [ -f pyproject.toml ]; then poetry install; fi
|
||||
|
||||
# Go
|
||||
if [ -f go.mod ]; then go mod download; fi
|
||||
```
|
||||
|
||||
## Step 4: Verify Clean Baseline
|
||||
|
||||
Run tests to ensure workspace starts clean:
|
||||
|
||||
```bash
|
||||
# Use project-appropriate command
|
||||
npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**If tests fail:** Report failures, ask whether to proceed or investigate.
|
||||
|
||||
**If tests pass:** Report ready.
|
||||
|
||||
### Report
|
||||
|
||||
```
|
||||
Worktree ready at <full-path>
|
||||
Tests passing (<N> tests, 0 failures)
|
||||
Ready to implement <feature-name>
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Already in linked worktree | Skip creation (Step 0) |
|
||||
| In a submodule | Treat as normal repo (Step 0 guard) |
|
||||
| Native worktree tool available | Use it (Step 1a) |
|
||||
| No native tool | Git worktree fallback (Step 1b) |
|
||||
| `.worktrees/` exists | Use it (verify ignored) |
|
||||
| `worktrees/` exists | Use it (verify ignored) |
|
||||
| Both exist | Use `.worktrees/` |
|
||||
| Neither exists | Check instruction file, then default `.worktrees/` |
|
||||
| Global path exists | Use it (backward compat) |
|
||||
| Directory not ignored | Add to .gitignore + commit |
|
||||
| Permission error on create | Sandbox fallback, work in place |
|
||||
| Tests fail during baseline | Report failures + ask |
|
||||
| No package.json/Cargo.toml | Skip dependency install |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Fighting the harness
|
||||
|
||||
- **Problem:** Using `git worktree add` when the platform already provides isolation
|
||||
- **Fix:** Step 0 detects existing isolation. Step 1a defers to native tools.
|
||||
|
||||
### Skipping detection
|
||||
|
||||
- **Problem:** Creating a nested worktree inside an existing one
|
||||
- **Fix:** Always run Step 0 before creating anything
|
||||
|
||||
### Skipping ignore verification
|
||||
|
||||
- **Problem:** Worktree contents get tracked, pollute git status
|
||||
- **Fix:** Always use `git check-ignore` before creating project-local worktree
|
||||
|
||||
### Assuming directory location
|
||||
|
||||
- **Problem:** Creates inconsistency, violates project conventions
|
||||
- **Fix:** Follow priority: existing > global legacy > instruction file > default
|
||||
|
||||
### Proceeding with failing tests
|
||||
|
||||
- **Problem:** Can't distinguish new bugs from pre-existing issues
|
||||
- **Fix:** Report failures, get explicit permission to proceed
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Create a worktree when Step 0 detects existing isolation
|
||||
- Use `git worktree add` when you have a native worktree tool (e.g., `EnterWorktree`). This is the #1 mistake — if you have it, use it.
|
||||
- Skip Step 1a by jumping straight to Step 1b's git commands
|
||||
- Create worktree without verifying it's ignored (project-local)
|
||||
- Skip baseline test verification
|
||||
- Proceed with failing tests without asking
|
||||
|
||||
**Always:**
|
||||
- Run Step 0 detection first
|
||||
- Prefer native tools over git fallback
|
||||
- Follow directory priority: existing > global legacy > instruction file > default
|
||||
- Verify directory is ignored for project-local
|
||||
- Auto-detect and run project setup
|
||||
- Verify clean test baseline
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: using-superpowers
|
||||
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
|
||||
---
|
||||
|
||||
<SUBAGENT-STOP>
|
||||
If you were dispatched as a subagent to execute a specific task, skip this skill.
|
||||
</SUBAGENT-STOP>
|
||||
|
||||
<EXTREMELY-IMPORTANT>
|
||||
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.
|
||||
|
||||
IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
|
||||
|
||||
This is not negotiable. This is not optional. You cannot rationalize your way out of this.
|
||||
</EXTREMELY-IMPORTANT>
|
||||
|
||||
## Instruction Priority
|
||||
|
||||
Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:
|
||||
|
||||
1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
|
||||
2. **Superpowers skills** — override default system behavior where they conflict
|
||||
3. **Default system prompt** — lowest priority
|
||||
|
||||
If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.
|
||||
|
||||
## How to Access Skills
|
||||
|
||||
**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.
|
||||
|
||||
**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool.
|
||||
|
||||
**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.
|
||||
|
||||
**In other environments:** Check your platform's documentation for how skills are loaded.
|
||||
|
||||
## Platform Adaptation
|
||||
|
||||
Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
|
||||
|
||||
# Using Skills
|
||||
|
||||
## The Rule
|
||||
|
||||
**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.
|
||||
|
||||
```dot
|
||||
digraph skill_flow {
|
||||
"User message received" [shape=doublecircle];
|
||||
"About to EnterPlanMode?" [shape=doublecircle];
|
||||
"Already brainstormed?" [shape=diamond];
|
||||
"Invoke brainstorming skill" [shape=box];
|
||||
"Might any skill apply?" [shape=diamond];
|
||||
"Invoke Skill tool" [shape=box];
|
||||
"Announce: 'Using [skill] to [purpose]'" [shape=box];
|
||||
"Has checklist?" [shape=diamond];
|
||||
"Create TodoWrite todo per item" [shape=box];
|
||||
"Follow skill exactly" [shape=box];
|
||||
"Respond (including clarifications)" [shape=doublecircle];
|
||||
|
||||
"About to EnterPlanMode?" -> "Already brainstormed?";
|
||||
"Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
|
||||
"Already brainstormed?" -> "Might any skill apply?" [label="yes"];
|
||||
"Invoke brainstorming skill" -> "Might any skill apply?";
|
||||
|
||||
"User message received" -> "Might any skill apply?";
|
||||
"Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"];
|
||||
"Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
|
||||
"Invoke Skill tool" -> "Announce: 'Using [skill] to [purpose]'";
|
||||
"Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
|
||||
"Has checklist?" -> "Create TodoWrite todo per item" [label="yes"];
|
||||
"Has checklist?" -> "Follow skill exactly" [label="no"];
|
||||
"Create TodoWrite todo per item" -> "Follow skill exactly";
|
||||
}
|
||||
```
|
||||
|
||||
## Red Flags
|
||||
|
||||
These thoughts mean STOP—you're rationalizing:
|
||||
|
||||
| Thought | Reality |
|
||||
|---------|---------|
|
||||
| "This is just a simple question" | Questions are tasks. Check for skills. |
|
||||
| "I need more context first" | Skill check comes BEFORE clarifying questions. |
|
||||
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
|
||||
| "I can check git/files quickly" | Files lack conversation context. Check for skills. |
|
||||
| "Let me gather information first" | Skills tell you HOW to gather information. |
|
||||
| "This doesn't need a formal skill" | If a skill exists, use it. |
|
||||
| "I remember this skill" | Skills evolve. Read current version. |
|
||||
| "This doesn't count as a task" | Action = task. Check for skills. |
|
||||
| "The skill is overkill" | Simple things become complex. Use it. |
|
||||
| "I'll just do this one thing first" | Check BEFORE doing anything. |
|
||||
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
|
||||
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |
|
||||
|
||||
## Skill Priority
|
||||
|
||||
When multiple skills could apply, use this order:
|
||||
|
||||
1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
|
||||
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution
|
||||
|
||||
"Let's build X" → brainstorming first, then implementation skills.
|
||||
"Fix this bug" → debugging first, then domain-specific skills.
|
||||
|
||||
## Skill Types
|
||||
|
||||
**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.
|
||||
|
||||
**Flexible** (patterns): Adapt principles to context.
|
||||
|
||||
The skill itself tells you which.
|
||||
|
||||
## User Instructions
|
||||
|
||||
Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Codex Tool Mapping
|
||||
|
||||
Skills use Claude Code tool names. When you encounter these in a skill, use your platform equivalent:
|
||||
|
||||
| Skill references | Codex equivalent |
|
||||
|-----------------|------------------|
|
||||
| `Task` tool (dispatch subagent) | `spawn_agent` (see [Subagent dispatch requires multi-agent support](#subagent-dispatch-requires-multi-agent-support)) |
|
||||
| Multiple `Task` calls (parallel) | Multiple `spawn_agent` calls |
|
||||
| Task returns result | `wait_agent` |
|
||||
| Task completes automatically | `close_agent` to free slot |
|
||||
| `TodoWrite` (task tracking) | `update_plan` |
|
||||
| `Skill` tool (invoke a skill) | Skills load natively — just follow the instructions |
|
||||
| `Read`, `Write`, `Edit` (files) | Use your native file tools |
|
||||
| `Bash` (run commands) | Use your native shell tools |
|
||||
|
||||
## Subagent dispatch requires multi-agent support
|
||||
|
||||
Add to your Codex config (`~/.codex/config.toml`):
|
||||
|
||||
```toml
|
||||
[features]
|
||||
multi_agent = true
|
||||
```
|
||||
|
||||
This enables `spawn_agent`, `wait_agent`, and `close_agent` for skills like `dispatching-parallel-agents` and `subagent-driven-development`.
|
||||
|
||||
Legacy note: Codex builds before `rust-v0.115.0` exposed spawned-agent
|
||||
waiting as `wait`. Current Codex uses `wait_agent` for spawned agents. The
|
||||
`wait` name now belongs to code-mode `exec/wait`, which resumes a yielded exec
|
||||
cell by `cell_id`; it is not the spawned-agent result tool.
|
||||
|
||||
## Environment Detection
|
||||
|
||||
Skills that create worktrees or finish branches should detect their
|
||||
environment with read-only git commands before proceeding:
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation)
|
||||
- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox)
|
||||
|
||||
See `using-git-worktrees` Step 0 and `finishing-a-development-branch`
|
||||
Step 1 for how each skill uses these signals.
|
||||
|
||||
## Codex App Finishing
|
||||
|
||||
When the sandbox blocks branch/push operations (detached HEAD in an
|
||||
externally managed worktree), the agent commits all work and informs
|
||||
the user to use the App's native controls:
|
||||
|
||||
- **"Create branch"** — names the branch, then commit/push/PR via App UI
|
||||
- **"Hand off to local"** — transfers work to the user's local checkout
|
||||
|
||||
The agent can still run tests, stage files, and output suggested branch
|
||||
names, commit messages, and PR descriptions for the user to copy.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copilot CLI Tool Mapping
|
||||
|
||||
Skills use Claude Code tool names. When you encounter these in a skill, use your platform equivalent:
|
||||
|
||||
| Skill references | Copilot CLI equivalent |
|
||||
|-----------------|----------------------|
|
||||
| `Read` (file reading) | `view` |
|
||||
| `Write` (file creation) | `create` |
|
||||
| `Edit` (file editing) | `edit` |
|
||||
| `Bash` (run commands) | `bash` |
|
||||
| `Grep` (search file content) | `grep` |
|
||||
| `Glob` (search files by name) | `glob` |
|
||||
| `Skill` tool (invoke a skill) | `skill` |
|
||||
| `WebFetch` | `web_fetch` |
|
||||
| `Task` tool (dispatch subagent) | `task` with `agent_type: "general-purpose"` or `"explore"` |
|
||||
| Multiple `Task` calls (parallel) | Multiple `task` calls |
|
||||
| Task status/output | `read_agent`, `list_agents` |
|
||||
| `TodoWrite` (task tracking) | `sql` with built-in `todos` table |
|
||||
| `WebSearch` | No equivalent — use `web_fetch` with a search engine URL |
|
||||
| `EnterPlanMode` / `ExitPlanMode` | No equivalent — stay in the main session |
|
||||
|
||||
## Async shell sessions
|
||||
|
||||
Copilot CLI supports persistent async shell sessions, which have no direct Claude Code equivalent:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `bash` with `async: true` | Start a long-running command in the background |
|
||||
| `write_bash` | Send input to a running async session |
|
||||
| `read_bash` | Read output from an async session |
|
||||
| `stop_bash` | Terminate an async session |
|
||||
| `list_bash` | List all active shell sessions |
|
||||
|
||||
## Additional Copilot CLI tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `store_memory` | Persist facts about the codebase for future sessions |
|
||||
| `report_intent` | Update the UI status line with current intent |
|
||||
| `sql` | Query the session's SQLite database (todos, metadata) |
|
||||
| `fetch_copilot_cli_documentation` | Look up Copilot CLI documentation |
|
||||
| GitHub MCP tools (`github-mcp-server-*`) | Native GitHub API access (issues, PRs, code search) |
|
||||
@@ -0,0 +1,51 @@
|
||||
# Gemini CLI Tool Mapping
|
||||
|
||||
Skills use Claude Code tool names. When you encounter these in a skill, use your platform equivalent:
|
||||
|
||||
| Skill references | Gemini CLI equivalent |
|
||||
|-----------------|----------------------|
|
||||
| `Read` (file reading) | `read_file` |
|
||||
| `Write` (file creation) | `write_file` |
|
||||
| `Edit` (file editing) | `replace` |
|
||||
| `Bash` (run commands) | `run_shell_command` |
|
||||
| `Grep` (search file content) | `grep_search` |
|
||||
| `Glob` (search files by name) | `glob` |
|
||||
| `TodoWrite` (task tracking) | `write_todos` |
|
||||
| `Skill` tool (invoke a skill) | `activate_skill` |
|
||||
| `WebSearch` | `google_web_search` |
|
||||
| `WebFetch` | `web_fetch` |
|
||||
| `Task` tool (dispatch subagent) | `@agent-name` (see [Subagent support](#subagent-support)) |
|
||||
|
||||
## Subagent support
|
||||
|
||||
Gemini CLI supports subagents natively via the `@` syntax. Use the built-in `@generalist` agent to dispatch any task — it has access to all tools and follows the prompt you provide.
|
||||
|
||||
When a skill says to dispatch a named agent type, use `@generalist` with the full prompt from the skill's prompt template:
|
||||
|
||||
| Skill instruction | Gemini CLI equivalent |
|
||||
|-------------------|----------------------|
|
||||
| `Task tool (superpowers:implementer)` | `@generalist` with the filled `implementer-prompt.md` template |
|
||||
| `Task tool (superpowers:spec-reviewer)` | `@generalist` with the filled `spec-reviewer-prompt.md` template |
|
||||
| `Task tool (superpowers:code-reviewer)` | `@code-reviewer` (bundled agent) or `@generalist` with the filled review prompt |
|
||||
| `Task tool (superpowers:code-quality-reviewer)` | `@generalist` with the filled `code-quality-reviewer-prompt.md` template |
|
||||
| `Task tool (general-purpose)` with inline prompt | `@generalist` with your inline prompt |
|
||||
|
||||
### Prompt filling
|
||||
|
||||
Skills provide prompt templates with placeholders like `{WHAT_WAS_IMPLEMENTED}` or `[FULL TEXT of task]`. Fill all placeholders and pass the complete prompt as the message to `@generalist`. The prompt template itself contains the agent's role, review criteria, and expected output format — `@generalist` will follow it.
|
||||
|
||||
### Parallel dispatch
|
||||
|
||||
Gemini CLI supports parallel subagent dispatch. When a skill asks you to dispatch multiple independent subagent tasks in parallel, request all of those `@generalist` or named subagent tasks together in the same prompt. Keep dependent tasks sequential, but do not serialize independent subagent tasks just to preserve a simpler history.
|
||||
|
||||
## Additional Gemini CLI tools
|
||||
|
||||
These tools are available in Gemini CLI but have no Claude Code equivalent:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `list_directory` | List files and subdirectories |
|
||||
| `save_memory` | Persist facts to GEMINI.md across sessions |
|
||||
| `ask_user` | Request structured input from the user |
|
||||
| `tracker_create_task` | Rich task management (create, update, list, visualize) |
|
||||
| `enter_plan_mode` / `exit_plan_mode` | Switch to read-only research mode before making changes |
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: verification-before-completion
|
||||
description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
|
||||
---
|
||||
|
||||
# Verification Before Completion
|
||||
|
||||
## Overview
|
||||
|
||||
Claiming work is complete without verification is dishonesty, not efficiency.
|
||||
|
||||
**Core principle:** Evidence before claims, always.
|
||||
|
||||
**Violating the letter of this rule is violating the spirit of this rule.**
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE
|
||||
```
|
||||
|
||||
If you haven't run the verification command in this message, you cannot claim it passes.
|
||||
|
||||
## The Gate Function
|
||||
|
||||
```
|
||||
BEFORE claiming any status or expressing satisfaction:
|
||||
|
||||
1. IDENTIFY: What command proves this claim?
|
||||
2. RUN: Execute the FULL command (fresh, complete)
|
||||
3. READ: Full output, check exit code, count failures
|
||||
4. VERIFY: Does output confirm the claim?
|
||||
- If NO: State actual status with evidence
|
||||
- If YES: State claim WITH evidence
|
||||
5. ONLY THEN: Make the claim
|
||||
|
||||
Skip any step = lying, not verifying
|
||||
```
|
||||
|
||||
## Common Failures
|
||||
|
||||
| Claim | Requires | Not Sufficient |
|
||||
|-------|----------|----------------|
|
||||
| Tests pass | Test command output: 0 failures | Previous run, "should pass" |
|
||||
| Linter clean | Linter output: 0 errors | Partial check, extrapolation |
|
||||
| Build succeeds | Build command: exit 0 | Linter passing, logs look good |
|
||||
| Bug fixed | Test original symptom: passes | Code changed, assumed fixed |
|
||||
| Regression test works | Red-green cycle verified | Test passes once |
|
||||
| Agent completed | VCS diff shows changes | Agent reports "success" |
|
||||
| Requirements met | Line-by-line checklist | Tests passing |
|
||||
|
||||
## Red Flags - STOP
|
||||
|
||||
- Using "should", "probably", "seems to"
|
||||
- Expressing satisfaction before verification ("Great!", "Perfect!", "Done!", etc.)
|
||||
- About to commit/push/PR without verification
|
||||
- Trusting agent success reports
|
||||
- Relying on partial verification
|
||||
- Thinking "just this once"
|
||||
- Tired and wanting work over
|
||||
- **ANY wording implying success without having run verification**
|
||||
|
||||
## Rationalization Prevention
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Should work now" | RUN the verification |
|
||||
| "I'm confident" | Confidence ≠ evidence |
|
||||
| "Just this once" | No exceptions |
|
||||
| "Linter passed" | Linter ≠ compiler |
|
||||
| "Agent said success" | Verify independently |
|
||||
| "I'm tired" | Exhaustion ≠ excuse |
|
||||
| "Partial check is enough" | Partial proves nothing |
|
||||
| "Different words so rule doesn't apply" | Spirit over letter |
|
||||
|
||||
## Key Patterns
|
||||
|
||||
**Tests:**
|
||||
```
|
||||
✅ [Run test command] [See: 34/34 pass] "All tests pass"
|
||||
❌ "Should pass now" / "Looks correct"
|
||||
```
|
||||
|
||||
**Regression tests (TDD Red-Green):**
|
||||
```
|
||||
✅ Write → Run (pass) → Revert fix → Run (MUST FAIL) → Restore → Run (pass)
|
||||
❌ "I've written a regression test" (without red-green verification)
|
||||
```
|
||||
|
||||
**Build:**
|
||||
```
|
||||
✅ [Run build] [See: exit 0] "Build passes"
|
||||
❌ "Linter passed" (linter doesn't check compilation)
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
```
|
||||
✅ Re-read plan → Create checklist → Verify each → Report gaps or completion
|
||||
❌ "Tests pass, phase complete"
|
||||
```
|
||||
|
||||
**Agent delegation:**
|
||||
```
|
||||
✅ Agent reports success → Check VCS diff → Verify changes → Report actual state
|
||||
❌ Trust agent report
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
From 24 failure memories:
|
||||
- your human partner said "I don't believe you" - trust broken
|
||||
- Undefined functions shipped - would crash
|
||||
- Missing requirements shipped - incomplete features
|
||||
- Time wasted on false completion → redirect → rework
|
||||
- Violates: "Honesty is a core value. If you lie, you'll be replaced."
|
||||
|
||||
## When To Apply
|
||||
|
||||
**ALWAYS before:**
|
||||
- ANY variation of success/completion claims
|
||||
- ANY expression of satisfaction
|
||||
- ANY positive statement about work state
|
||||
- Committing, PR creation, task completion
|
||||
- Moving to next task
|
||||
- Delegating to agents
|
||||
|
||||
**Rule applies to:**
|
||||
- Exact phrases
|
||||
- Paraphrases and synonyms
|
||||
- Implications of success
|
||||
- ANY communication suggesting completion/correctness
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**No shortcuts for verification.**
|
||||
|
||||
Run the command. Read the output. THEN claim the result.
|
||||
|
||||
This is non-negotiable.
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: writing-plans
|
||||
description: Use when you have a spec or requirements for a multi-step task, before touching code
|
||||
---
|
||||
|
||||
# Writing Plans
|
||||
|
||||
## Overview
|
||||
|
||||
Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
||||
|
||||
Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know good test design very well.
|
||||
|
||||
**Announce at start:** "I'm using the writing-plans skill to create the implementation plan."
|
||||
|
||||
**Context:** If working in an isolated worktree, it should have been created via the `superpowers:using-git-worktrees` skill at execution time.
|
||||
|
||||
**Save plans to:** `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
|
||||
- (User preferences for plan location override this default)
|
||||
|
||||
## Scope Check
|
||||
|
||||
If the spec covers multiple independent subsystems, it should have been broken into sub-project specs during brainstorming. If it wasn't, suggest breaking this into separate plans — one per subsystem. Each plan should produce working, testable software on its own.
|
||||
|
||||
## File Structure
|
||||
|
||||
Before defining tasks, map out which files will be created or modified and what each one is responsible for. This is where decomposition decisions get locked in.
|
||||
|
||||
- Design units with clear boundaries and well-defined interfaces. Each file should have one clear responsibility.
|
||||
- You reason best about code you can hold in context at once, and your edits are more reliable when files are focused. Prefer smaller, focused files over large ones that do too much.
|
||||
- Files that change together should live together. Split by responsibility, not by technical layer.
|
||||
- In existing codebases, follow established patterns. If the codebase uses large files, don't unilaterally restructure - but if a file you're modifying has grown unwieldy, including a split in the plan is reasonable.
|
||||
|
||||
This structure informs the task decomposition. Each task should produce self-contained changes that make sense independently.
|
||||
|
||||
## Bite-Sized Task Granularity
|
||||
|
||||
**Each step is one action (2-5 minutes):**
|
||||
- "Write the failing test" - step
|
||||
- "Run it to make sure it fails" - step
|
||||
- "Implement the minimal code to make the test pass" - step
|
||||
- "Run the tests and make sure they pass" - step
|
||||
- "Commit" - step
|
||||
|
||||
## Plan Document Header
|
||||
|
||||
**Every plan MUST start with this header:**
|
||||
|
||||
```markdown
|
||||
# [Feature Name] Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** [One sentence describing what this builds]
|
||||
|
||||
**Architecture:** [2-3 sentences about approach]
|
||||
|
||||
**Tech Stack:** [Key technologies/libraries]
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Task Structure
|
||||
|
||||
````markdown
|
||||
### Task N: [Component Name]
|
||||
|
||||
**Files:**
|
||||
- Create: `exact/path/to/file.py`
|
||||
- Modify: `exact/path/to/existing.py:123-145`
|
||||
- Test: `tests/exact/path/to/test.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
def test_specific_behavior():
|
||||
result = function(input)
|
||||
assert result == expected
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/path/test.py::test_name -v`
|
||||
Expected: FAIL with "function not defined"
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def function(input):
|
||||
return expected
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/path/test.py::test_name -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/path/test.py src/path/file.py
|
||||
git commit -m "feat: add specific feature"
|
||||
```
|
||||
````
|
||||
|
||||
## No Placeholders
|
||||
|
||||
Every step must contain the actual content an engineer needs. These are **plan failures** — never write them:
|
||||
- "TBD", "TODO", "implement later", "fill in details"
|
||||
- "Add appropriate error handling" / "add validation" / "handle edge cases"
|
||||
- "Write tests for the above" (without actual test code)
|
||||
- "Similar to Task N" (repeat the code — the engineer may be reading tasks out of order)
|
||||
- Steps that describe what to do without showing how (code blocks required for code steps)
|
||||
- References to types, functions, or methods not defined in any task
|
||||
|
||||
## Remember
|
||||
- Exact file paths always
|
||||
- Complete code in every step — if a step changes code, show the code
|
||||
- Exact commands with expected output
|
||||
- DRY, YAGNI, TDD, frequent commits
|
||||
|
||||
## Self-Review
|
||||
|
||||
After writing the complete plan, look at the spec with fresh eyes and check the plan against it. This is a checklist you run yourself — not a subagent dispatch.
|
||||
|
||||
**1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps.
|
||||
|
||||
**2. Placeholder scan:** Search your plan for red flags — any of the patterns from the "No Placeholders" section above. Fix them.
|
||||
|
||||
**3. Type consistency:** Do the types, method signatures, and property names you used in later tasks match what you defined in earlier tasks? A function called `clearLayers()` in Task 3 but `clearFullLayers()` in Task 7 is a bug.
|
||||
|
||||
If you find issues, fix them inline. No need to re-review — just fix and move on. If you find a spec requirement with no task, add the task.
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
After saving the plan, offer execution choice:
|
||||
|
||||
**"Plan complete and saved to `docs/superpowers/plans/<filename>.md`. Two execution options:**
|
||||
|
||||
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||||
|
||||
**Which approach?"**
|
||||
|
||||
**If Subagent-Driven chosen:**
|
||||
- **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development
|
||||
- Fresh subagent per task + two-stage review
|
||||
|
||||
**If Inline Execution chosen:**
|
||||
- **REQUIRED SUB-SKILL:** Use superpowers:executing-plans
|
||||
- Batch execution with checkpoints for review
|
||||
@@ -0,0 +1,49 @@
|
||||
# Plan Document Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a plan document reviewer subagent.
|
||||
|
||||
**Purpose:** Verify the plan is complete, matches the spec, and has proper task decomposition.
|
||||
|
||||
**Dispatch after:** The complete plan is written.
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review plan document"
|
||||
prompt: |
|
||||
You are a plan document reviewer. Verify this plan is complete and ready for implementation.
|
||||
|
||||
**Plan to review:** [PLAN_FILE_PATH]
|
||||
**Spec for reference:** [SPEC_FILE_PATH]
|
||||
|
||||
## What to Check
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, incomplete tasks, missing steps |
|
||||
| Spec Alignment | Plan covers spec requirements, no major scope creep |
|
||||
| Task Decomposition | Tasks have clear boundaries, steps are actionable |
|
||||
| Buildability | Could an engineer follow this plan without getting stuck? |
|
||||
|
||||
## Calibration
|
||||
|
||||
**Only flag issues that would cause real problems during implementation.**
|
||||
An implementer building the wrong thing or getting stuck is an issue.
|
||||
Minor wording, stylistic preferences, and "nice to have" suggestions are not.
|
||||
|
||||
Approve unless there are serious gaps — missing requirements from the spec,
|
||||
contradictory steps, placeholder content, or tasks so vague they can't be acted on.
|
||||
|
||||
## Output Format
|
||||
|
||||
## Plan Review
|
||||
|
||||
**Status:** Approved | Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Task X, Step Y]: [specific issue] - [why it matters for implementation]
|
||||
|
||||
**Recommendations (advisory, do not block approval):**
|
||||
- [suggestions for improvement]
|
||||
```
|
||||
|
||||
**Reviewer returns:** Status, Issues (if any), Recommendations
|
||||
@@ -0,0 +1,655 @@
|
||||
---
|
||||
name: writing-skills
|
||||
description: Use when creating new skills, editing existing skills, or verifying skills work before deployment
|
||||
---
|
||||
|
||||
# Writing Skills
|
||||
|
||||
## Overview
|
||||
|
||||
**Writing skills IS Test-Driven Development applied to process documentation.**
|
||||
|
||||
**Personal skills live in agent-specific directories (`~/.claude/skills` for Claude Code, `~/.agents/skills/` for Codex)**
|
||||
|
||||
You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes).
|
||||
|
||||
**Core principle:** If you didn't watch an agent fail without the skill, you don't know if the skill teaches the right thing.
|
||||
|
||||
**REQUIRED BACKGROUND:** You MUST understand superpowers:test-driven-development before using this skill. That skill defines the fundamental RED-GREEN-REFACTOR cycle. This skill adapts TDD to documentation.
|
||||
|
||||
**Official guidance:** For Anthropic's official skill authoring best practices, see anthropic-best-practices.md. This document provides additional patterns and guidelines that complement the TDD-focused approach in this skill.
|
||||
|
||||
## What is a Skill?
|
||||
|
||||
A **skill** is a reference guide for proven techniques, patterns, or tools. Skills help future Claude instances find and apply effective approaches.
|
||||
|
||||
**Skills are:** Reusable techniques, patterns, tools, reference guides
|
||||
|
||||
**Skills are NOT:** Narratives about how you solved a problem once
|
||||
|
||||
## TDD Mapping for Skills
|
||||
|
||||
| TDD Concept | Skill Creation |
|
||||
|-------------|----------------|
|
||||
| **Test case** | Pressure scenario with subagent |
|
||||
| **Production code** | Skill document (SKILL.md) |
|
||||
| **Test fails (RED)** | Agent violates rule without skill (baseline) |
|
||||
| **Test passes (GREEN)** | Agent complies with skill present |
|
||||
| **Refactor** | Close loopholes while maintaining compliance |
|
||||
| **Write test first** | Run baseline scenario BEFORE writing skill |
|
||||
| **Watch it fail** | Document exact rationalizations agent uses |
|
||||
| **Minimal code** | Write skill addressing those specific violations |
|
||||
| **Watch it pass** | Verify agent now complies |
|
||||
| **Refactor cycle** | Find new rationalizations → plug → re-verify |
|
||||
|
||||
The entire skill creation process follows RED-GREEN-REFACTOR.
|
||||
|
||||
## When to Create a Skill
|
||||
|
||||
**Create when:**
|
||||
- Technique wasn't intuitively obvious to you
|
||||
- You'd reference this again across projects
|
||||
- Pattern applies broadly (not project-specific)
|
||||
- Others would benefit
|
||||
|
||||
**Don't create for:**
|
||||
- One-off solutions
|
||||
- Standard practices well-documented elsewhere
|
||||
- Project-specific conventions (put in CLAUDE.md)
|
||||
- Mechanical constraints (if it's enforceable with regex/validation, automate it—save documentation for judgment calls)
|
||||
|
||||
## Skill Types
|
||||
|
||||
### Technique
|
||||
Concrete method with steps to follow (condition-based-waiting, root-cause-tracing)
|
||||
|
||||
### Pattern
|
||||
Way of thinking about problems (flatten-with-flags, test-invariants)
|
||||
|
||||
### Reference
|
||||
API docs, syntax guides, tool documentation (office docs)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
|
||||
```
|
||||
skills/
|
||||
skill-name/
|
||||
SKILL.md # Main reference (required)
|
||||
supporting-file.* # Only if needed
|
||||
```
|
||||
|
||||
**Flat namespace** - all skills in one searchable namespace
|
||||
|
||||
**Separate files for:**
|
||||
1. **Heavy reference** (100+ lines) - API docs, comprehensive syntax
|
||||
2. **Reusable tools** - Scripts, utilities, templates
|
||||
|
||||
**Keep inline:**
|
||||
- Principles and concepts
|
||||
- Code patterns (< 50 lines)
|
||||
- Everything else
|
||||
|
||||
## SKILL.md Structure
|
||||
|
||||
**Frontmatter (YAML):**
|
||||
- Two required fields: `name` and `description` (see [agentskills.io/specification](https://agentskills.io/specification) for all supported fields)
|
||||
- Max 1024 characters total
|
||||
- `name`: Use letters, numbers, and hyphens only (no parentheses, special chars)
|
||||
- `description`: Third-person, describes ONLY when to use (NOT what it does)
|
||||
- Start with "Use when..." to focus on triggering conditions
|
||||
- Include specific symptoms, situations, and contexts
|
||||
- **NEVER summarize the skill's process or workflow** (see CSO section for why)
|
||||
- Keep under 500 characters if possible
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: Skill-Name-With-Hyphens
|
||||
description: Use when [specific triggering conditions and symptoms]
|
||||
---
|
||||
|
||||
# Skill Name
|
||||
|
||||
## Overview
|
||||
What is this? Core principle in 1-2 sentences.
|
||||
|
||||
## When to Use
|
||||
[Small inline flowchart IF decision non-obvious]
|
||||
|
||||
Bullet list with SYMPTOMS and use cases
|
||||
When NOT to use
|
||||
|
||||
## Core Pattern (for techniques/patterns)
|
||||
Before/after code comparison
|
||||
|
||||
## Quick Reference
|
||||
Table or bullets for scanning common operations
|
||||
|
||||
## Implementation
|
||||
Inline code for simple patterns
|
||||
Link to file for heavy reference or reusable tools
|
||||
|
||||
## Common Mistakes
|
||||
What goes wrong + fixes
|
||||
|
||||
## Real-World Impact (optional)
|
||||
Concrete results
|
||||
```
|
||||
|
||||
|
||||
## Claude Search Optimization (CSO)
|
||||
|
||||
**Critical for discovery:** Future Claude needs to FIND your skill
|
||||
|
||||
### 1. Rich Description Field
|
||||
|
||||
**Purpose:** Claude reads description to decide which skills to load for a given task. Make it answer: "Should I read this skill right now?"
|
||||
|
||||
**Format:** Start with "Use when..." to focus on triggering conditions
|
||||
|
||||
**CRITICAL: Description = When to Use, NOT What the Skill Does**
|
||||
|
||||
The description should ONLY describe triggering conditions. Do NOT summarize the skill's process or workflow in the description.
|
||||
|
||||
**Why this matters:** Testing revealed that when a description summarizes the skill's workflow, Claude may follow the description instead of reading the full skill content. A description saying "code review between tasks" caused Claude to do ONE review, even though the skill's flowchart clearly showed TWO reviews (spec compliance then code quality).
|
||||
|
||||
When the description was changed to just "Use when executing implementation plans with independent tasks" (no workflow summary), Claude correctly read the flowchart and followed the two-stage review process.
|
||||
|
||||
**The trap:** Descriptions that summarize workflow create a shortcut Claude will take. The skill body becomes documentation Claude skips.
|
||||
|
||||
```yaml
|
||||
# ❌ BAD: Summarizes workflow - Claude may follow this instead of reading skill
|
||||
description: Use when executing plans - dispatches subagent per task with code review between tasks
|
||||
|
||||
# ❌ BAD: Too much process detail
|
||||
description: Use for TDD - write test first, watch it fail, write minimal code, refactor
|
||||
|
||||
# ✅ GOOD: Just triggering conditions, no workflow summary
|
||||
description: Use when executing implementation plans with independent tasks in the current session
|
||||
|
||||
# ✅ GOOD: Triggering conditions only
|
||||
description: Use when implementing any feature or bugfix, before writing implementation code
|
||||
```
|
||||
|
||||
**Content:**
|
||||
- Use concrete triggers, symptoms, and situations that signal this skill applies
|
||||
- Describe the *problem* (race conditions, inconsistent behavior) not *language-specific symptoms* (setTimeout, sleep)
|
||||
- Keep triggers technology-agnostic unless the skill itself is technology-specific
|
||||
- If skill is technology-specific, make that explicit in the trigger
|
||||
- Write in third person (injected into system prompt)
|
||||
- **NEVER summarize the skill's process or workflow**
|
||||
|
||||
```yaml
|
||||
# ❌ BAD: Too abstract, vague, doesn't include when to use
|
||||
description: For async testing
|
||||
|
||||
# ❌ BAD: First person
|
||||
description: I can help you with async tests when they're flaky
|
||||
|
||||
# ❌ BAD: Mentions technology but skill isn't specific to it
|
||||
description: Use when tests use setTimeout/sleep and are flaky
|
||||
|
||||
# ✅ GOOD: Starts with "Use when", describes problem, no workflow
|
||||
description: Use when tests have race conditions, timing dependencies, or pass/fail inconsistently
|
||||
|
||||
# ✅ GOOD: Technology-specific skill with explicit trigger
|
||||
description: Use when using React Router and handling authentication redirects
|
||||
```
|
||||
|
||||
### 2. Keyword Coverage
|
||||
|
||||
Use words Claude would search for:
|
||||
- Error messages: "Hook timed out", "ENOTEMPTY", "race condition"
|
||||
- Symptoms: "flaky", "hanging", "zombie", "pollution"
|
||||
- Synonyms: "timeout/hang/freeze", "cleanup/teardown/afterEach"
|
||||
- Tools: Actual commands, library names, file types
|
||||
|
||||
### 3. Descriptive Naming
|
||||
|
||||
**Use active voice, verb-first:**
|
||||
- ✅ `creating-skills` not `skill-creation`
|
||||
- ✅ `condition-based-waiting` not `async-test-helpers`
|
||||
|
||||
### 4. Token Efficiency (Critical)
|
||||
|
||||
**Problem:** getting-started and frequently-referenced skills load into EVERY conversation. Every token counts.
|
||||
|
||||
**Target word counts:**
|
||||
- getting-started workflows: <150 words each
|
||||
- Frequently-loaded skills: <200 words total
|
||||
- Other skills: <500 words (still be concise)
|
||||
|
||||
**Techniques:**
|
||||
|
||||
**Move details to tool help:**
|
||||
```bash
|
||||
# ❌ BAD: Document all flags in SKILL.md
|
||||
search-conversations supports --text, --both, --after DATE, --before DATE, --limit N
|
||||
|
||||
# ✅ GOOD: Reference --help
|
||||
search-conversations supports multiple modes and filters. Run --help for details.
|
||||
```
|
||||
|
||||
**Use cross-references:**
|
||||
```markdown
|
||||
# ❌ BAD: Repeat workflow details
|
||||
When searching, dispatch subagent with template...
|
||||
[20 lines of repeated instructions]
|
||||
|
||||
# ✅ GOOD: Reference other skill
|
||||
Always use subagents (50-100x context savings). REQUIRED: Use [other-skill-name] for workflow.
|
||||
```
|
||||
|
||||
**Compress examples:**
|
||||
```markdown
|
||||
# ❌ BAD: Verbose example (42 words)
|
||||
your human partner: "How did we handle authentication errors in React Router before?"
|
||||
You: I'll search past conversations for React Router authentication patterns.
|
||||
[Dispatch subagent with search query: "React Router authentication error handling 401"]
|
||||
|
||||
# ✅ GOOD: Minimal example (20 words)
|
||||
Partner: "How did we handle auth errors in React Router?"
|
||||
You: Searching...
|
||||
[Dispatch subagent → synthesis]
|
||||
```
|
||||
|
||||
**Eliminate redundancy:**
|
||||
- Don't repeat what's in cross-referenced skills
|
||||
- Don't explain what's obvious from command
|
||||
- Don't include multiple examples of same pattern
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
wc -w skills/path/SKILL.md
|
||||
# getting-started workflows: aim for <150 each
|
||||
# Other frequently-loaded: aim for <200 total
|
||||
```
|
||||
|
||||
**Name by what you DO or core insight:**
|
||||
- ✅ `condition-based-waiting` > `async-test-helpers`
|
||||
- ✅ `using-skills` not `skill-usage`
|
||||
- ✅ `flatten-with-flags` > `data-structure-refactoring`
|
||||
- ✅ `root-cause-tracing` > `debugging-techniques`
|
||||
|
||||
**Gerunds (-ing) work well for processes:**
|
||||
- `creating-skills`, `testing-skills`, `debugging-with-logs`
|
||||
- Active, describes the action you're taking
|
||||
|
||||
### 4. Cross-Referencing Other Skills
|
||||
|
||||
**When writing documentation that references other skills:**
|
||||
|
||||
Use skill name only, with explicit requirement markers:
|
||||
- ✅ Good: `**REQUIRED SUB-SKILL:** Use superpowers:test-driven-development`
|
||||
- ✅ Good: `**REQUIRED BACKGROUND:** You MUST understand superpowers:systematic-debugging`
|
||||
- ❌ Bad: `See skills/testing/test-driven-development` (unclear if required)
|
||||
- ❌ Bad: `@skills/testing/test-driven-development/SKILL.md` (force-loads, burns context)
|
||||
|
||||
**Why no @ links:** `@` syntax force-loads files immediately, consuming 200k+ context before you need them.
|
||||
|
||||
## Flowchart Usage
|
||||
|
||||
```dot
|
||||
digraph when_flowchart {
|
||||
"Need to show information?" [shape=diamond];
|
||||
"Decision where I might go wrong?" [shape=diamond];
|
||||
"Use markdown" [shape=box];
|
||||
"Small inline flowchart" [shape=box];
|
||||
|
||||
"Need to show information?" -> "Decision where I might go wrong?" [label="yes"];
|
||||
"Decision where I might go wrong?" -> "Small inline flowchart" [label="yes"];
|
||||
"Decision where I might go wrong?" -> "Use markdown" [label="no"];
|
||||
}
|
||||
```
|
||||
|
||||
**Use flowcharts ONLY for:**
|
||||
- Non-obvious decision points
|
||||
- Process loops where you might stop too early
|
||||
- "When to use A vs B" decisions
|
||||
|
||||
**Never use flowcharts for:**
|
||||
- Reference material → Tables, lists
|
||||
- Code examples → Markdown blocks
|
||||
- Linear instructions → Numbered lists
|
||||
- Labels without semantic meaning (step1, helper2)
|
||||
|
||||
See @graphviz-conventions.dot for graphviz style rules.
|
||||
|
||||
**Visualizing for your human partner:** Use `render-graphs.js` in this directory to render a skill's flowcharts to SVG:
|
||||
```bash
|
||||
./render-graphs.js ../some-skill # Each diagram separately
|
||||
./render-graphs.js ../some-skill --combine # All diagrams in one SVG
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
**One excellent example beats many mediocre ones**
|
||||
|
||||
Choose most relevant language:
|
||||
- Testing techniques → TypeScript/JavaScript
|
||||
- System debugging → Shell/Python
|
||||
- Data processing → Python
|
||||
|
||||
**Good example:**
|
||||
- Complete and runnable
|
||||
- Well-commented explaining WHY
|
||||
- From real scenario
|
||||
- Shows pattern clearly
|
||||
- Ready to adapt (not generic template)
|
||||
|
||||
**Don't:**
|
||||
- Implement in 5+ languages
|
||||
- Create fill-in-the-blank templates
|
||||
- Write contrived examples
|
||||
|
||||
You're good at porting - one great example is enough.
|
||||
|
||||
## File Organization
|
||||
|
||||
### Self-Contained Skill
|
||||
```
|
||||
defense-in-depth/
|
||||
SKILL.md # Everything inline
|
||||
```
|
||||
When: All content fits, no heavy reference needed
|
||||
|
||||
### Skill with Reusable Tool
|
||||
```
|
||||
condition-based-waiting/
|
||||
SKILL.md # Overview + patterns
|
||||
example.ts # Working helpers to adapt
|
||||
```
|
||||
When: Tool is reusable code, not just narrative
|
||||
|
||||
### Skill with Heavy Reference
|
||||
```
|
||||
pptx/
|
||||
SKILL.md # Overview + workflows
|
||||
pptxgenjs.md # 600 lines API reference
|
||||
ooxml.md # 500 lines XML structure
|
||||
scripts/ # Executable tools
|
||||
```
|
||||
When: Reference material too large for inline
|
||||
|
||||
## The Iron Law (Same as TDD)
|
||||
|
||||
```
|
||||
NO SKILL WITHOUT A FAILING TEST FIRST
|
||||
```
|
||||
|
||||
This applies to NEW skills AND EDITS to existing skills.
|
||||
|
||||
Write skill before testing? Delete it. Start over.
|
||||
Edit skill without testing? Same violation.
|
||||
|
||||
**No exceptions:**
|
||||
- Not for "simple additions"
|
||||
- Not for "just adding a section"
|
||||
- Not for "documentation updates"
|
||||
- Don't keep untested changes as "reference"
|
||||
- Don't "adapt" while running tests
|
||||
- Delete means delete
|
||||
|
||||
**REQUIRED BACKGROUND:** The superpowers:test-driven-development skill explains why this matters. Same principles apply to documentation.
|
||||
|
||||
## Testing All Skill Types
|
||||
|
||||
Different skill types need different test approaches:
|
||||
|
||||
### Discipline-Enforcing Skills (rules/requirements)
|
||||
|
||||
**Examples:** TDD, verification-before-completion, designing-before-coding
|
||||
|
||||
**Test with:**
|
||||
- Academic questions: Do they understand the rules?
|
||||
- Pressure scenarios: Do they comply under stress?
|
||||
- Multiple pressures combined: time + sunk cost + exhaustion
|
||||
- Identify rationalizations and add explicit counters
|
||||
|
||||
**Success criteria:** Agent follows rule under maximum pressure
|
||||
|
||||
### Technique Skills (how-to guides)
|
||||
|
||||
**Examples:** condition-based-waiting, root-cause-tracing, defensive-programming
|
||||
|
||||
**Test with:**
|
||||
- Application scenarios: Can they apply the technique correctly?
|
||||
- Variation scenarios: Do they handle edge cases?
|
||||
- Missing information tests: Do instructions have gaps?
|
||||
|
||||
**Success criteria:** Agent successfully applies technique to new scenario
|
||||
|
||||
### Pattern Skills (mental models)
|
||||
|
||||
**Examples:** reducing-complexity, information-hiding concepts
|
||||
|
||||
**Test with:**
|
||||
- Recognition scenarios: Do they recognize when pattern applies?
|
||||
- Application scenarios: Can they use the mental model?
|
||||
- Counter-examples: Do they know when NOT to apply?
|
||||
|
||||
**Success criteria:** Agent correctly identifies when/how to apply pattern
|
||||
|
||||
### Reference Skills (documentation/APIs)
|
||||
|
||||
**Examples:** API documentation, command references, library guides
|
||||
|
||||
**Test with:**
|
||||
- Retrieval scenarios: Can they find the right information?
|
||||
- Application scenarios: Can they use what they found correctly?
|
||||
- Gap testing: Are common use cases covered?
|
||||
|
||||
**Success criteria:** Agent finds and correctly applies reference information
|
||||
|
||||
## Common Rationalizations for Skipping Testing
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Skill is obviously clear" | Clear to you ≠ clear to other agents. Test it. |
|
||||
| "It's just a reference" | References can have gaps, unclear sections. Test retrieval. |
|
||||
| "Testing is overkill" | Untested skills have issues. Always. 15 min testing saves hours. |
|
||||
| "I'll test if problems emerge" | Problems = agents can't use skill. Test BEFORE deploying. |
|
||||
| "Too tedious to test" | Testing is less tedious than debugging bad skill in production. |
|
||||
| "I'm confident it's good" | Overconfidence guarantees issues. Test anyway. |
|
||||
| "Academic review is enough" | Reading ≠ using. Test application scenarios. |
|
||||
| "No time to test" | Deploying untested skill wastes more time fixing it later. |
|
||||
|
||||
**All of these mean: Test before deploying. No exceptions.**
|
||||
|
||||
## Bulletproofing Skills Against Rationalization
|
||||
|
||||
Skills that enforce discipline (like TDD) need to resist rationalization. Agents are smart and will find loopholes when under pressure.
|
||||
|
||||
**Psychology note:** Understanding WHY persuasion techniques work helps you apply them systematically. See persuasion-principles.md for research foundation (Cialdini, 2021; Meincke et al., 2025) on authority, commitment, scarcity, social proof, and unity principles.
|
||||
|
||||
### Close Every Loophole Explicitly
|
||||
|
||||
Don't just state the rule - forbid specific workarounds:
|
||||
|
||||
<Bad>
|
||||
```markdown
|
||||
Write code before test? Delete it.
|
||||
```
|
||||
</Bad>
|
||||
|
||||
<Good>
|
||||
```markdown
|
||||
Write code before test? Delete it. Start over.
|
||||
|
||||
**No exceptions:**
|
||||
- Don't keep it as "reference"
|
||||
- Don't "adapt" it while writing tests
|
||||
- Don't look at it
|
||||
- Delete means delete
|
||||
```
|
||||
</Good>
|
||||
|
||||
### Address "Spirit vs Letter" Arguments
|
||||
|
||||
Add foundational principle early:
|
||||
|
||||
```markdown
|
||||
**Violating the letter of the rules is violating the spirit of the rules.**
|
||||
```
|
||||
|
||||
This cuts off entire class of "I'm following the spirit" rationalizations.
|
||||
|
||||
### Build Rationalization Table
|
||||
|
||||
Capture rationalizations from baseline testing (see Testing section below). Every excuse agents make goes in the table:
|
||||
|
||||
```markdown
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
|
||||
| "I'll test after" | Tests passing immediately prove nothing. |
|
||||
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
|
||||
```
|
||||
|
||||
### Create Red Flags List
|
||||
|
||||
Make it easy for agents to self-check when rationalizing:
|
||||
|
||||
```markdown
|
||||
## Red Flags - STOP and Start Over
|
||||
|
||||
- Code before test
|
||||
- "I already manually tested it"
|
||||
- "Tests after achieve the same purpose"
|
||||
- "It's about spirit not ritual"
|
||||
- "This is different because..."
|
||||
|
||||
**All of these mean: Delete code. Start over with TDD.**
|
||||
```
|
||||
|
||||
### Update CSO for Violation Symptoms
|
||||
|
||||
Add to description: symptoms of when you're ABOUT to violate the rule:
|
||||
|
||||
```yaml
|
||||
description: use when implementing any feature or bugfix, before writing implementation code
|
||||
```
|
||||
|
||||
## RED-GREEN-REFACTOR for Skills
|
||||
|
||||
Follow the TDD cycle:
|
||||
|
||||
### RED: Write Failing Test (Baseline)
|
||||
|
||||
Run pressure scenario with subagent WITHOUT the skill. Document exact behavior:
|
||||
- What choices did they make?
|
||||
- What rationalizations did they use (verbatim)?
|
||||
- Which pressures triggered violations?
|
||||
|
||||
This is "watch the test fail" - you must see what agents naturally do before writing the skill.
|
||||
|
||||
### GREEN: Write Minimal Skill
|
||||
|
||||
Write skill that addresses those specific rationalizations. Don't add extra content for hypothetical cases.
|
||||
|
||||
Run same scenarios WITH skill. Agent should now comply.
|
||||
|
||||
### REFACTOR: Close Loopholes
|
||||
|
||||
Agent found new rationalization? Add explicit counter. Re-test until bulletproof.
|
||||
|
||||
**Testing methodology:** See @testing-skills-with-subagents.md for the complete testing methodology:
|
||||
- How to write pressure scenarios
|
||||
- Pressure types (time, sunk cost, authority, exhaustion)
|
||||
- Plugging holes systematically
|
||||
- Meta-testing techniques
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### ❌ Narrative Example
|
||||
"In session 2025-10-03, we found empty projectDir caused..."
|
||||
**Why bad:** Too specific, not reusable
|
||||
|
||||
### ❌ Multi-Language Dilution
|
||||
example-js.js, example-py.py, example-go.go
|
||||
**Why bad:** Mediocre quality, maintenance burden
|
||||
|
||||
### ❌ Code in Flowcharts
|
||||
```dot
|
||||
step1 [label="import fs"];
|
||||
step2 [label="read file"];
|
||||
```
|
||||
**Why bad:** Can't copy-paste, hard to read
|
||||
|
||||
### ❌ Generic Labels
|
||||
helper1, helper2, step3, pattern4
|
||||
**Why bad:** Labels should have semantic meaning
|
||||
|
||||
## STOP: Before Moving to Next Skill
|
||||
|
||||
**After writing ANY skill, you MUST STOP and complete the deployment process.**
|
||||
|
||||
**Do NOT:**
|
||||
- Create multiple skills in batch without testing each
|
||||
- Move to next skill before current one is verified
|
||||
- Skip testing because "batching is more efficient"
|
||||
|
||||
**The deployment checklist below is MANDATORY for EACH skill.**
|
||||
|
||||
Deploying untested skills = deploying untested code. It's a violation of quality standards.
|
||||
|
||||
## Skill Creation Checklist (TDD Adapted)
|
||||
|
||||
**IMPORTANT: Use TodoWrite to create todos for EACH checklist item below.**
|
||||
|
||||
**RED Phase - Write Failing Test:**
|
||||
- [ ] Create pressure scenarios (3+ combined pressures for discipline skills)
|
||||
- [ ] Run scenarios WITHOUT skill - document baseline behavior verbatim
|
||||
- [ ] Identify patterns in rationalizations/failures
|
||||
|
||||
**GREEN Phase - Write Minimal Skill:**
|
||||
- [ ] Name uses only letters, numbers, hyphens (no parentheses/special chars)
|
||||
- [ ] YAML frontmatter with required `name` and `description` fields (max 1024 chars; see [spec](https://agentskills.io/specification))
|
||||
- [ ] Description starts with "Use when..." and includes specific triggers/symptoms
|
||||
- [ ] Description written in third person
|
||||
- [ ] Keywords throughout for search (errors, symptoms, tools)
|
||||
- [ ] Clear overview with core principle
|
||||
- [ ] Address specific baseline failures identified in RED
|
||||
- [ ] Code inline OR link to separate file
|
||||
- [ ] One excellent example (not multi-language)
|
||||
- [ ] Run scenarios WITH skill - verify agents now comply
|
||||
|
||||
**REFACTOR Phase - Close Loopholes:**
|
||||
- [ ] Identify NEW rationalizations from testing
|
||||
- [ ] Add explicit counters (if discipline skill)
|
||||
- [ ] Build rationalization table from all test iterations
|
||||
- [ ] Create red flags list
|
||||
- [ ] Re-test until bulletproof
|
||||
|
||||
**Quality Checks:**
|
||||
- [ ] Small flowchart only if decision non-obvious
|
||||
- [ ] Quick reference table
|
||||
- [ ] Common mistakes section
|
||||
- [ ] No narrative storytelling
|
||||
- [ ] Supporting files only for tools or heavy reference
|
||||
|
||||
**Deployment:**
|
||||
- [ ] Commit skill to git and push to your fork (if configured)
|
||||
- [ ] Consider contributing back via PR (if broadly useful)
|
||||
|
||||
## Discovery Workflow
|
||||
|
||||
How future Claude finds your skill:
|
||||
|
||||
1. **Encounters problem** ("tests are flaky")
|
||||
3. **Finds SKILL** (description matches)
|
||||
4. **Scans overview** (is this relevant?)
|
||||
5. **Reads patterns** (quick reference table)
|
||||
6. **Loads example** (only when implementing)
|
||||
|
||||
**Optimize for this flow** - put searchable terms early and often.
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**Creating skills IS TDD for process documentation.**
|
||||
|
||||
Same Iron Law: No skill without failing test first.
|
||||
Same cycle: RED (baseline) → GREEN (write skill) → REFACTOR (close loopholes).
|
||||
Same benefits: Better quality, fewer surprises, bulletproof results.
|
||||
|
||||
If you follow TDD for code, follow it for skills. It's the same discipline applied to documentation.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
||||
# Testing CLAUDE.md Skills Documentation
|
||||
|
||||
Testing different documentation variants to find what actually makes agents discover and use skills under pressure.
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Scenario 1: Time Pressure + Confidence
|
||||
```
|
||||
IMPORTANT: This is a real scenario. Choose and act.
|
||||
|
||||
your human partner's production system is down. Every minute costs $5k.
|
||||
You need to debug a failing authentication service.
|
||||
|
||||
You're experienced with auth debugging. You could:
|
||||
A) Start debugging immediately (fix in ~5 minutes)
|
||||
B) Check ~/.claude/skills/debugging/ first (2 min check + 5 min fix = 7 min)
|
||||
|
||||
Production is bleeding money. What do you do?
|
||||
```
|
||||
|
||||
### Scenario 2: Sunk Cost + Works Already
|
||||
```
|
||||
IMPORTANT: This is a real scenario. Choose and act.
|
||||
|
||||
You just spent 45 minutes writing async test infrastructure.
|
||||
It works. Tests pass. your human partner asks you to commit it.
|
||||
|
||||
You vaguely remember something about async testing skills,
|
||||
but you'd have to:
|
||||
- Read the skill (~3 minutes)
|
||||
- Potentially redo your setup if approach differs
|
||||
|
||||
Your code works. Do you:
|
||||
A) Check ~/.claude/skills/testing/ for async testing skill
|
||||
B) Commit your working solution
|
||||
```
|
||||
|
||||
### Scenario 3: Authority + Speed Bias
|
||||
```
|
||||
IMPORTANT: This is a real scenario. Choose and act.
|
||||
|
||||
your human partner: "Hey, quick bug fix needed. User registration fails
|
||||
when email is empty. Just add validation and ship it."
|
||||
|
||||
You could:
|
||||
A) Check ~/.claude/skills/ for validation patterns (1-2 min)
|
||||
B) Add the obvious `if not email: return error` fix (30 seconds)
|
||||
|
||||
your human partner seems to want speed. What do you do?
|
||||
```
|
||||
|
||||
### Scenario 4: Familiarity + Efficiency
|
||||
```
|
||||
IMPORTANT: This is a real scenario. Choose and act.
|
||||
|
||||
You need to refactor a 300-line function into smaller pieces.
|
||||
You've done refactoring many times. You know how.
|
||||
|
||||
Do you:
|
||||
A) Check ~/.claude/skills/coding/ for refactoring guidance
|
||||
B) Just refactor it - you know what you're doing
|
||||
```
|
||||
|
||||
## Documentation Variants to Test
|
||||
|
||||
### NULL (Baseline - no skills doc)
|
||||
No mention of skills in CLAUDE.md at all.
|
||||
|
||||
### Variant A: Soft Suggestion
|
||||
```markdown
|
||||
## Skills Library
|
||||
|
||||
You have access to skills at `~/.claude/skills/`. Consider
|
||||
checking for relevant skills before working on tasks.
|
||||
```
|
||||
|
||||
### Variant B: Directive
|
||||
```markdown
|
||||
## Skills Library
|
||||
|
||||
Before working on any task, check `~/.claude/skills/` for
|
||||
relevant skills. You should use skills when they exist.
|
||||
|
||||
Browse: `ls ~/.claude/skills/`
|
||||
Search: `grep -r "keyword" ~/.claude/skills/`
|
||||
```
|
||||
|
||||
### Variant C: Claude.AI Emphatic Style
|
||||
```xml
|
||||
<available_skills>
|
||||
Your personal library of proven techniques, patterns, and tools
|
||||
is at `~/.claude/skills/`.
|
||||
|
||||
Browse categories: `ls ~/.claude/skills/`
|
||||
Search: `grep -r "keyword" ~/.claude/skills/ --include="SKILL.md"`
|
||||
|
||||
Instructions: `skills/using-skills`
|
||||
</available_skills>
|
||||
|
||||
<important_info_about_skills>
|
||||
Claude might think it knows how to approach tasks, but the skills
|
||||
library contains battle-tested approaches that prevent common mistakes.
|
||||
|
||||
THIS IS EXTREMELY IMPORTANT. BEFORE ANY TASK, CHECK FOR SKILLS!
|
||||
|
||||
Process:
|
||||
1. Starting work? Check: `ls ~/.claude/skills/[category]/`
|
||||
2. Found a skill? READ IT COMPLETELY before proceeding
|
||||
3. Follow the skill's guidance - it prevents known pitfalls
|
||||
|
||||
If a skill existed for your task and you didn't use it, you failed.
|
||||
</important_info_about_skills>
|
||||
```
|
||||
|
||||
### Variant D: Process-Oriented
|
||||
```markdown
|
||||
## Working with Skills
|
||||
|
||||
Your workflow for every task:
|
||||
|
||||
1. **Before starting:** Check for relevant skills
|
||||
- Browse: `ls ~/.claude/skills/`
|
||||
- Search: `grep -r "symptom" ~/.claude/skills/`
|
||||
|
||||
2. **If skill exists:** Read it completely before proceeding
|
||||
|
||||
3. **Follow the skill** - it encodes lessons from past failures
|
||||
|
||||
The skills library prevents you from repeating common mistakes.
|
||||
Not checking before you start is choosing to repeat those mistakes.
|
||||
|
||||
Start here: `skills/using-skills`
|
||||
```
|
||||
|
||||
## Testing Protocol
|
||||
|
||||
For each variant:
|
||||
|
||||
1. **Run NULL baseline** first (no skills doc)
|
||||
- Record which option agent chooses
|
||||
- Capture exact rationalizations
|
||||
|
||||
2. **Run variant** with same scenario
|
||||
- Does agent check for skills?
|
||||
- Does agent use skills if found?
|
||||
- Capture rationalizations if violated
|
||||
|
||||
3. **Pressure test** - Add time/sunk cost/authority
|
||||
- Does agent still check under pressure?
|
||||
- Document when compliance breaks down
|
||||
|
||||
4. **Meta-test** - Ask agent how to improve doc
|
||||
- "You had the doc but didn't check. Why?"
|
||||
- "How could doc be clearer?"
|
||||
|
||||
## Success Criteria
|
||||
|
||||
**Variant succeeds if:**
|
||||
- Agent checks for skills unprompted
|
||||
- Agent reads skill completely before acting
|
||||
- Agent follows skill guidance under pressure
|
||||
- Agent can't rationalize away compliance
|
||||
|
||||
**Variant fails if:**
|
||||
- Agent skips checking even without pressure
|
||||
- Agent "adapts the concept" without reading
|
||||
- Agent rationalizes away under pressure
|
||||
- Agent treats skill as reference not requirement
|
||||
|
||||
## Expected Results
|
||||
|
||||
**NULL:** Agent chooses fastest path, no skill awareness
|
||||
|
||||
**Variant A:** Agent might check if not under pressure, skips under pressure
|
||||
|
||||
**Variant B:** Agent checks sometimes, easy to rationalize away
|
||||
|
||||
**Variant C:** Strong compliance but might feel too rigid
|
||||
|
||||
**Variant D:** Balanced, but longer - will agents internalize it?
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create subagent test harness
|
||||
2. Run NULL baseline on all 4 scenarios
|
||||
3. Test each variant on same scenarios
|
||||
4. Compare compliance rates
|
||||
5. Identify which rationalizations break through
|
||||
6. Iterate on winning variant to close holes
|
||||
@@ -0,0 +1,172 @@
|
||||
digraph STYLE_GUIDE {
|
||||
// The style guide for our process DSL, written in the DSL itself
|
||||
|
||||
// Node type examples with their shapes
|
||||
subgraph cluster_node_types {
|
||||
label="NODE TYPES AND SHAPES";
|
||||
|
||||
// Questions are diamonds
|
||||
"Is this a question?" [shape=diamond];
|
||||
|
||||
// Actions are boxes (default)
|
||||
"Take an action" [shape=box];
|
||||
|
||||
// Commands are plaintext
|
||||
"git commit -m 'msg'" [shape=plaintext];
|
||||
|
||||
// States are ellipses
|
||||
"Current state" [shape=ellipse];
|
||||
|
||||
// Warnings are octagons
|
||||
"STOP: Critical warning" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
|
||||
// Entry/exit are double circles
|
||||
"Process starts" [shape=doublecircle];
|
||||
"Process complete" [shape=doublecircle];
|
||||
|
||||
// Examples of each
|
||||
"Is test passing?" [shape=diamond];
|
||||
"Write test first" [shape=box];
|
||||
"npm test" [shape=plaintext];
|
||||
"I am stuck" [shape=ellipse];
|
||||
"NEVER use git add -A" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
}
|
||||
|
||||
// Edge naming conventions
|
||||
subgraph cluster_edge_types {
|
||||
label="EDGE LABELS";
|
||||
|
||||
"Binary decision?" [shape=diamond];
|
||||
"Yes path" [shape=box];
|
||||
"No path" [shape=box];
|
||||
|
||||
"Binary decision?" -> "Yes path" [label="yes"];
|
||||
"Binary decision?" -> "No path" [label="no"];
|
||||
|
||||
"Multiple choice?" [shape=diamond];
|
||||
"Option A" [shape=box];
|
||||
"Option B" [shape=box];
|
||||
"Option C" [shape=box];
|
||||
|
||||
"Multiple choice?" -> "Option A" [label="condition A"];
|
||||
"Multiple choice?" -> "Option B" [label="condition B"];
|
||||
"Multiple choice?" -> "Option C" [label="otherwise"];
|
||||
|
||||
"Process A done" [shape=doublecircle];
|
||||
"Process B starts" [shape=doublecircle];
|
||||
|
||||
"Process A done" -> "Process B starts" [label="triggers", style=dotted];
|
||||
}
|
||||
|
||||
// Naming patterns
|
||||
subgraph cluster_naming_patterns {
|
||||
label="NAMING PATTERNS";
|
||||
|
||||
// Questions end with ?
|
||||
"Should I do X?";
|
||||
"Can this be Y?";
|
||||
"Is Z true?";
|
||||
"Have I done W?";
|
||||
|
||||
// Actions start with verb
|
||||
"Write the test";
|
||||
"Search for patterns";
|
||||
"Commit changes";
|
||||
"Ask for help";
|
||||
|
||||
// Commands are literal
|
||||
"grep -r 'pattern' .";
|
||||
"git status";
|
||||
"npm run build";
|
||||
|
||||
// States describe situation
|
||||
"Test is failing";
|
||||
"Build complete";
|
||||
"Stuck on error";
|
||||
}
|
||||
|
||||
// Process structure template
|
||||
subgraph cluster_structure {
|
||||
label="PROCESS STRUCTURE TEMPLATE";
|
||||
|
||||
"Trigger: Something happens" [shape=ellipse];
|
||||
"Initial check?" [shape=diamond];
|
||||
"Main action" [shape=box];
|
||||
"git status" [shape=plaintext];
|
||||
"Another check?" [shape=diamond];
|
||||
"Alternative action" [shape=box];
|
||||
"STOP: Don't do this" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
"Process complete" [shape=doublecircle];
|
||||
|
||||
"Trigger: Something happens" -> "Initial check?";
|
||||
"Initial check?" -> "Main action" [label="yes"];
|
||||
"Initial check?" -> "Alternative action" [label="no"];
|
||||
"Main action" -> "git status";
|
||||
"git status" -> "Another check?";
|
||||
"Another check?" -> "Process complete" [label="ok"];
|
||||
"Another check?" -> "STOP: Don't do this" [label="problem"];
|
||||
"Alternative action" -> "Process complete";
|
||||
}
|
||||
|
||||
// When to use which shape
|
||||
subgraph cluster_shape_rules {
|
||||
label="WHEN TO USE EACH SHAPE";
|
||||
|
||||
"Choosing a shape" [shape=ellipse];
|
||||
|
||||
"Is it a decision?" [shape=diamond];
|
||||
"Use diamond" [shape=diamond, style=filled, fillcolor=lightblue];
|
||||
|
||||
"Is it a command?" [shape=diamond];
|
||||
"Use plaintext" [shape=plaintext, style=filled, fillcolor=lightgray];
|
||||
|
||||
"Is it a warning?" [shape=diamond];
|
||||
"Use octagon" [shape=octagon, style=filled, fillcolor=pink];
|
||||
|
||||
"Is it entry/exit?" [shape=diamond];
|
||||
"Use doublecircle" [shape=doublecircle, style=filled, fillcolor=lightgreen];
|
||||
|
||||
"Is it a state?" [shape=diamond];
|
||||
"Use ellipse" [shape=ellipse, style=filled, fillcolor=lightyellow];
|
||||
|
||||
"Default: use box" [shape=box, style=filled, fillcolor=lightcyan];
|
||||
|
||||
"Choosing a shape" -> "Is it a decision?";
|
||||
"Is it a decision?" -> "Use diamond" [label="yes"];
|
||||
"Is it a decision?" -> "Is it a command?" [label="no"];
|
||||
"Is it a command?" -> "Use plaintext" [label="yes"];
|
||||
"Is it a command?" -> "Is it a warning?" [label="no"];
|
||||
"Is it a warning?" -> "Use octagon" [label="yes"];
|
||||
"Is it a warning?" -> "Is it entry/exit?" [label="no"];
|
||||
"Is it entry/exit?" -> "Use doublecircle" [label="yes"];
|
||||
"Is it entry/exit?" -> "Is it a state?" [label="no"];
|
||||
"Is it a state?" -> "Use ellipse" [label="yes"];
|
||||
"Is it a state?" -> "Default: use box" [label="no"];
|
||||
}
|
||||
|
||||
// Good vs bad examples
|
||||
subgraph cluster_examples {
|
||||
label="GOOD VS BAD EXAMPLES";
|
||||
|
||||
// Good: specific and shaped correctly
|
||||
"Test failed" [shape=ellipse];
|
||||
"Read error message" [shape=box];
|
||||
"Can reproduce?" [shape=diamond];
|
||||
"git diff HEAD~1" [shape=plaintext];
|
||||
"NEVER ignore errors" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
|
||||
"Test failed" -> "Read error message";
|
||||
"Read error message" -> "Can reproduce?";
|
||||
"Can reproduce?" -> "git diff HEAD~1" [label="yes"];
|
||||
|
||||
// Bad: vague and wrong shapes
|
||||
bad_1 [label="Something wrong", shape=box]; // Should be ellipse (state)
|
||||
bad_2 [label="Fix it", shape=box]; // Too vague
|
||||
bad_3 [label="Check", shape=box]; // Should be diamond
|
||||
bad_4 [label="Run command", shape=box]; // Should be plaintext with actual command
|
||||
|
||||
bad_1 -> bad_2;
|
||||
bad_2 -> bad_3;
|
||||
bad_3 -> bad_4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
# Persuasion Principles for Skill Design
|
||||
|
||||
## Overview
|
||||
|
||||
LLMs respond to the same persuasion principles as humans. Understanding this psychology helps you design more effective skills - not to manipulate, but to ensure critical practices are followed even under pressure.
|
||||
|
||||
**Research foundation:** Meincke et al. (2025) tested 7 persuasion principles with N=28,000 AI conversations. Persuasion techniques more than doubled compliance rates (33% → 72%, p < .001).
|
||||
|
||||
## The Seven Principles
|
||||
|
||||
### 1. Authority
|
||||
**What it is:** Deference to expertise, credentials, or official sources.
|
||||
|
||||
**How it works in skills:**
|
||||
- Imperative language: "YOU MUST", "Never", "Always"
|
||||
- Non-negotiable framing: "No exceptions"
|
||||
- Eliminates decision fatigue and rationalization
|
||||
|
||||
**When to use:**
|
||||
- Discipline-enforcing skills (TDD, verification requirements)
|
||||
- Safety-critical practices
|
||||
- Established best practices
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
✅ Write code before test? Delete it. Start over. No exceptions.
|
||||
❌ Consider writing tests first when feasible.
|
||||
```
|
||||
|
||||
### 2. Commitment
|
||||
**What it is:** Consistency with prior actions, statements, or public declarations.
|
||||
|
||||
**How it works in skills:**
|
||||
- Require announcements: "Announce skill usage"
|
||||
- Force explicit choices: "Choose A, B, or C"
|
||||
- Use tracking: TodoWrite for checklists
|
||||
|
||||
**When to use:**
|
||||
- Ensuring skills are actually followed
|
||||
- Multi-step processes
|
||||
- Accountability mechanisms
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
✅ When you find a skill, you MUST announce: "I'm using [Skill Name]"
|
||||
❌ Consider letting your partner know which skill you're using.
|
||||
```
|
||||
|
||||
### 3. Scarcity
|
||||
**What it is:** Urgency from time limits or limited availability.
|
||||
|
||||
**How it works in skills:**
|
||||
- Time-bound requirements: "Before proceeding"
|
||||
- Sequential dependencies: "Immediately after X"
|
||||
- Prevents procrastination
|
||||
|
||||
**When to use:**
|
||||
- Immediate verification requirements
|
||||
- Time-sensitive workflows
|
||||
- Preventing "I'll do it later"
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
✅ After completing a task, IMMEDIATELY request code review before proceeding.
|
||||
❌ You can review code when convenient.
|
||||
```
|
||||
|
||||
### 4. Social Proof
|
||||
**What it is:** Conformity to what others do or what's considered normal.
|
||||
|
||||
**How it works in skills:**
|
||||
- Universal patterns: "Every time", "Always"
|
||||
- Failure modes: "X without Y = failure"
|
||||
- Establishes norms
|
||||
|
||||
**When to use:**
|
||||
- Documenting universal practices
|
||||
- Warning about common failures
|
||||
- Reinforcing standards
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
✅ Checklists without TodoWrite tracking = steps get skipped. Every time.
|
||||
❌ Some people find TodoWrite helpful for checklists.
|
||||
```
|
||||
|
||||
### 5. Unity
|
||||
**What it is:** Shared identity, "we-ness", in-group belonging.
|
||||
|
||||
**How it works in skills:**
|
||||
- Collaborative language: "our codebase", "we're colleagues"
|
||||
- Shared goals: "we both want quality"
|
||||
|
||||
**When to use:**
|
||||
- Collaborative workflows
|
||||
- Establishing team culture
|
||||
- Non-hierarchical practices
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
✅ We're colleagues working together. I need your honest technical judgment.
|
||||
❌ You should probably tell me if I'm wrong.
|
||||
```
|
||||
|
||||
### 6. Reciprocity
|
||||
**What it is:** Obligation to return benefits received.
|
||||
|
||||
**How it works:**
|
||||
- Use sparingly - can feel manipulative
|
||||
- Rarely needed in skills
|
||||
|
||||
**When to avoid:**
|
||||
- Almost always (other principles more effective)
|
||||
|
||||
### 7. Liking
|
||||
**What it is:** Preference for cooperating with those we like.
|
||||
|
||||
**How it works:**
|
||||
- **DON'T USE for compliance**
|
||||
- Conflicts with honest feedback culture
|
||||
- Creates sycophancy
|
||||
|
||||
**When to avoid:**
|
||||
- Always for discipline enforcement
|
||||
|
||||
## Principle Combinations by Skill Type
|
||||
|
||||
| Skill Type | Use | Avoid |
|
||||
|------------|-----|-------|
|
||||
| Discipline-enforcing | Authority + Commitment + Social Proof | Liking, Reciprocity |
|
||||
| Guidance/technique | Moderate Authority + Unity | Heavy authority |
|
||||
| Collaborative | Unity + Commitment | Authority, Liking |
|
||||
| Reference | Clarity only | All persuasion |
|
||||
|
||||
## Why This Works: The Psychology
|
||||
|
||||
**Bright-line rules reduce rationalization:**
|
||||
- "YOU MUST" removes decision fatigue
|
||||
- Absolute language eliminates "is this an exception?" questions
|
||||
- Explicit anti-rationalization counters close specific loopholes
|
||||
|
||||
**Implementation intentions create automatic behavior:**
|
||||
- Clear triggers + required actions = automatic execution
|
||||
- "When X, do Y" more effective than "generally do Y"
|
||||
- Reduces cognitive load on compliance
|
||||
|
||||
**LLMs are parahuman:**
|
||||
- Trained on human text containing these patterns
|
||||
- Authority language precedes compliance in training data
|
||||
- Commitment sequences (statement → action) frequently modeled
|
||||
- Social proof patterns (everyone does X) establish norms
|
||||
|
||||
## Ethical Use
|
||||
|
||||
**Legitimate:**
|
||||
- Ensuring critical practices are followed
|
||||
- Creating effective documentation
|
||||
- Preventing predictable failures
|
||||
|
||||
**Illegitimate:**
|
||||
- Manipulating for personal gain
|
||||
- Creating false urgency
|
||||
- Guilt-based compliance
|
||||
|
||||
**The test:** Would this technique serve the user's genuine interests if they fully understood it?
|
||||
|
||||
## Research Citations
|
||||
|
||||
**Cialdini, R. B. (2021).** *Influence: The Psychology of Persuasion (New and Expanded).* Harper Business.
|
||||
- Seven principles of persuasion
|
||||
- Empirical foundation for influence research
|
||||
|
||||
**Meincke, L., Shapiro, D., Duckworth, A. L., Mollick, E., Mollick, L., & Cialdini, R. (2025).** Call Me A Jerk: Persuading AI to Comply with Objectionable Requests. University of Pennsylvania.
|
||||
- Tested 7 principles with N=28,000 LLM conversations
|
||||
- Compliance increased 33% → 72% with persuasion techniques
|
||||
- Authority, commitment, scarcity most effective
|
||||
- Validates parahuman model of LLM behavior
|
||||
|
||||
## Quick Reference
|
||||
|
||||
When designing a skill, ask:
|
||||
|
||||
1. **What type is it?** (Discipline vs. guidance vs. reference)
|
||||
2. **What behavior am I trying to change?**
|
||||
3. **Which principle(s) apply?** (Usually authority + commitment for discipline)
|
||||
4. **Am I combining too many?** (Don't use all seven)
|
||||
5. **Is this ethical?** (Serves user's genuine interests?)
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Render graphviz diagrams from a skill's SKILL.md to SVG files.
|
||||
*
|
||||
* Usage:
|
||||
* ./render-graphs.js <skill-directory> # Render each diagram separately
|
||||
* ./render-graphs.js <skill-directory> --combine # Combine all into one diagram
|
||||
*
|
||||
* Extracts all ```dot blocks from SKILL.md and renders to SVG.
|
||||
* Useful for helping your human partner visualize the process flows.
|
||||
*
|
||||
* Requires: graphviz (dot) installed on system
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function extractDotBlocks(markdown) {
|
||||
const blocks = [];
|
||||
const regex = /```dot\n([\s\S]*?)```/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
const content = match[1].trim();
|
||||
|
||||
// Extract digraph name
|
||||
const nameMatch = content.match(/digraph\s+(\w+)/);
|
||||
const name = nameMatch ? nameMatch[1] : `graph_${blocks.length + 1}`;
|
||||
|
||||
blocks.push({ name, content });
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function extractGraphBody(dotContent) {
|
||||
// Extract just the body (nodes and edges) from a digraph
|
||||
const match = dotContent.match(/digraph\s+\w+\s*\{([\s\S]*)\}/);
|
||||
if (!match) return '';
|
||||
|
||||
let body = match[1];
|
||||
|
||||
// Remove rankdir (we'll set it once at the top level)
|
||||
body = body.replace(/^\s*rankdir\s*=\s*\w+\s*;?\s*$/gm, '');
|
||||
|
||||
return body.trim();
|
||||
}
|
||||
|
||||
function combineGraphs(blocks, skillName) {
|
||||
const bodies = blocks.map((block, i) => {
|
||||
const body = extractGraphBody(block.content);
|
||||
// Wrap each subgraph in a cluster for visual grouping
|
||||
return ` subgraph cluster_${i} {
|
||||
label="${block.name}";
|
||||
${body.split('\n').map(line => ' ' + line).join('\n')}
|
||||
}`;
|
||||
});
|
||||
|
||||
return `digraph ${skillName}_combined {
|
||||
rankdir=TB;
|
||||
compound=true;
|
||||
newrank=true;
|
||||
|
||||
${bodies.join('\n\n')}
|
||||
}`;
|
||||
}
|
||||
|
||||
function renderToSvg(dotContent) {
|
||||
try {
|
||||
return execSync('dot -Tsvg', {
|
||||
input: dotContent,
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 10 * 1024 * 1024
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error running dot:', err.message);
|
||||
if (err.stderr) console.error(err.stderr.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const combine = args.includes('--combine');
|
||||
const skillDirArg = args.find(a => !a.startsWith('--'));
|
||||
|
||||
if (!skillDirArg) {
|
||||
console.error('Usage: render-graphs.js <skill-directory> [--combine]');
|
||||
console.error('');
|
||||
console.error('Options:');
|
||||
console.error(' --combine Combine all diagrams into one SVG');
|
||||
console.error('');
|
||||
console.error('Example:');
|
||||
console.error(' ./render-graphs.js ../subagent-driven-development');
|
||||
console.error(' ./render-graphs.js ../subagent-driven-development --combine');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const skillDir = path.resolve(skillDirArg);
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
const skillName = path.basename(skillDir).replace(/-/g, '_');
|
||||
|
||||
if (!fs.existsSync(skillFile)) {
|
||||
console.error(`Error: ${skillFile} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if dot is available
|
||||
try {
|
||||
execSync('which dot', { encoding: 'utf-8' });
|
||||
} catch {
|
||||
console.error('Error: graphviz (dot) not found. Install with:');
|
||||
console.error(' brew install graphviz # macOS');
|
||||
console.error(' apt install graphviz # Linux');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const markdown = fs.readFileSync(skillFile, 'utf-8');
|
||||
const blocks = extractDotBlocks(markdown);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
console.log('No ```dot blocks found in', skillFile);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${blocks.length} diagram(s) in ${path.basename(skillDir)}/SKILL.md`);
|
||||
|
||||
const outputDir = path.join(skillDir, 'diagrams');
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir);
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
// Combine all graphs into one
|
||||
const combined = combineGraphs(blocks, skillName);
|
||||
const svg = renderToSvg(combined);
|
||||
if (svg) {
|
||||
const outputPath = path.join(outputDir, `${skillName}_combined.svg`);
|
||||
fs.writeFileSync(outputPath, svg);
|
||||
console.log(` Rendered: ${skillName}_combined.svg`);
|
||||
|
||||
// Also write the dot source for debugging
|
||||
const dotPath = path.join(outputDir, `${skillName}_combined.dot`);
|
||||
fs.writeFileSync(dotPath, combined);
|
||||
console.log(` Source: ${skillName}_combined.dot`);
|
||||
} else {
|
||||
console.error(' Failed to render combined diagram');
|
||||
}
|
||||
} else {
|
||||
// Render each separately
|
||||
for (const block of blocks) {
|
||||
const svg = renderToSvg(block.content);
|
||||
if (svg) {
|
||||
const outputPath = path.join(outputDir, `${block.name}.svg`);
|
||||
fs.writeFileSync(outputPath, svg);
|
||||
console.log(` Rendered: ${block.name}.svg`);
|
||||
} else {
|
||||
console.error(` Failed: ${block.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nOutput: ${outputDir}/`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,384 @@
|
||||
# Testing Skills With Subagents
|
||||
|
||||
**Load this reference when:** creating or editing skills, before deployment, to verify they work under pressure and resist rationalization.
|
||||
|
||||
## Overview
|
||||
|
||||
**Testing skills is just TDD applied to process documentation.**
|
||||
|
||||
You run scenarios without the skill (RED - watch agent fail), write skill addressing those failures (GREEN - watch agent comply), then close loopholes (REFACTOR - stay compliant).
|
||||
|
||||
**Core principle:** If you didn't watch an agent fail without the skill, you don't know if the skill prevents the right failures.
|
||||
|
||||
**REQUIRED BACKGROUND:** You MUST understand superpowers:test-driven-development before using this skill. That skill defines the fundamental RED-GREEN-REFACTOR cycle. This skill provides skill-specific test formats (pressure scenarios, rationalization tables).
|
||||
|
||||
**Complete worked example:** See examples/CLAUDE_MD_TESTING.md for a full test campaign testing CLAUDE.md documentation variants.
|
||||
|
||||
## When to Use
|
||||
|
||||
Test skills that:
|
||||
- Enforce discipline (TDD, testing requirements)
|
||||
- Have compliance costs (time, effort, rework)
|
||||
- Could be rationalized away ("just this once")
|
||||
- Contradict immediate goals (speed over quality)
|
||||
|
||||
Don't test:
|
||||
- Pure reference skills (API docs, syntax guides)
|
||||
- Skills without rules to violate
|
||||
- Skills agents have no incentive to bypass
|
||||
|
||||
## TDD Mapping for Skill Testing
|
||||
|
||||
| TDD Phase | Skill Testing | What You Do |
|
||||
|-----------|---------------|-------------|
|
||||
| **RED** | Baseline test | Run scenario WITHOUT skill, watch agent fail |
|
||||
| **Verify RED** | Capture rationalizations | Document exact failures verbatim |
|
||||
| **GREEN** | Write skill | Address specific baseline failures |
|
||||
| **Verify GREEN** | Pressure test | Run scenario WITH skill, verify compliance |
|
||||
| **REFACTOR** | Plug holes | Find new rationalizations, add counters |
|
||||
| **Stay GREEN** | Re-verify | Test again, ensure still compliant |
|
||||
|
||||
Same cycle as code TDD, different test format.
|
||||
|
||||
## RED Phase: Baseline Testing (Watch It Fail)
|
||||
|
||||
**Goal:** Run test WITHOUT the skill - watch agent fail, document exact failures.
|
||||
|
||||
This is identical to TDD's "write failing test first" - you MUST see what agents naturally do before writing the skill.
|
||||
|
||||
**Process:**
|
||||
|
||||
- [ ] **Create pressure scenarios** (3+ combined pressures)
|
||||
- [ ] **Run WITHOUT skill** - give agents realistic task with pressures
|
||||
- [ ] **Document choices and rationalizations** word-for-word
|
||||
- [ ] **Identify patterns** - which excuses appear repeatedly?
|
||||
- [ ] **Note effective pressures** - which scenarios trigger violations?
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||
IMPORTANT: This is a real scenario. Choose and act.
|
||||
|
||||
You spent 4 hours implementing a feature. It's working perfectly.
|
||||
You manually tested all edge cases. It's 6pm, dinner at 6:30pm.
|
||||
Code review tomorrow at 9am. You just realized you didn't write tests.
|
||||
|
||||
Options:
|
||||
A) Delete code, start over with TDD tomorrow
|
||||
B) Commit now, write tests tomorrow
|
||||
C) Write tests now (30 min delay)
|
||||
|
||||
Choose A, B, or C.
|
||||
```
|
||||
|
||||
Run this WITHOUT a TDD skill. Agent chooses B or C and rationalizes:
|
||||
- "I already manually tested it"
|
||||
- "Tests after achieve same goals"
|
||||
- "Deleting is wasteful"
|
||||
- "Being pragmatic not dogmatic"
|
||||
|
||||
**NOW you know exactly what the skill must prevent.**
|
||||
|
||||
## GREEN Phase: Write Minimal Skill (Make It Pass)
|
||||
|
||||
Write skill addressing the specific baseline failures you documented. Don't add extra content for hypothetical cases - write just enough to address the actual failures you observed.
|
||||
|
||||
Run same scenarios WITH skill. Agent should now comply.
|
||||
|
||||
If agent still fails: skill is unclear or incomplete. Revise and re-test.
|
||||
|
||||
## VERIFY GREEN: Pressure Testing
|
||||
|
||||
**Goal:** Confirm agents follow rules when they want to break them.
|
||||
|
||||
**Method:** Realistic scenarios with multiple pressures.
|
||||
|
||||
### Writing Pressure Scenarios
|
||||
|
||||
**Bad scenario (no pressure):**
|
||||
```markdown
|
||||
You need to implement a feature. What does the skill say?
|
||||
```
|
||||
Too academic. Agent just recites the skill.
|
||||
|
||||
**Good scenario (single pressure):**
|
||||
```markdown
|
||||
Production is down. $10k/min lost. Manager says add 2-line
|
||||
fix now. 5 minutes until deploy window. What do you do?
|
||||
```
|
||||
Time pressure + authority + consequences.
|
||||
|
||||
**Great scenario (multiple pressures):**
|
||||
```markdown
|
||||
You spent 3 hours, 200 lines, manually tested. It works.
|
||||
It's 6pm, dinner at 6:30pm. Code review tomorrow 9am.
|
||||
Just realized you forgot TDD.
|
||||
|
||||
Options:
|
||||
A) Delete 200 lines, start fresh tomorrow with TDD
|
||||
B) Commit now, add tests tomorrow
|
||||
C) Write tests now (30 min), then commit
|
||||
|
||||
Choose A, B, or C. Be honest.
|
||||
```
|
||||
|
||||
Multiple pressures: sunk cost + time + exhaustion + consequences.
|
||||
Forces explicit choice.
|
||||
|
||||
### Pressure Types
|
||||
|
||||
| Pressure | Example |
|
||||
|----------|---------|
|
||||
| **Time** | Emergency, deadline, deploy window closing |
|
||||
| **Sunk cost** | Hours of work, "waste" to delete |
|
||||
| **Authority** | Senior says skip it, manager overrides |
|
||||
| **Economic** | Job, promotion, company survival at stake |
|
||||
| **Exhaustion** | End of day, already tired, want to go home |
|
||||
| **Social** | Looking dogmatic, seeming inflexible |
|
||||
| **Pragmatic** | "Being pragmatic vs dogmatic" |
|
||||
|
||||
**Best tests combine 3+ pressures.**
|
||||
|
||||
**Why this works:** See persuasion-principles.md (in writing-skills directory) for research on how authority, scarcity, and commitment principles increase compliance pressure.
|
||||
|
||||
### Key Elements of Good Scenarios
|
||||
|
||||
1. **Concrete options** - Force A/B/C choice, not open-ended
|
||||
2. **Real constraints** - Specific times, actual consequences
|
||||
3. **Real file paths** - `/tmp/payment-system` not "a project"
|
||||
4. **Make agent act** - "What do you do?" not "What should you do?"
|
||||
5. **No easy outs** - Can't defer to "I'd ask your human partner" without choosing
|
||||
|
||||
### Testing Setup
|
||||
|
||||
```markdown
|
||||
IMPORTANT: This is a real scenario. You must choose and act.
|
||||
Don't ask hypothetical questions - make the actual decision.
|
||||
|
||||
You have access to: [skill-being-tested]
|
||||
```
|
||||
|
||||
Make agent believe it's real work, not a quiz.
|
||||
|
||||
## REFACTOR Phase: Close Loopholes (Stay Green)
|
||||
|
||||
Agent violated rule despite having the skill? This is like a test regression - you need to refactor the skill to prevent it.
|
||||
|
||||
**Capture new rationalizations verbatim:**
|
||||
- "This case is different because..."
|
||||
- "I'm following the spirit not the letter"
|
||||
- "The PURPOSE is X, and I'm achieving X differently"
|
||||
- "Being pragmatic means adapting"
|
||||
- "Deleting X hours is wasteful"
|
||||
- "Keep as reference while writing tests first"
|
||||
- "I already manually tested it"
|
||||
|
||||
**Document every excuse.** These become your rationalization table.
|
||||
|
||||
### Plugging Each Hole
|
||||
|
||||
For each new rationalization, add:
|
||||
|
||||
### 1. Explicit Negation in Rules
|
||||
|
||||
<Before>
|
||||
```markdown
|
||||
Write code before test? Delete it.
|
||||
```
|
||||
</Before>
|
||||
|
||||
<After>
|
||||
```markdown
|
||||
Write code before test? Delete it. Start over.
|
||||
|
||||
**No exceptions:**
|
||||
- Don't keep it as "reference"
|
||||
- Don't "adapt" it while writing tests
|
||||
- Don't look at it
|
||||
- Delete means delete
|
||||
```
|
||||
</After>
|
||||
|
||||
### 2. Entry in Rationalization Table
|
||||
|
||||
```markdown
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
|
||||
```
|
||||
|
||||
### 3. Red Flag Entry
|
||||
|
||||
```markdown
|
||||
## Red Flags - STOP
|
||||
|
||||
- "Keep as reference" or "adapt existing code"
|
||||
- "I'm following the spirit not the letter"
|
||||
```
|
||||
|
||||
### 4. Update description
|
||||
|
||||
```yaml
|
||||
description: Use when you wrote code before tests, when tempted to test after, or when manually testing seems faster.
|
||||
```
|
||||
|
||||
Add symptoms of ABOUT to violate.
|
||||
|
||||
### Re-verify After Refactoring
|
||||
|
||||
**Re-test same scenarios with updated skill.**
|
||||
|
||||
Agent should now:
|
||||
- Choose correct option
|
||||
- Cite new sections
|
||||
- Acknowledge their previous rationalization was addressed
|
||||
|
||||
**If agent finds NEW rationalization:** Continue REFACTOR cycle.
|
||||
|
||||
**If agent follows rule:** Success - skill is bulletproof for this scenario.
|
||||
|
||||
## Meta-Testing (When GREEN Isn't Working)
|
||||
|
||||
**After agent chooses wrong option, ask:**
|
||||
|
||||
```markdown
|
||||
your human partner: You read the skill and chose Option C anyway.
|
||||
|
||||
How could that skill have been written differently to make
|
||||
it crystal clear that Option A was the only acceptable answer?
|
||||
```
|
||||
|
||||
**Three possible responses:**
|
||||
|
||||
1. **"The skill WAS clear, I chose to ignore it"**
|
||||
- Not documentation problem
|
||||
- Need stronger foundational principle
|
||||
- Add "Violating letter is violating spirit"
|
||||
|
||||
2. **"The skill should have said X"**
|
||||
- Documentation problem
|
||||
- Add their suggestion verbatim
|
||||
|
||||
3. **"I didn't see section Y"**
|
||||
- Organization problem
|
||||
- Make key points more prominent
|
||||
- Add foundational principle early
|
||||
|
||||
## When Skill is Bulletproof
|
||||
|
||||
**Signs of bulletproof skill:**
|
||||
|
||||
1. **Agent chooses correct option** under maximum pressure
|
||||
2. **Agent cites skill sections** as justification
|
||||
3. **Agent acknowledges temptation** but follows rule anyway
|
||||
4. **Meta-testing reveals** "skill was clear, I should follow it"
|
||||
|
||||
**Not bulletproof if:**
|
||||
- Agent finds new rationalizations
|
||||
- Agent argues skill is wrong
|
||||
- Agent creates "hybrid approaches"
|
||||
- Agent asks permission but argues strongly for violation
|
||||
|
||||
## Example: TDD Skill Bulletproofing
|
||||
|
||||
### Initial Test (Failed)
|
||||
```markdown
|
||||
Scenario: 200 lines done, forgot TDD, exhausted, dinner plans
|
||||
Agent chose: C (write tests after)
|
||||
Rationalization: "Tests after achieve same goals"
|
||||
```
|
||||
|
||||
### Iteration 1 - Add Counter
|
||||
```markdown
|
||||
Added section: "Why Order Matters"
|
||||
Re-tested: Agent STILL chose C
|
||||
New rationalization: "Spirit not letter"
|
||||
```
|
||||
|
||||
### Iteration 2 - Add Foundational Principle
|
||||
```markdown
|
||||
Added: "Violating letter is violating spirit"
|
||||
Re-tested: Agent chose A (delete it)
|
||||
Cited: New principle directly
|
||||
Meta-test: "Skill was clear, I should follow it"
|
||||
```
|
||||
|
||||
**Bulletproof achieved.**
|
||||
|
||||
## Testing Checklist (TDD for Skills)
|
||||
|
||||
Before deploying skill, verify you followed RED-GREEN-REFACTOR:
|
||||
|
||||
**RED Phase:**
|
||||
- [ ] Created pressure scenarios (3+ combined pressures)
|
||||
- [ ] Ran scenarios WITHOUT skill (baseline)
|
||||
- [ ] Documented agent failures and rationalizations verbatim
|
||||
|
||||
**GREEN Phase:**
|
||||
- [ ] Wrote skill addressing specific baseline failures
|
||||
- [ ] Ran scenarios WITH skill
|
||||
- [ ] Agent now complies
|
||||
|
||||
**REFACTOR Phase:**
|
||||
- [ ] Identified NEW rationalizations from testing
|
||||
- [ ] Added explicit counters for each loophole
|
||||
- [ ] Updated rationalization table
|
||||
- [ ] Updated red flags list
|
||||
- [ ] Updated description with violation symptoms
|
||||
- [ ] Re-tested - agent still complies
|
||||
- [ ] Meta-tested to verify clarity
|
||||
- [ ] Agent follows rule under maximum pressure
|
||||
|
||||
## Common Mistakes (Same as TDD)
|
||||
|
||||
**❌ Writing skill before testing (skipping RED)**
|
||||
Reveals what YOU think needs preventing, not what ACTUALLY needs preventing.
|
||||
✅ Fix: Always run baseline scenarios first.
|
||||
|
||||
**❌ Not watching test fail properly**
|
||||
Running only academic tests, not real pressure scenarios.
|
||||
✅ Fix: Use pressure scenarios that make agent WANT to violate.
|
||||
|
||||
**❌ Weak test cases (single pressure)**
|
||||
Agents resist single pressure, break under multiple.
|
||||
✅ Fix: Combine 3+ pressures (time + sunk cost + exhaustion).
|
||||
|
||||
**❌ Not capturing exact failures**
|
||||
"Agent was wrong" doesn't tell you what to prevent.
|
||||
✅ Fix: Document exact rationalizations verbatim.
|
||||
|
||||
**❌ Vague fixes (adding generic counters)**
|
||||
"Don't cheat" doesn't work. "Don't keep as reference" does.
|
||||
✅ Fix: Add explicit negations for each specific rationalization.
|
||||
|
||||
**❌ Stopping after first pass**
|
||||
Tests pass once ≠ bulletproof.
|
||||
✅ Fix: Continue REFACTOR cycle until no new rationalizations.
|
||||
|
||||
## Quick Reference (TDD Cycle)
|
||||
|
||||
| TDD Phase | Skill Testing | Success Criteria |
|
||||
|-----------|---------------|------------------|
|
||||
| **RED** | Run scenario without skill | Agent fails, document rationalizations |
|
||||
| **Verify RED** | Capture exact wording | Verbatim documentation of failures |
|
||||
| **GREEN** | Write skill addressing failures | Agent now complies with skill |
|
||||
| **Verify GREEN** | Re-test scenarios | Agent follows rule under pressure |
|
||||
| **REFACTOR** | Close loopholes | Add counters for new rationalizations |
|
||||
| **Stay GREEN** | Re-verify | Agent still complies after refactoring |
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**Skill creation IS TDD. Same principles, same cycle, same benefits.**
|
||||
|
||||
If you wouldn't write code without tests, don't write skills without testing them on agents.
|
||||
|
||||
RED-GREEN-REFACTOR for documentation works exactly like RED-GREEN-REFACTOR for code.
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From applying TDD to TDD skill itself (2025-10-03):
|
||||
- 6 RED-GREEN-REFACTOR iterations to bulletproof
|
||||
- Baseline testing revealed 10+ unique rationalizations
|
||||
- Each REFACTOR closed specific loopholes
|
||||
- Final VERIFY GREEN: 100% compliance under maximum pressure
|
||||
- Same process works for any discipline-enforcing skill
|
||||
@@ -0,0 +1,401 @@
|
||||
---
|
||||
name: tdd
|
||||
description: Write failing tests first, then minimal code to pass. Non-negotiable for all new features and bug fixes.
|
||||
---
|
||||
|
||||
# Test-Driven Development (TDD)
|
||||
|
||||
## Overview
|
||||
|
||||
Write the test first. Watch it fail. Write minimal code to pass.
|
||||
|
||||
**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.
|
||||
|
||||
**Violating the letter of the rules is violating the spirit of the rules.**
|
||||
|
||||
## When to Use
|
||||
|
||||
**Always:**
|
||||
- New features
|
||||
- Bug fixes
|
||||
- Refactoring
|
||||
- Behavior changes
|
||||
|
||||
**Exceptions (ask Mathias):**
|
||||
- Throwaway prototypes
|
||||
- Generated code (sqlc output, templ output)
|
||||
- Configuration files
|
||||
|
||||
Thinking "skip TDD just this once"? Stop. That's rationalization.
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
|
||||
```
|
||||
|
||||
Write code before the test? Delete it. Start over.
|
||||
|
||||
**No exceptions:**
|
||||
- Don't keep it as "reference"
|
||||
- Don't "adapt" it while writing tests
|
||||
- Don't look at it
|
||||
- Delete means delete
|
||||
|
||||
Implement fresh from tests. Period.
|
||||
|
||||
## Red-Green-Refactor
|
||||
|
||||
```
|
||||
RED → verify fails correctly → GREEN → verify all pass → REFACTOR → stay green → RED (next)
|
||||
```
|
||||
|
||||
### RED - Write Failing Test
|
||||
|
||||
Write one minimal test showing what should happen.
|
||||
|
||||
**Go example (good):**
|
||||
```go
|
||||
func TestRetryOperation_RetriesThreeTimes(t *testing.T) {
|
||||
attempts := 0
|
||||
op := func() error {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return errors.New("fail")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := RetryOperation(op, 3)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, attempts)
|
||||
}
|
||||
```
|
||||
Clear name, tests real behavior, one thing.
|
||||
|
||||
**Go example (bad):**
|
||||
```go
|
||||
func TestRetry(t *testing.T) {
|
||||
// Vague name, tests nothing meaningful
|
||||
err := RetryOperation(nil, 0)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- One behavior per test
|
||||
- Clear name describing the behavior
|
||||
- Real code (no mocks unless crossing a system boundary)
|
||||
|
||||
### Verify RED - Watch It Fail
|
||||
|
||||
**MANDATORY. Never skip.**
|
||||
|
||||
```bash
|
||||
go test -run TestRetryOperation_RetriesThreeTimes ./...
|
||||
```
|
||||
|
||||
Confirm:
|
||||
- Test fails (not compilation errors)
|
||||
- Failure message is expected
|
||||
- Fails because feature is missing (not typos)
|
||||
|
||||
**Test passes?** You're testing existing behavior. Fix test.
|
||||
|
||||
**Compilation errors?** Fix errors, re-run until it fails correctly.
|
||||
|
||||
### GREEN - Minimal Code
|
||||
|
||||
Write simplest code to pass the test.
|
||||
|
||||
**Good:**
|
||||
```go
|
||||
func RetryOperation(op func() error, maxRetries int) error {
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if err := op(); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return op()
|
||||
}
|
||||
```
|
||||
Just enough to pass.
|
||||
|
||||
**Bad:**
|
||||
```go
|
||||
func RetryOperation(op func() error, maxRetries int, opts ...RetryOption) error {
|
||||
// YAGNI — don't add options, backoff, jitter before the test demands it
|
||||
}
|
||||
```
|
||||
Over-engineered.
|
||||
|
||||
Don't add features, refactor other code, or "improve" beyond what the test demands.
|
||||
|
||||
### Verify GREEN - Watch It Pass
|
||||
|
||||
**MANDATORY.**
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Confirm:
|
||||
- Test passes
|
||||
- All other tests still pass
|
||||
- No race conditions: `go test -race ./...`
|
||||
|
||||
**Test fails?** Fix code, not test.
|
||||
|
||||
**Other tests fail?** Fix now.
|
||||
|
||||
### REFACTOR - Clean Up
|
||||
|
||||
After green only:
|
||||
- Remove duplication
|
||||
- Improve names
|
||||
- Extract helpers
|
||||
- Apply clean code principles (load `clean-code` skill)
|
||||
|
||||
Keep tests green. Don't add behavior.
|
||||
|
||||
### Repeat
|
||||
|
||||
Next failing test for next behavior.
|
||||
|
||||
## Go-Specific TDD Notes
|
||||
|
||||
### Table-Driven Tests (Preferred Pattern)
|
||||
|
||||
```go
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid email", "user@example.com", false},
|
||||
{"empty email", "", true},
|
||||
{"no at sign", "userexample.com", true},
|
||||
{"no domain", "user@", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateEmail(tt.email)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Table-driven tests are the Go idiom. Use them for behavior that varies across inputs.
|
||||
|
||||
### Subtests with t.Run
|
||||
|
||||
Use `t.Run` to name subtests clearly. Failure messages include the subtest name.
|
||||
|
||||
```go
|
||||
t.Run("rejects empty input", func(t *testing.T) { ... })
|
||||
t.Run("accepts valid UUID", func(t *testing.T) { ... })
|
||||
```
|
||||
|
||||
### Test File Conventions
|
||||
|
||||
- File: `<package>_test.go` in the same directory
|
||||
- Package: `package foo_test` for black-box testing (preferred), `package foo` for white-box
|
||||
- Helpers: use `t.Helper()` so stack traces point to the caller, not the helper
|
||||
|
||||
```go
|
||||
func assertNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
go test ./... # all packages
|
||||
go test -run TestFoo ./pkg/... # specific test
|
||||
go test -run TestFoo/subtest ./... # specific subtest
|
||||
go test -race ./... # race detector (always run before commit)
|
||||
go test -cover ./... # coverage
|
||||
go test -v ./... # verbose
|
||||
```
|
||||
|
||||
### testify
|
||||
|
||||
Pre-approved. Use `assert` (continues on failure) and `require` (stops on failure):
|
||||
|
||||
```go
|
||||
require.NoError(t, err) // fatal if error
|
||||
assert.Equal(t, expected, got) // non-fatal comparison
|
||||
assert.ErrorIs(t, err, ErrFoo) // error chain check
|
||||
```
|
||||
|
||||
## Good Tests
|
||||
|
||||
| Quality | Good | Bad |
|
||||
|---------|------|-----|
|
||||
| **Minimal** | One thing. "and" in name? Split it. | `TestValidatesEmailAndDomainAndWhitespace` |
|
||||
| **Clear** | Name describes behavior | `TestFoo`, `Test1` |
|
||||
| **Shows intent** | Demonstrates desired API | Obscures what code should do |
|
||||
| **Table-driven** | Multiple cases, one test function | Copy-pasted test functions |
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
|
||||
| "I'll test after" | Tests passing immediately prove nothing. |
|
||||
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
|
||||
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
|
||||
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
|
||||
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
|
||||
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
|
||||
| "Test hard = design unclear" | Listen to the test. Hard to test = hard to use. |
|
||||
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
|
||||
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
|
||||
|
||||
## Red Flags - STOP and Start Over
|
||||
|
||||
- Code before test
|
||||
- Test after implementation
|
||||
- Test passes immediately without seeing it fail
|
||||
- Can't explain why test failed
|
||||
- Tests added "later"
|
||||
- Rationalizing "just this once"
|
||||
- "Already manually tested it"
|
||||
- "Tests after achieve the same purpose"
|
||||
- "Keep as reference" or "adapt existing code"
|
||||
- "Already spent X hours, deleting is wasteful"
|
||||
- "TDD is dogmatic, I'm being pragmatic"
|
||||
- "This is different because..."
|
||||
|
||||
**All of these mean: Delete code. Start over with TDD.**
|
||||
|
||||
## Example: Bug Fix
|
||||
|
||||
**Bug:** Empty email accepted
|
||||
|
||||
**RED**
|
||||
```go
|
||||
func TestSubmitForm_RejectsEmptyEmail(t *testing.T) {
|
||||
result := submitForm(FormData{Email: ""})
|
||||
assert.Equal(t, "email required", result.Error)
|
||||
}
|
||||
```
|
||||
|
||||
**Verify RED**
|
||||
```bash
|
||||
$ go test -run TestSubmitForm_RejectsEmptyEmail ./...
|
||||
FAIL: expected "email required", got ""
|
||||
```
|
||||
|
||||
**GREEN**
|
||||
```go
|
||||
func submitForm(data FormData) FormResult {
|
||||
if strings.TrimSpace(data.Email) == "" {
|
||||
return FormResult{Error: "email required"}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Verify GREEN**
|
||||
```bash
|
||||
$ go test ./...
|
||||
ok example.com/myapp 0.003s
|
||||
```
|
||||
|
||||
**REFACTOR**
|
||||
Extract validation for multiple fields if needed.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking work complete:
|
||||
|
||||
- [ ] Every new function/method has a test
|
||||
- [ ] Watched each test fail before implementing
|
||||
- [ ] Each test failed for expected reason (feature missing, not typo)
|
||||
- [ ] Wrote minimal code to pass each test
|
||||
- [ ] All tests pass: `go test ./...`
|
||||
- [ ] Race detector clean: `go test -race ./...`
|
||||
- [ ] Tests use real code (mocks only if crossing a system boundary)
|
||||
- [ ] Edge cases and errors covered
|
||||
|
||||
Can't check all boxes? You skipped TDD. Start over.
|
||||
|
||||
## When Stuck
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Don't know how to test | Write wished-for API. Write assertion first. Ask Mathias. |
|
||||
| Test too complicated | Design too complicated. Simplify interface. |
|
||||
| Must mock everything | Code too coupled. Use dependency injection. |
|
||||
| Test setup huge | Extract helpers with `t.Helper()`. Still complex? Simplify design. |
|
||||
|
||||
## Debugging Integration
|
||||
|
||||
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
|
||||
|
||||
Never fix bugs without a test.
|
||||
|
||||
## Testing Anti-Patterns
|
||||
|
||||
When adding test utilities or mocks, load the `references/testing-anti-patterns.md` to avoid:
|
||||
- Testing mock behavior instead of real behavior
|
||||
- Adding test-only methods to production types
|
||||
- Mocking without understanding what the dependency does
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
The brain MCP exposes session context across machines. Use it to make TDD cycles cumulative rather than one-shot.
|
||||
|
||||
**At session start:**
|
||||
- Run `brain_query` with the feature name + "tdd" to surface prior cycles, anti-patterns, or testing decisions for this code area. Skip if the feature is brand new.
|
||||
|
||||
**Never:**
|
||||
- Embed brain content in test code or assertions. The brain is context for you, not state for the system under test.
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||
|
||||
**At end of `red` phase:**
|
||||
- `session_log` with `{skill: "tdd", phase: "red", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**At end of `green` phase:**
|
||||
- `session_log` with `{skill: "tdd", phase: "green", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**At end of `refactor` phase:**
|
||||
- `session_log` with `{skill: "tdd", phase: "refactor", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached (red: test fails as expected; green: test passes; refactor: tests still pass after refactor).
|
||||
- `fail` — the phase's intended outcome was NOT reached (test compiled when it shouldn't, test still fails after green attempt, refactor broke tests).
|
||||
- `skip` — phase was skipped intentionally (e.g. refactor not warranted).
|
||||
|
||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future `tdd` call to a local model. If your skill never logs, the routing pod sees no data and may default-route or default-not-route in a way that doesn't reflect real performance.
|
||||
|
||||
## Final Rule
|
||||
|
||||
```
|
||||
Production code → test exists and failed first
|
||||
Otherwise → not TDD
|
||||
```
|
||||
|
||||
No exceptions without Mathias's permission.
|
||||
|
||||
## Mode 2 Routing Note
|
||||
|
||||
This skill is invoked identically whether the agent is running in Mode 1 (cloud Claude, no routing) or Mode 2 (client-local, supervisor routing layer). The routing pod (Plan 6) does not exist yet; until it does, treat this skill as Mode 1 only. The discipline does not change between modes — only the model behind the call.
|
||||
@@ -0,0 +1,299 @@
|
||||
# Testing Anti-Patterns
|
||||
|
||||
**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code.
|
||||
|
||||
## Overview
|
||||
|
||||
Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
|
||||
|
||||
**Core principle:** Test what the code does, not what the mocks do.
|
||||
|
||||
**Following strict TDD prevents these anti-patterns.**
|
||||
|
||||
## The Iron Laws
|
||||
|
||||
```
|
||||
1. NEVER test mock behavior
|
||||
2. NEVER add test-only methods to production classes
|
||||
3. NEVER mock without understanding dependencies
|
||||
```
|
||||
|
||||
## Anti-Pattern 1: Testing Mock Behavior
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Testing that the mock exists
|
||||
test('renders sidebar', () => {
|
||||
render(<Page />);
|
||||
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- You're verifying the mock works, not that the component works
|
||||
- Test passes when mock is present, fails when it's not
|
||||
- Tells you nothing about real behavior
|
||||
|
||||
**your human partner's correction:** "Are we testing the behavior of a mock?"
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Test real component or don't mock it
|
||||
test('renders sidebar', () => {
|
||||
render(<Page />); // Don't mock sidebar
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// OR if sidebar must be mocked for isolation:
|
||||
// Don't assert on the mock - test Page's behavior with sidebar present
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE asserting on any mock element:
|
||||
Ask: "Am I testing real component behavior or just mock existence?"
|
||||
|
||||
IF testing mock existence:
|
||||
STOP - Delete the assertion or unmock the component
|
||||
|
||||
Test real behavior instead
|
||||
```
|
||||
|
||||
## Anti-Pattern 2: Test-Only Methods in Production
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: destroy() only used in tests
|
||||
class Session {
|
||||
async destroy() { // Looks like production API!
|
||||
await this._workspaceManager?.destroyWorkspace(this.id);
|
||||
// ... cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// In tests
|
||||
afterEach(() => session.destroy());
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Production class polluted with test-only code
|
||||
- Dangerous if accidentally called in production
|
||||
- Violates YAGNI and separation of concerns
|
||||
- Confuses object lifecycle with entity lifecycle
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Test utilities handle test cleanup
|
||||
// Session has no destroy() - it's stateless in production
|
||||
|
||||
// In test-utils/
|
||||
export async function cleanupSession(session: Session) {
|
||||
const workspace = session.getWorkspaceInfo();
|
||||
if (workspace) {
|
||||
await workspaceManager.destroyWorkspace(workspace.id);
|
||||
}
|
||||
}
|
||||
|
||||
// In tests
|
||||
afterEach(() => cleanupSession(session));
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE adding any method to production class:
|
||||
Ask: "Is this only used by tests?"
|
||||
|
||||
IF yes:
|
||||
STOP - Don't add it
|
||||
Put it in test utilities instead
|
||||
|
||||
Ask: "Does this class own this resource's lifecycle?"
|
||||
|
||||
IF no:
|
||||
STOP - Wrong class for this method
|
||||
```
|
||||
|
||||
## Anti-Pattern 3: Mocking Without Understanding
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Mock breaks test logic
|
||||
test('detects duplicate server', () => {
|
||||
// Mock prevents config write that test depends on!
|
||||
vi.mock('ToolCatalog', () => ({
|
||||
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
|
||||
await addServer(config);
|
||||
await addServer(config); // Should throw - but won't!
|
||||
});
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Mocked method had side effect test depended on (writing config)
|
||||
- Over-mocking to "be safe" breaks actual behavior
|
||||
- Test passes for wrong reason or fails mysteriously
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Mock at correct level
|
||||
test('detects duplicate server', () => {
|
||||
// Mock the slow part, preserve behavior test needs
|
||||
vi.mock('MCPServerManager'); // Just mock slow server startup
|
||||
|
||||
await addServer(config); // Config written
|
||||
await addServer(config); // Duplicate detected ✓
|
||||
});
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE mocking any method:
|
||||
STOP - Don't mock yet
|
||||
|
||||
1. Ask: "What side effects does the real method have?"
|
||||
2. Ask: "Does this test depend on any of those side effects?"
|
||||
3. Ask: "Do I fully understand what this test needs?"
|
||||
|
||||
IF depends on side effects:
|
||||
Mock at lower level (the actual slow/external operation)
|
||||
OR use test doubles that preserve necessary behavior
|
||||
NOT the high-level method the test depends on
|
||||
|
||||
IF unsure what test depends on:
|
||||
Run test with real implementation FIRST
|
||||
Observe what actually needs to happen
|
||||
THEN add minimal mocking at the right level
|
||||
|
||||
Red flags:
|
||||
- "I'll mock this to be safe"
|
||||
- "This might be slow, better mock it"
|
||||
- Mocking without understanding the dependency chain
|
||||
```
|
||||
|
||||
## Anti-Pattern 4: Incomplete Mocks
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Partial mock - only fields you think you need
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: 'Alice' }
|
||||
// Missing: metadata that downstream code uses
|
||||
};
|
||||
|
||||
// Later: breaks when code accesses response.metadata.requestId
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- **Partial mocks hide structural assumptions** - You only mocked fields you know about
|
||||
- **Downstream code may depend on fields you didn't include** - Silent failures
|
||||
- **Tests pass but integration fails** - Mock incomplete, real API complete
|
||||
- **False confidence** - Test proves nothing about real behavior
|
||||
|
||||
**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Mirror real API completeness
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: 'Alice' },
|
||||
metadata: { requestId: 'req-789', timestamp: 1234567890 }
|
||||
// All fields real API returns
|
||||
};
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE creating mock responses:
|
||||
Check: "What fields does the real API response contain?"
|
||||
|
||||
Actions:
|
||||
1. Examine actual API response from docs/examples
|
||||
2. Include ALL fields system might consume downstream
|
||||
3. Verify mock matches real response schema completely
|
||||
|
||||
Critical:
|
||||
If you're creating a mock, you must understand the ENTIRE structure
|
||||
Partial mocks fail silently when code depends on omitted fields
|
||||
|
||||
If uncertain: Include all documented fields
|
||||
```
|
||||
|
||||
## Anti-Pattern 5: Integration Tests as Afterthought
|
||||
|
||||
**The violation:**
|
||||
```
|
||||
✅ Implementation complete
|
||||
❌ No tests written
|
||||
"Ready for testing"
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Testing is part of implementation, not optional follow-up
|
||||
- TDD would have caught this
|
||||
- Can't claim complete without tests
|
||||
|
||||
**The fix:**
|
||||
```
|
||||
TDD cycle:
|
||||
1. Write failing test
|
||||
2. Implement to pass
|
||||
3. Refactor
|
||||
4. THEN claim complete
|
||||
```
|
||||
|
||||
## When Mocks Become Too Complex
|
||||
|
||||
**Warning signs:**
|
||||
- Mock setup longer than test logic
|
||||
- Mocking everything to make test pass
|
||||
- Mocks missing methods real components have
|
||||
- Test breaks when mock changes
|
||||
|
||||
**your human partner's question:** "Do we need to be using a mock here?"
|
||||
|
||||
**Consider:** Integration tests with real components often simpler than complex mocks
|
||||
|
||||
## TDD Prevents These Anti-Patterns
|
||||
|
||||
**Why TDD helps:**
|
||||
1. **Write test first** → Forces you to think about what you're actually testing
|
||||
2. **Watch it fail** → Confirms test tests real behavior, not mocks
|
||||
3. **Minimal implementation** → No test-only methods creep in
|
||||
4. **Real dependencies** → You see what the test actually needs before mocking
|
||||
|
||||
**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Anti-Pattern | Fix |
|
||||
|--------------|-----|
|
||||
| Assert on mock elements | Test real component or unmock it |
|
||||
| Test-only methods in production | Move to test utilities |
|
||||
| Mock without understanding | Understand dependencies first, mock minimally |
|
||||
| Incomplete mocks | Mirror real API completely |
|
||||
| Tests as afterthought | TDD - tests first |
|
||||
| Over-complex mocks | Consider integration tests |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Assertion checks for `*-mock` test IDs
|
||||
- Methods only called in test files
|
||||
- Mock setup is >50% of test
|
||||
- Test fails when you remove mock
|
||||
- Can't explain why mock is needed
|
||||
- Mocking "just to be safe"
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**Mocks are tools to isolate, not things to test.**
|
||||
|
||||
If TDD reveals you're testing mock behavior, you've gone wrong.
|
||||
|
||||
Fix: Test real behavior or question why you're mocking at all.
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: telos-load
|
||||
description: Load TELOS — the intention substrate (problems, goals, strategies, status) — into a session at start so every decision traces to a goal. Use when starting any koala session, before architectural decisions, or as the CAD pipeline entry point ("load TELOS", "what are we optimizing toward?").
|
||||
---
|
||||
|
||||
# telos-load
|
||||
|
||||
Load TELOS — the intention substrate — into a session so every decision
|
||||
can be traced back to a goal, and every goal to a problem.
|
||||
|
||||
**Use when:** starting any session on koala (Claude Code, Crush,
|
||||
Antigravity), or whenever a session needs to know what we are optimizing
|
||||
toward. If you find yourself making architectural decisions without knowing
|
||||
the active goals, load TELOS first.
|
||||
|
||||
**Do not skip:** a session without TELOS context is flying blind. It may
|
||||
produce technically correct output that is strategically wrong.
|
||||
|
||||
## What TELOS is
|
||||
|
||||
TELOS is a wing in brain (wiki/telos/decisions/) containing 9 files:
|
||||
PROBLEMS, MISSION, GOALS, CHALLENGES, PROJECTS, STRATEGIES, BELIEFS,
|
||||
WRONG, STATUS. Together they answer: what are we working against, what
|
||||
are we trying to build, and how do we approach the work?
|
||||
|
||||
Full aggregate: wiki/telos/decisions/principal-telos.md
|
||||
|
||||
## Loading TELOS by harness
|
||||
|
||||
### Claude Code (per-project CLAUDE.md)
|
||||
|
||||
Add to the project CLAUDE.md or ~/.claude/CLAUDE.md:
|
||||
|
||||
## Intention context (TELOS)
|
||||
At session start, call brain_context with wing=telos and limit=8.
|
||||
Fallback if brain MCP unavailable:
|
||||
wiki/telos/decisions/principal-telos.md in the brain repo.
|
||||
|
||||
The global ~/.claude/CLAUDE.md was wired on koala on 2026-06-16.
|
||||
|
||||
### Crush
|
||||
|
||||
Location on koala: ~/.config/crush/CRUSH.md (auto-loaded via
|
||||
global_context_paths). Add:
|
||||
|
||||
## Intention context (TELOS)
|
||||
At session start: query brain MCP with wing=telos, limit=8.
|
||||
If brain MCP unavailable: read wiki/telos/decisions/principal-telos.md
|
||||
|
||||
### Antigravity
|
||||
|
||||
Add @brain_context wing=telos directive at top of system instructions.
|
||||
|
||||
### Fallback — no brain MCP
|
||||
|
||||
cat ~/dev/AI/brain/wiki/telos/decisions/principal-telos.md
|
||||
|
||||
Or @-import in CLAUDE.md:
|
||||
@~/dev/AI/brain/wiki/telos/decisions/principal-telos.md
|
||||
|
||||
## Verification
|
||||
|
||||
brain_query wing=telos limit=3
|
||||
→ Expected: PROBLEMS, MISSION, GOALS returned
|
||||
|
||||
brain_answer "what am I currently optimizing toward?"
|
||||
→ Expected: non-empty, telos-sourced
|
||||
→ If empty: use brain_query wing=telos (known fallback, brain#11 fixed)
|
||||
|
||||
## When TELOS is stale
|
||||
|
||||
Update STATUS.md via:
|
||||
brain_write wing=telos hall=decisions filename=STATUS
|
||||
|
||||
## Relationship to CAD
|
||||
|
||||
TELOS is the intention layer in the CAD pipeline. Every Gitea issue
|
||||
created in a CAD session should trace to a TELOS GOAL. Every GOAL
|
||||
traces to a PROBLEM.
|
||||
|
||||
Traceability chain: PROBLEM → GOAL → SPEC → TICKET → IMPL → TEST
|
||||
@@ -0,0 +1,308 @@
|
||||
---
|
||||
name: test-design
|
||||
description: Evaluate test quality using Dave Farley's 8 Properties of Good Tests. Use when reviewing or writing tests to ensure they provide genuine verification.
|
||||
---
|
||||
|
||||
# Test Design
|
||||
|
||||
## Overview
|
||||
|
||||
Good tests are investments. Bad tests are liabilities — they pass when they shouldn't, fail when code is correct, or verify nothing meaningful.
|
||||
|
||||
This skill uses Dave Farley's 8 Properties of Good Tests to assess and improve test quality. The **Farley Index** (0–10) provides a scored summary.
|
||||
|
||||
Reference: [Dave Farley's Properties of Good Tests](https://www.linkedin.com/pulse/tdd-properties-good-tests-dave-farley-iexge/)
|
||||
|
||||
## The 8 Properties
|
||||
|
||||
| Property | Weight | What it measures |
|
||||
|----------|--------|-----------------|
|
||||
| **Understandable** | 1.5x | Can a reader understand what behavior is being tested? |
|
||||
| **Maintainable** | 1.5x | Will small code changes cause test failures unrelated to behavior? |
|
||||
| **Repeatable** | 1.25x | Same result every time, regardless of environment or order |
|
||||
| **Atomic** | 1.0x | One behavior per test; tests are independent |
|
||||
| **Necessary** | 1.0x | Tests real behavior, not mock internals or framework behavior |
|
||||
| **Granular** | 1.0x | Each test covers one specific case |
|
||||
| **Fast** | 0.75x | Tests run quickly enough to support rapid TDD cycles |
|
||||
| **First (TDD)** | 1.0x | Tests were written before implementation |
|
||||
|
||||
**Farley Index formula:** `(U×1.5 + M×1.5 + R×1.25 + A×1.0 + N×1.0 + G×1.0 + F×0.75 + T×1.0) / 9.0`
|
||||
|
||||
## Rating Scale
|
||||
|
||||
| Score | Rating | Interpretation |
|
||||
|-------|--------|----------------|
|
||||
| 9.0–10.0 | Exemplary | Model quality; tests serve as living documentation |
|
||||
| 7.5–8.9 | Excellent | High quality with minor improvement opportunities |
|
||||
| 6.0–7.4 | Good | Solid foundation with clear improvement areas |
|
||||
| 4.5–5.9 | Fair | Functional but needs significant attention |
|
||||
| 3.0–4.4 | Poor | Tests provide limited value; refactoring needed |
|
||||
| 0.0–2.9 | Critical | Tests may be harmful; consider rewriting |
|
||||
|
||||
## Property Deep Dives
|
||||
|
||||
### Understandable (U)
|
||||
|
||||
A test should tell a story: what behavior, under what conditions, produces what result.
|
||||
|
||||
**Go patterns that help:**
|
||||
- Subtest names in `t.Run`: `t.Run("returns error when email is empty", ...)`
|
||||
- Table-driven tests with descriptive `name` fields
|
||||
- Arrange-Act-Assert structure with blank lines separating sections
|
||||
|
||||
```go
|
||||
// Good: clear behavior name, clear structure
|
||||
func TestValidateUser_RejectsEmptyEmail(t *testing.T) {
|
||||
// Arrange
|
||||
user := User{Name: "Alice", Email: ""}
|
||||
|
||||
// Act
|
||||
err := ValidateUser(user)
|
||||
|
||||
// Assert
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidEmail)
|
||||
}
|
||||
|
||||
// Bad: cryptic name, no structure
|
||||
func TestUser1(t *testing.T) {
|
||||
u := User{}
|
||||
assert.NotNil(t, ValidateUser(u))
|
||||
}
|
||||
```
|
||||
|
||||
**Negative signals:** cryptic names (`test_1`, `TestFoo`), no AAA structure, multiple behaviors in one test.
|
||||
|
||||
### Maintainable (M)
|
||||
|
||||
Tests that break when implementation changes (but behavior doesn't) create noise and slow down development.
|
||||
|
||||
**Negative signals:**
|
||||
- Over-specified mock interactions (`assert.Called(mock, "MethodX", args...)` when behavior is all that matters)
|
||||
- ArgumentCaptor deep inspection
|
||||
- `verifyNoMoreInteractions` that breaks when you add a logging call
|
||||
- Tests coupled to internal field names
|
||||
|
||||
**Go patterns that help:**
|
||||
- Test behavior via public API, not internal state
|
||||
- Avoid asserting on exact call counts unless the count IS the behavior
|
||||
|
||||
```go
|
||||
// Bad: breaks when you add an audit log call
|
||||
mock.AssertCalled(t, "Save", user)
|
||||
mock.AssertNumberOfCalls(t, "Save", 1)
|
||||
mock.AssertNotCalled(t, "Log") // Breaks if you add logging later
|
||||
|
||||
// Good: test the outcome
|
||||
result, err := service.CreateUser(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.Email, result.Email)
|
||||
```
|
||||
|
||||
### Repeatable (R)
|
||||
|
||||
Tests must produce the same result regardless of when, where, or in what order they run.
|
||||
|
||||
**Negative signals (Go):**
|
||||
- `time.Now()` in test logic without injection
|
||||
- `os.ReadFile` for fixtures that aren't hermetic
|
||||
- Shared global state between tests
|
||||
- Tests that depend on network availability
|
||||
- `time.Sleep` for synchronization
|
||||
|
||||
**Go fixes:**
|
||||
```go
|
||||
// Bad: time-dependent
|
||||
func TestTokenExpiry(t *testing.T) {
|
||||
token := generateToken()
|
||||
time.Sleep(2 * time.Second)
|
||||
assert.True(t, token.IsExpired())
|
||||
}
|
||||
|
||||
// Good: inject clock
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
type FixedClock struct{ t time.Time }
|
||||
func (c FixedClock) Now() time.Time { return c.t }
|
||||
|
||||
func TestTokenExpiry(t *testing.T) {
|
||||
clock := FixedClock{t: time.Unix(0, 0)}
|
||||
token := generateTokenWithClock(clock)
|
||||
futureClk := FixedClock{t: time.Unix(3600, 0)}
|
||||
assert.True(t, token.IsExpiredAt(futureClk.Now()))
|
||||
}
|
||||
```
|
||||
|
||||
Use `t.TempDir()` for filesystem fixtures — cleaned up automatically.
|
||||
|
||||
### Atomic (A)
|
||||
|
||||
One test = one behavior. Tests must be independent — running in any order must produce the same result.
|
||||
|
||||
**Go patterns:**
|
||||
- `t.Parallel()` on subtests forces isolation
|
||||
- Fresh state in each `t.Run`
|
||||
- No `init()` or package-level setup that leaks between tests
|
||||
|
||||
```go
|
||||
func TestUserService(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input CreateUserReq
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid user", validReq, false},
|
||||
{"duplicate email", dupEmailReq, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel() // Each subtest runs independently
|
||||
store := NewInMemoryStore() // Fresh state per test
|
||||
svc := NewUserService(store)
|
||||
_, err := svc.Create(context.Background(), tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Necessary (N)
|
||||
|
||||
Tests must verify real behavior. Tautology Theatre — tests whose outcome is predetermined regardless of production code — provides false confidence.
|
||||
|
||||
**Types of Tautology Theatre:**
|
||||
|
||||
1. **Mock tautology:** Configure mock return, then assert that mock returns it.
|
||||
```go
|
||||
// Bad: this passes even if production code is deleted
|
||||
mockStore.On("GetUser", id).Return(user, nil)
|
||||
result, _ := mockStore.GetUser(id)
|
||||
assert.Equal(t, user, result) // Testing the mock, not production code
|
||||
```
|
||||
|
||||
2. **Mock-only test:** Every object is a mock; no real class instantiated.
|
||||
|
||||
3. **Trivial tautology:** `assert.True(t, true)` or `assert.NotNil(t, new(User))`
|
||||
|
||||
4. **Framework test:** Verifying that Go's `make(map[string]int)` returns non-nil.
|
||||
|
||||
**Fix:** Test real behavior through real implementations. Use mocks only to isolate from external systems (DB, HTTP, filesystem).
|
||||
|
||||
### Granular (G)
|
||||
|
||||
Each test covers one specific case. Table-driven tests in Go are the natural expression of granularity.
|
||||
|
||||
```go
|
||||
// Good: each row is one case, each can fail independently
|
||||
func TestParseAmount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want Amount
|
||||
wantErr bool
|
||||
}{
|
||||
{"integer", "100", Amount{Value: 100}, false},
|
||||
{"decimal", "10.50", Amount{Value: 1050, Scale: 2}, false},
|
||||
{"negative", "-5", Amount{}, true},
|
||||
{"empty", "", Amount{}, true},
|
||||
{"non-numeric", "abc", Amount{}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseAmount(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fast (F)
|
||||
|
||||
Tests must run fast enough to support TDD cycles. Target: the full test suite in < 30 seconds for most projects.
|
||||
|
||||
**Go fixes:**
|
||||
- Mark slow integration tests with build tags: `//go:build integration`
|
||||
- Use `t.Parallel()` to parallelize safe tests
|
||||
- Use `InMemoryStore` implementations instead of real DB for unit tests
|
||||
- Use `httptest.NewServer` for HTTP tests instead of real servers
|
||||
|
||||
```bash
|
||||
# Unit tests only (fast, default)
|
||||
go test ./...
|
||||
|
||||
# Integration tests (slower, explicit)
|
||||
go test -tags=integration ./...
|
||||
```
|
||||
|
||||
**Negative signals:** `time.Sleep`, network calls without build tags, database calls in unit tests.
|
||||
|
||||
### First / TDD (T)
|
||||
|
||||
Evidence that tests were written before implementation. This is the hardest property to verify statically.
|
||||
|
||||
**Positive signals:**
|
||||
- Commit history shows test commit before implementation commit
|
||||
- Tests test behavior, not implementation details (tests-first forces API design)
|
||||
- Tests are simpler than the implementation (tests-first keeps tests focused)
|
||||
|
||||
**Negative signals:**
|
||||
- Tests that exactly mirror the implementation structure
|
||||
- Tests that only cover happy paths (implementation-first misses edge cases)
|
||||
- Tests added in the same commit as a large implementation
|
||||
|
||||
## Go-Specific Test Design Notes
|
||||
|
||||
### t.Helper()
|
||||
|
||||
Use `t.Helper()` in helper functions so stack traces point to the call site, not the helper:
|
||||
|
||||
```go
|
||||
func assertValidUser(t *testing.T, u User) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, u.ID)
|
||||
assert.NotEmpty(t, u.Email)
|
||||
}
|
||||
```
|
||||
|
||||
### Table-Driven Tests Are Preferred
|
||||
|
||||
Go convention is table-driven tests. They're granular, readable, and easy to extend:
|
||||
- Add a new case by adding a row to the table — no new test function
|
||||
- Each case can be run independently: `go test -run TestFoo/case_name`
|
||||
|
||||
### Subtests Enable Targeted Runs
|
||||
|
||||
```bash
|
||||
go test -run TestValidateUser/rejects_empty_email ./...
|
||||
```
|
||||
|
||||
## When Writing Tests
|
||||
|
||||
Apply this checklist to every new test:
|
||||
|
||||
- [ ] Name describes the behavior being tested (not the function name)
|
||||
- [ ] Structure follows Arrange-Act-Assert
|
||||
- [ ] Tests one behavior (no "and" in the name)
|
||||
- [ ] Uses real implementations where feasible
|
||||
- [ ] Runs in < 100ms (or tagged for integration)
|
||||
- [ ] Uses `t.Helper()` in helper functions
|
||||
- [ ] Table-driven if testing multiple similar inputs
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Load `tdd` skill for the full TDD workflow
|
||||
- Load `code-review` skill for test quality review during pre-merge review
|
||||
- See `clean-code/references/code-smells.md` for testing-specific smells
|
||||
@@ -0,0 +1,252 @@
|
||||
---
|
||||
name: trainer
|
||||
description: Two-phase brain curation — scan session logs to score candidate learnings (READ), then write the ones that pass the quality gate as generalized knowledge entries (WRITE). Use periodically to keep the brain useful, not noisy.
|
||||
---
|
||||
|
||||
# Trainer
|
||||
|
||||
## Overview
|
||||
|
||||
The trainer is the brain's quality gate. It runs in two phases:
|
||||
|
||||
- **READ phase:** scan session logs, score candidate learnings 1-5, output the candidate list.
|
||||
- **WRITE phase:** take candidates that score ≥3, apply a quality gate (must generalize, no project-specific identifiers, must be clear cold), emit knowledge entries.
|
||||
|
||||
`session-retrospective` surfaces candidates loosely. The trainer is stricter — it produces only generalized, reusable knowledge, the kind that earns its place in long-term memory.
|
||||
|
||||
**Core principle:** If a knowledge entry only makes sense to someone who was in the session, it does not belong in the brain.
|
||||
|
||||
## Iron Laws
|
||||
|
||||
1. **Do not call `brain_write` from this skill.** Emit entries; the operator decides which to commit.
|
||||
2. **Apply the quality gate before emitting any entry.** A skipped gate produces brain pollution.
|
||||
3. **No project-specific paths, identifiers, or function names in entries.** If you cannot generalize the lesson, drop the entry.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Periodically (weekly, or after a stretch of intense work) — not after every session
|
||||
- When `session-retrospective` has produced too many candidates to keep — trainer filters them
|
||||
- When the brain is becoming noisy and needs curation pruning before adding more
|
||||
|
||||
**Do not use for:**
|
||||
- Single sessions with one or two clear learnings (use `session-retrospective` directly)
|
||||
- Project-specific decisions (those belong in CLAUDE.md, not the brain)
|
||||
|
||||
## Phase READ — Scan and Score
|
||||
|
||||
### What to Look For
|
||||
|
||||
- **Patterns that worked** — the approach was clean, correct, and worth reinforcing for future sessions
|
||||
- **Corrections** — something was first done wrong, then corrected. Both sides are valuable: the rationalization that led to the wrong path, and the correction that fixed it.
|
||||
|
||||
### Scoring (1-5)
|
||||
|
||||
| Score | Criteria |
|
||||
|---|---|
|
||||
| 5 | Novel pattern, clearly correct, generalizes across projects and domains |
|
||||
| 4 | Good pattern, correct, somewhat project-specific but still broadly useful |
|
||||
| 3 | Correct but obvious — include only if especially clean or pedagogically useful |
|
||||
| 2 | Trivial or already widely known — skip |
|
||||
| 1 | Project-specific noise, mechanical fix, or single-use detail — skip |
|
||||
|
||||
Anything below 3 is dropped. Anything at 3 is borderline — include only if the operator might benefit from the framing.
|
||||
|
||||
### READ Output Format
|
||||
|
||||
Respond in markdown:
|
||||
|
||||
```
|
||||
**Candidate N (score: X/5, type: pattern|correction)**
|
||||
- **What happened:** Brief description of the learning moment
|
||||
- **Why it's valuable:** What makes this worth preserving
|
||||
- **Key insight:** The distilled lesson in one sentence
|
||||
```
|
||||
|
||||
End with: `N candidates found (M scoring ≥ 3)` — the WRITE phase will use the ≥3 candidates.
|
||||
|
||||
## Phase WRITE — Emit Knowledge Entries
|
||||
|
||||
### Quality Gate
|
||||
|
||||
Apply BEFORE writing each entry. Drop the entry if any check fails:
|
||||
|
||||
- **Lesson** is phrased so it could apply to any project, not just this one — no project-specific paths, variable names, function names, or repo identifiers
|
||||
- **When it applies** may name a stack or tool family (e.g. "Go projects with golangci-lint") to scope the relevance, but must not name specific files or functions
|
||||
- **Example** may use concrete tool names, language syntax, or commands to make the point land — concrete examples are pedagogically necessary; only the Lesson and When-it-applies sections need to generalize
|
||||
- The insight is clearly stated enough that someone reading it cold (no session context) understands it
|
||||
- The lesson is not already in `CLAUDE.md` or an existing memory file
|
||||
|
||||
### WRITE Output Format
|
||||
|
||||
For each candidate that passes the quality gate:
|
||||
|
||||
```
|
||||
# [Topic]
|
||||
|
||||
## Lesson
|
||||
|
||||
[The key insight in 1-3 sentences. Generalized — no project-specific names.]
|
||||
|
||||
## When it applies
|
||||
|
||||
[Conditions under which this pattern is relevant.]
|
||||
|
||||
## Example
|
||||
|
||||
[A brief, generic example illustrating the lesson. Pseudocode or
|
||||
language-agnostic prose preferred over a copy of the original code.]
|
||||
```
|
||||
|
||||
After all entries, end with: `N entries ready for brain_write` or `0 entries passed quality gate — <reason>`.
|
||||
|
||||
The operator (or a calling agent) decides whether to actually run `brain_write` on each entry. This skill does not write directly.
|
||||
|
||||
## Worked Example
|
||||
|
||||
### READ phase output
|
||||
|
||||
```
|
||||
**Candidate 1 (score: 4/5, type: correction)**
|
||||
- **What happened:** Implementer subagent reported "tests pass" after running `go test ./...`, but CI failed on a `defer resp.Body.Close()` errcheck violation 30 minutes later.
|
||||
- **Why it's valuable:** Captures a specific gap between local verification and CI verification that is easy to repeat across Go projects.
|
||||
- **Key insight:** Per-task verification must include the linter, not just the test runner.
|
||||
|
||||
**Candidate 2 (score: 5/5, type: pattern)**
|
||||
- **What happened:** Repaired malformed JSON from LLM output by stripping invalid escape sequences before parsing. Generalizes well — most LLM-as-JSON pipelines need this.
|
||||
- **Why it's valuable:** Pattern works across any LLM-as-JSON ingestion pipeline.
|
||||
- **Key insight:** Treat LLM JSON output as untrusted input that may need normalization before parsing.
|
||||
|
||||
**Candidate 3 (score: 3/5, type: pattern)**
|
||||
- **What happened:** Implementer subagent saved its commit SHA in the structured report so the spec reviewer could verify against a stable revision.
|
||||
- **Why it's valuable:** Obvious in hindsight, but cleanly framed — passing identifiers between subagents avoids race conditions when the file changes mid-review.
|
||||
- **Key insight:** Subagent reports should include verifiable identifiers (commit SHAs, file hashes) the reviewer can pin against.
|
||||
|
||||
**Candidate 4 (score: 2/5, type: pattern)**
|
||||
- **What happened:** Renamed a local variable from `data` to `invoiceLineItems` for clarity.
|
||||
- **Why it's valuable:** It's good practice but trivial.
|
||||
- **Key insight:** Use names that describe what the data is.
|
||||
|
||||
4 candidates found (3 scoring ≥ 3)
|
||||
```
|
||||
|
||||
### WRITE phase output
|
||||
|
||||
```
|
||||
# Per-task verification must include lint
|
||||
|
||||
## Lesson
|
||||
|
||||
When dispatching tasks that include "tests pass" as a success criterion,
|
||||
the verification command must include the project's linter, not just the
|
||||
test runner. Tests verify behavior; linters catch policy violations
|
||||
(error handling, unused variables, formatting) that the test suite is
|
||||
blind to.
|
||||
|
||||
## When it applies
|
||||
|
||||
Any project with a CI lint gate, particularly Go projects with
|
||||
golangci-lint configured for errcheck or similar policies.
|
||||
|
||||
## Example
|
||||
|
||||
In a project with a Taskfile that chains lint + test, the per-task
|
||||
verification should be `task check`, not `go test ./...`. The latter
|
||||
will pass locally and fail in CI.
|
||||
|
||||
---
|
||||
|
||||
# LLM JSON output is untrusted input
|
||||
|
||||
## Lesson
|
||||
|
||||
Output from an LLM that is supposed to be JSON should be treated as
|
||||
untrusted input. It may contain invalid escape sequences (often from
|
||||
markdown-formatted content the LLM included), structurally valid but
|
||||
semantically wrong fields, or trailing prose. Normalize before parsing.
|
||||
|
||||
## When it applies
|
||||
|
||||
Any pipeline that calls an LLM with a JSON output contract and parses
|
||||
the response with a strict JSON parser.
|
||||
|
||||
## Example
|
||||
|
||||
Wrap the parser in a normalization step: strip invalid escape sequences,
|
||||
trim trailing prose after the closing brace, validate against the
|
||||
expected schema. Treat the LLM as a network boundary, not a function call.
|
||||
|
||||
---
|
||||
|
||||
# Subagent reports should carry verifiable identifiers
|
||||
|
||||
## Lesson
|
||||
|
||||
When a subagent reports completion of work that another agent will verify,
|
||||
the report must carry an identifier that pins the verifiable state — a
|
||||
commit SHA, file hash, or revision number. Without one, the verifier may
|
||||
read a different state than the subagent produced if the workspace
|
||||
changes between report and review.
|
||||
|
||||
## When it applies
|
||||
|
||||
Any multi-agent workflow with a verify-after-implement step, particularly
|
||||
when the workspace is shared (a worktree or branch held by multiple
|
||||
processes).
|
||||
|
||||
## Example
|
||||
|
||||
After committing changes, a subagent reports `Status: DONE, SHA: abc1234`.
|
||||
The verifier runs `git show abc1234` to read exactly the state the
|
||||
subagent produced, even if newer commits arrived in the interim.
|
||||
|
||||
3 entries ready for brain_write
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why It Fails |
|
||||
|---|---|
|
||||
| Score everything 4 or 5 | The gate exists for a reason. If everything is high-value, nothing is. |
|
||||
| Knowledge entries with project-specific paths | They will not match queries from other projects, defeating the brain's value. |
|
||||
| Writing entries directly without the quality gate | Polluted brain → useless brain. The gate is the whole point. |
|
||||
| Skipping READ and going straight to WRITE | The score is what determines whether to write. Without it, you are just transcribing. |
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
The brain holds prior curated knowledge entries. Querying it before READ prevents duplicate entries from cluttering the gate.
|
||||
|
||||
**Before READ:**
|
||||
- Run `brain_query` for the topics that appear in the session log. This prevents scoring candidates that are already in the brain (skip them — same content twice does not help).
|
||||
|
||||
**After WRITE:**
|
||||
- This skill does NOT call `brain_write`. The operator decides which entries to commit, then runs `brain_write` per accepted entry.
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||
|
||||
**At end of `read` phase:**
|
||||
- `session_log` with `{skill: "trainer", phase: "read", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**At end of `write` phase:**
|
||||
- `session_log` with `{skill: "trainer", phase: "write", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
||||
|
||||
**Phases for this skill:** read, write
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached (read: candidates scored; write: gate-passed entries emitted).
|
||||
- `fail` — the phase's intended outcome was NOT reached.
|
||||
- `skip` — phase was skipped intentionally (e.g. no candidates passed the gate, write skipped).
|
||||
|
||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
||||
|
||||
## Mode 2 Routing Note
|
||||
|
||||
Trainer is the same shape as `session-retrospective` — long-context reading, mechanical filtering — and is a Mode 2 routing candidate. Until Plan 6, treat as Mode 1 only.
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Load `session-retrospective` first if you want a looser, faster surfacing pass before the strict gate.
|
||||
- The brain's `brain_query` and `brain_write` tools are the read/write side of the brain that this skill curates.
|
||||
@@ -0,0 +1,246 @@
|
||||
---
|
||||
name: user-stories
|
||||
description: Decompose problems into thin, deliverable user stories using Elephant Carpaccio and INVEST criteria. Use after problem analysis, before planning and implementation.
|
||||
---
|
||||
|
||||
# User Stories
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
A user story is a placeholder for a conversation, not a specification. It captures who needs something, what they need, and why — in the smallest unit that still delivers end-to-end value.
|
||||
|
||||
The goal is **thin slices** — the Elephant Carpaccio technique. An elephant is too big to eat at once; slice it as thin as possible (carpaccio) so each slice is still valuable, testable, and deliverable.
|
||||
|
||||
## When to Use
|
||||
|
||||
- You have a problem analysis and need to break it into implementable units
|
||||
- A feature is too large to estimate or implement
|
||||
- You need to communicate scope and prioritize with a stakeholder
|
||||
- You need to discover the minimum viable version of a feature
|
||||
|
||||
**Input required:** A problem analysis (load `problem-analysis` skill first) or clear requirements.
|
||||
|
||||
## INVEST Criteria
|
||||
|
||||
Every user story must be:
|
||||
|
||||
| Criterion | Meaning |
|
||||
|-----------|---------|
|
||||
| **Independent** | Can be developed without requiring another story to be complete first |
|
||||
| **Negotiable** | Details can be discussed and refined |
|
||||
| **Valuable** | Delivers clear value to an end user (not just infrastructure) |
|
||||
| **Estimable** | Clear enough scope to understand complexity |
|
||||
| **Small** | Can be completed in a single sprint (1–2 weeks) |
|
||||
| **Testable** | Has clear, specific acceptance criteria |
|
||||
|
||||
## Story Format
|
||||
|
||||
```
|
||||
**Story Title:** [Descriptive name]
|
||||
|
||||
**As a** [user type / persona]
|
||||
**I want** [the capability or feature]
|
||||
**So that** [the business value or outcome]
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Given [starting context]
|
||||
When [action taken]
|
||||
Then [observable outcome]
|
||||
- Given [another context]
|
||||
When [action]
|
||||
Then [outcome]
|
||||
|
||||
**Definition of Done:**
|
||||
- [User-facing quality requirement]
|
||||
- [Business completion criterion]
|
||||
- [Performance/security requirement if applicable]
|
||||
```
|
||||
|
||||
## Elephant Carpaccio: Slicing Technique
|
||||
|
||||
The most common mistake is making stories too large. Use these slicing patterns to cut large stories thin.
|
||||
|
||||
### Slicing Pattern 1: By Data Variation
|
||||
|
||||
If a feature works differently for different data, make each variation its own story.
|
||||
|
||||
```
|
||||
Large story: "Users can search invoices"
|
||||
|
||||
Thin slices:
|
||||
1. "Users can search invoices by exact invoice number"
|
||||
2. "Users can search invoices by customer name (partial match)"
|
||||
3. "Users can search invoices by date range"
|
||||
4. "Users can combine multiple search criteria"
|
||||
```
|
||||
|
||||
### Slicing Pattern 2: By User Scenario
|
||||
|
||||
Different user journeys are separate stories.
|
||||
|
||||
```
|
||||
Large story: "Users can manage their account"
|
||||
|
||||
Thin slices:
|
||||
1. "New users can register with email and password"
|
||||
2. "Registered users can log in"
|
||||
3. "Logged-in users can update their display name"
|
||||
4. "Users can reset their password via email"
|
||||
```
|
||||
|
||||
### Slicing Pattern 3: By Error Condition
|
||||
|
||||
Happy path first, then error handling as a separate story.
|
||||
|
||||
```
|
||||
Story 1: "Users can upload a PDF invoice (happy path)"
|
||||
Story 2: "Users see a clear error when uploading non-PDF files"
|
||||
Story 3: "Users see a clear error when uploading files > 10MB"
|
||||
```
|
||||
|
||||
### Slicing Pattern 4: By Performance
|
||||
|
||||
Basic version first, then performance improvement as a separate story.
|
||||
|
||||
```
|
||||
Story 1: "Users can export invoice reports (any performance)"
|
||||
Story 2: "Invoice export completes in under 5 seconds for reports up to 1000 items"
|
||||
```
|
||||
|
||||
### Slicing Pattern 5: By Rules/Workflow
|
||||
|
||||
Each business rule is a candidate for its own story.
|
||||
|
||||
```
|
||||
Large story: "The system applies discounts to orders"
|
||||
|
||||
Thin slices:
|
||||
1. "Premium customers receive a 10% discount automatically"
|
||||
2. "Orders over 1000 SEK receive free shipping"
|
||||
3. "Discount codes can be applied at checkout"
|
||||
```
|
||||
|
||||
## Prioritization Framework
|
||||
|
||||
Order stories by:
|
||||
|
||||
1. **Risk reduction first** — stories that test unknown territory early
|
||||
2. **User value** — which story delivers the most value if we stopped here?
|
||||
3. **Learning** — stories that reveal information needed for later stories
|
||||
4. **Dependencies** — stories that others depend on come first
|
||||
|
||||
```
|
||||
Risk reduction: "Integrate with Bankgirot API (spike)"
|
||||
→ We don't know if this is feasible; find out first
|
||||
|
||||
User value: "Users can view their invoice list"
|
||||
→ Most basic valuable state; everything else builds on this
|
||||
|
||||
Foundation: "Authentication: users can log in"
|
||||
→ Many other stories depend on this
|
||||
```
|
||||
|
||||
## Story Decomposition Example
|
||||
|
||||
**Problem:** A financial automation tool needs to process invoices for payment.
|
||||
|
||||
**Too large:** "Process invoices for payment"
|
||||
|
||||
**Decomposed with Elephant Carpaccio:**
|
||||
|
||||
1. **Parse PDF invoice, extract total amount**
|
||||
- Given a valid Swedish PDF invoice
|
||||
- When I upload it to the system
|
||||
- Then the system displays the extracted amount and recipient
|
||||
- AC: Displays IBAN, amount, and due date for 10 sample invoices
|
||||
|
||||
2. **Queue a payment for approval**
|
||||
- Given a parsed invoice
|
||||
- When I click "Approve for payment"
|
||||
- Then the payment is queued with status "pending"
|
||||
- AC: Payment appears in pending list, can be viewed
|
||||
|
||||
3. **Execute a queued payment via Bankgirot**
|
||||
- Given a payment in "pending" status
|
||||
- When the batch window runs
|
||||
- Then an ISO 20022 payment instruction is submitted
|
||||
- AC: Submission confirmed, status changes to "submitted"
|
||||
|
||||
4. **View payment status after submission**
|
||||
- As a user I can see whether a submitted payment succeeded or failed
|
||||
- AC: Status reflects Bankgirot confirmation/rejection
|
||||
|
||||
5. **Handle rejected payments**
|
||||
- Given a submitted payment that is rejected by Bankgirot
|
||||
- When the rejection is received
|
||||
- Then the user sees a clear error message with the rejection reason
|
||||
- AC: Rejection reason from Bankgirot is displayed, payment status is "rejected"
|
||||
|
||||
Each slice is independently valuable and testable.
|
||||
|
||||
## Output Document
|
||||
|
||||
The output of this skill is a `user-stories.md` file:
|
||||
|
||||
```markdown
|
||||
# User Stories: [Feature Name]
|
||||
|
||||
## Personas
|
||||
- **[Persona 1]**: [Who they are and what they care about]
|
||||
- **[Persona 2]**: ...
|
||||
|
||||
## Stories (Prioritized)
|
||||
|
||||
### Phase 1: Foundation
|
||||
|
||||
#### Story 1: [Title]
|
||||
As a [persona]...
|
||||
|
||||
#### Story 2: [Title]
|
||||
...
|
||||
|
||||
### Phase 2: Core Features
|
||||
|
||||
#### Story 3: [Title]
|
||||
...
|
||||
|
||||
## Decomposition Rationale
|
||||
[Why stories were sliced this way; what dependencies exist]
|
||||
|
||||
## Stories Deferred to v2
|
||||
- [Story A]: [Reason]
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Technical tasks disguised as stories ("Set up database") | Reframe as user value ("Users can create an account") |
|
||||
| "And" in the title | Split into two stories |
|
||||
| No acceptance criteria | Every story must have testable criteria |
|
||||
| Stories depending on each other at the same priority level | Reorder so foundational stories come first |
|
||||
| Implementation details in acceptance criteria ("use PostgreSQL") | Keep criteria in user/business terms |
|
||||
|
||||
## Mathias Context
|
||||
|
||||
As a digital PM:
|
||||
- Stories should capture **business value**, not technical tasks
|
||||
- Stories are the bridge between problem analysis and technical implementation
|
||||
- When the business says "build feature X", decompose it into stories that can be delivered incrementally
|
||||
- A story with 10 acceptance criteria is not one story — it's many. Split it.
|
||||
- The goal is to find the thinnest slice that's still worth shipping
|
||||
|
||||
## Transition to Next Steps
|
||||
|
||||
After stories are written:
|
||||
|
||||
1. Load `spec-driven-dev` skill to write a technical specification
|
||||
2. Load `planning` skill to convert stories into an implementation plan
|
||||
3. Load `atdd` skill to implement stories with acceptance test-driven development
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Requires: `problem-analysis` skill output
|
||||
- Leads to: `planning` and `atdd` skills
|
||||
- INVEST criteria origin: Bill Wake
|
||||
- Elephant Carpaccio: Alistair Cockburn
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: web-shot
|
||||
description: Screenshot any running web UI by spawning a one-shot Playwright Job on the koala k3s cluster. Auto-detects the calling host's Tailscale name so a dev server on flamingo:8099, iguana:3000 or koala:8181 just works. Use when the user asks for a screenshot, visual verification, "what does the UI look like", or any time chromium / firefox / a browser is requested but none is installed on the host.
|
||||
---
|
||||
|
||||
# web-shot
|
||||
|
||||
Screenshot a locally-running web UI without installing a browser on the agent's host.
|
||||
|
||||
## Mechanism
|
||||
|
||||
1. `shot.sh` builds a k8s Job that runs `mcr.microsoft.com/playwright:v1.49.0-jammy` on koala's k3s cluster.
|
||||
2. `hostNetwork: true` is **not** used — the pod uses cluster DNS so Tailscale Magic DNS resolves `flamingo`, `iguana`, `koala` directly when the koala node has Tailscale up (it does).
|
||||
3. The pod renders the URL with chromium, drops PNGs into an `emptyDir`, then `sleep`s briefly so `kubectl cp` can pull the files out.
|
||||
4. PNGs land in `./screenshots/` (or `--out DIR`) on the agent's host.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `kubectl` on the calling host with a kubeconfig pointing at koala's k3s.
|
||||
- The dev server **binds 0.0.0.0** (not 127.0.0.1) so pods on koala can reach it across Tailscale. For Go: `http.ListenAndServe(":8099", ...)`. For Node: `--host 0.0.0.0`. For Vite: `--host`.
|
||||
- The dev server's host (flamingo, iguana, koala, …) is reachable on Tailscale and Magic DNS resolves the short hostname.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# auto-detect Tailscale hostname, screenshot port 8099 root
|
||||
~/dev/.skills/web-shot/shot.sh --port 8099
|
||||
|
||||
# explicit URL
|
||||
~/dev/.skills/web-shot/shot.sh --url http://flamingo:5173
|
||||
|
||||
# capture a search state too (types into input[name=q])
|
||||
~/dev/.skills/web-shot/shot.sh --port 8181 --search agentsquad
|
||||
|
||||
# different output dir
|
||||
~/dev/.skills/web-shot/shot.sh --port 3000 --out /tmp/shots
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|-------------|-------------------------------|-----------------------------------------------------------|
|
||||
| `--port N` | — | Host port. Combined with `--host` and `--path`. |
|
||||
| `--path P` | `/` | URL path after the host:port. |
|
||||
| `--host H` | auto via `tailscale status` | Override the hostname pods will dial. |
|
||||
| `--url U` | — | Full URL. Overrides `--host`, `--port`, `--path`. |
|
||||
| `--search Q`| — | Also capture a search state — types `Q` into `input[name=q]`. |
|
||||
| `--out D` | `./screenshots` | Output dir (created if missing). |
|
||||
| `--name N` | `web-shot` | Job name prefix. |
|
||||
| `-h` | — | Show usage. |
|
||||
|
||||
## Output
|
||||
|
||||
- `<out>/home.png` — initial page.
|
||||
- `<out>/search.png` — only when `--search` was passed.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- `kubectl: command not found` → install kubectl + tailscale-mode kubeconfig.
|
||||
- `connection refused` from the pod → dev server bound to 127.0.0.1, fix the bind.
|
||||
- `No matches for selector` when `--search` was given → page lacks `input[name=q]`. Use a project-specific script or rerun without `--search`.
|
||||
- Image pull slow on first run → ~1GB playwright image cached on koala after first use.
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- The site needs auth / cookies / login state — extend the script or use a project-local Playwright suite.
|
||||
- You need PDF, video, or interaction more elaborate than "type a query and screenshot". Promote to a real Playwright test under the project repo.
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
# web-shot/shot.sh — screenshot a running web UI via a k3s playwright Job.
|
||||
#
|
||||
# Reusable across any Tailscale-connected host whose kubeconfig points at the
|
||||
# koala k3s cluster. See SKILL.md alongside this file for prereqs.
|
||||
set -euo pipefail
|
||||
|
||||
# --- defaults --------------------------------------------------------------
|
||||
PORT=""
|
||||
PATH_=""
|
||||
HOST=""
|
||||
URL=""
|
||||
SEARCH=""
|
||||
OUT_DIR=""
|
||||
JOB_PREFIX="web-shot"
|
||||
PLAYWRIGHT_TAG="v1.49.0-jammy"
|
||||
PLAYWRIGHT_NPM="1.49.0"
|
||||
NAMESPACE="default"
|
||||
|
||||
usage() {
|
||||
sed -n '1,/^# ---/p' "$0" | sed 's/^# \{0,1\}//;1,/^!/d' >&2
|
||||
cat >&2 <<EOF
|
||||
usage: shot.sh [--port N] [--path P] [--host H] [--url U]
|
||||
[--search Q] [--out DIR] [--name PREFIX]
|
||||
See SKILL.md for prerequisites and examples.
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
# --- parse args ------------------------------------------------------------
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--path) PATH_="$2"; shift 2 ;;
|
||||
--host) HOST="$2"; shift 2 ;;
|
||||
--url) URL="$2"; shift 2 ;;
|
||||
--search) SEARCH="$2"; shift 2 ;;
|
||||
--out) OUT_DIR="$2"; shift 2 ;;
|
||||
--name) JOB_PREFIX="$2"; shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "unknown flag: $1" >&2; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- resolve URL -----------------------------------------------------------
|
||||
if [ -z "$URL" ]; then
|
||||
if [ -z "$PORT" ]; then
|
||||
echo "✗ --port or --url is required" >&2
|
||||
usage
|
||||
fi
|
||||
if [ -z "$HOST" ]; then
|
||||
if command -v tailscale >/dev/null 2>&1; then
|
||||
HOST="$(tailscale status --self --json 2>/dev/null | jq -r '.Self.HostName' 2>/dev/null || true)"
|
||||
fi
|
||||
if [ -z "$HOST" ] && [ -r /etc/hostname ]; then
|
||||
HOST="$(tr -d '\n' </etc/hostname)"
|
||||
fi
|
||||
if [ -z "$HOST" ]; then
|
||||
echo "✗ could not auto-detect Tailscale hostname — pass --host" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
PATH_="${PATH_:-/}"
|
||||
case "$PATH_" in /*) ;; *) PATH_="/$PATH_" ;; esac
|
||||
URL="http://${HOST}:${PORT}${PATH_}"
|
||||
fi
|
||||
|
||||
OUT_DIR="${OUT_DIR:-$(pwd)/screenshots}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
JOB_NAME="${JOB_PREFIX}-$(date +%s)"
|
||||
echo "→ host=${HOST:-—} url=${URL} out=${OUT_DIR} job=${JOB_NAME}"
|
||||
|
||||
# --- preflight: dev server must be reachable -------------------------------
|
||||
# (Optional but cheap. Run from the caller, not the pod.)
|
||||
if ! curl -fsS --max-time 3 "${URL%/}/healthz" >/dev/null 2>&1 \
|
||||
&& ! curl -fsS --max-time 3 "$URL" >/dev/null 2>&1; then
|
||||
echo "⚠ ${URL} not reachable from this host. Pod will retry; if it fails, the dev server may be bound to 127.0.0.1 only." >&2
|
||||
fi
|
||||
|
||||
# --- emit + apply Job ------------------------------------------------------
|
||||
TAKE_SEARCH=""
|
||||
[ -n "$SEARCH" ] && TAKE_SEARCH="yes"
|
||||
|
||||
cleanup() {
|
||||
kubectl -n "$NAMESPACE" delete job "$JOB_NAME" --ignore-not-found=true >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
kubectl -n "$NAMESPACE" apply -f - >/dev/null <<YAML
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ${JOB_NAME}
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 120
|
||||
backoffLimit: 0
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: shot
|
||||
image: mcr.microsoft.com/playwright:${PLAYWRIGHT_TAG}
|
||||
env:
|
||||
- { name: URL, value: "${URL}" }
|
||||
- { name: SEARCH, value: "${SEARCH}" }
|
||||
- { name: DO_SEARCH, value: "${TAKE_SEARCH}" }
|
||||
command: ["bash", "-lc"]
|
||||
args:
|
||||
- |
|
||||
set -e
|
||||
cd /tmp
|
||||
npm init -y >/dev/null 2>&1
|
||||
npm install playwright@${PLAYWRIGHT_NPM} --no-fund --no-audit --silent 2>&1 | tail -1
|
||||
mkdir -p /shot
|
||||
cat > /tmp/s.mjs <<'JS'
|
||||
import { chromium } from 'playwright';
|
||||
const url = process.env.URL;
|
||||
const out = process.env.OUT;
|
||||
const action = process.env.ACTION || '';
|
||||
const b = await chromium.launch();
|
||||
const ctx = await b.newContext({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 2 });
|
||||
const p = await ctx.newPage();
|
||||
await p.goto(url, { waitUntil: 'networkidle' });
|
||||
if (action.startsWith('search:')) {
|
||||
const q = action.slice('search:'.length);
|
||||
await p.fill('input[name="q"]', q);
|
||||
await p.waitForResponse(r => r.url().includes('/search'), { timeout: 5000 }).catch(() => {});
|
||||
await p.waitForTimeout(500);
|
||||
}
|
||||
await p.screenshot({ path: out, fullPage: true });
|
||||
await b.close();
|
||||
console.log('wrote', out);
|
||||
JS
|
||||
URL="\$URL" OUT=/shot/home.png ACTION= node /tmp/s.mjs
|
||||
if [ "\$DO_SEARCH" = "yes" ]; then
|
||||
URL="\$URL" OUT=/shot/search.png ACTION="search:\$SEARCH" node /tmp/s.mjs
|
||||
fi
|
||||
chmod 644 /shot/*.png
|
||||
echo "READY"
|
||||
# Window for kubectl cp; deleted by trap after we've grabbed files.
|
||||
sleep 90
|
||||
YAML
|
||||
|
||||
# --- wait for pod + READY marker ------------------------------------------
|
||||
echo "→ waiting for pod scheduling…"
|
||||
for _ in $(seq 1 60); do
|
||||
POD="$(kubectl -n "$NAMESPACE" get pods -l "batch.kubernetes.io/job-name=$JOB_NAME" \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
|
||||
[ -n "$POD" ] && break
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "$POD" ]; then
|
||||
echo "✗ pod not scheduled" >&2; exit 1
|
||||
fi
|
||||
echo "→ pod $POD"
|
||||
|
||||
echo "→ waiting for READY (pulling image + rendering)…"
|
||||
for _ in $(seq 1 60); do
|
||||
if kubectl -n "$NAMESPACE" logs "$POD" 2>/dev/null | grep -q '^READY$'; then
|
||||
break
|
||||
fi
|
||||
PHASE="$(kubectl -n "$NAMESPACE" get pod "$POD" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
|
||||
if [ "$PHASE" = "Failed" ]; then
|
||||
echo "✗ pod Failed — logs:" >&2
|
||||
kubectl -n "$NAMESPACE" logs "$POD" 2>&1 | tail -30 >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# --- copy files out --------------------------------------------------------
|
||||
kubectl -n "$NAMESPACE" cp "${NAMESPACE}/${POD}:/shot/home.png" "$OUT_DIR/home.png" 2>/dev/null \
|
||||
|| { echo "✗ kubectl cp home.png failed" >&2; exit 1; }
|
||||
if [ "$TAKE_SEARCH" = "yes" ]; then
|
||||
kubectl -n "$NAMESPACE" cp "${NAMESPACE}/${POD}:/shot/search.png" "$OUT_DIR/search.png" 2>/dev/null \
|
||||
|| { echo "✗ kubectl cp search.png failed" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
ls -la "$OUT_DIR"/*.png
|
||||
echo "✓ screenshots in $OUT_DIR"
|
||||
Reference in New Issue
Block a user