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:
2026-08-23 14:29:51 +02:00
co-authored by Claude Opus 5
parent 21be3ea245
commit 56a947b2a0
88 changed files with 182 additions and 253 deletions
+259
View File
@@ -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