Loading...
Loading...
Use when designing or architecting Elixir/Phoenix applications, creating comprehensive project documentation, planning OTP supervision trees, defining domain models with Ash Framework, structuring multi-app projects with path-based dependencies, or preparing handoff documentation for Director/Implementor AI collaboration
npx skill4agent add gsmlg-dev/code-agent elixir-architectTask 1: Research [domain] architecture patterns and data models
Task 2: Analyze Ash Framework resource patterns, extensions, and best practices
Task 3: Study Dave Thomas's path-based dependency approach from available projects
Task 4: Research Superpowers framework for implementation plan formatproject_root/
├── README.md
├── CLAUDE.md
├── docs/
│ ├── HANDOFF.md
│ ├── architecture/
│ │ ├── 00_SYSTEM_OVERVIEW.md
│ │ ├── 01_DOMAIN_MODEL.md
│ │ ├── 02_DATA_LAYER.md
│ │ ├── 03_FUNCTIONAL_CORE.md
│ │ ├── 04_BOUNDARIES.md
│ │ ├── 05_LIFECYCLE.md
│ │ ├── 06_WORKERS.md
│ │ └── 07_INTEGRATION_PATTERNS.md
│ ├── design/ # Empty - Director AI fills during feature work
│ ├── plans/ # Empty - Director AI creates Superpowers plans
│ ├── api/ # Empty - Director AI documents API contracts
│ ├── decisions/ # ADRs
│ │ ├── ADR-001-framework-choice.md
│ │ ├── ADR-002-id-strategy.md
│ │ ├── ADR-003-process-architecture.md
│ │ └── [domain-specific ADRs]
│ └── guardrails/
│ ├── NEVER_DO.md
│ ├── ALWAYS_DO.md
│ ├── DIRECTOR_ROLE.md
│ ├── IMPLEMENTOR_ROLE.md
│ └── CODE_REVIEW_CHECKLIST.md# [Project Name]
[One-line description]
## Overview
[2-3 paragraphs: what this system does and why]
## Architecture
This project follows Dave Thomas's multi-app structure:
project_root/
├── [app_name]_core/ # Domain logic (Ash resources, pure functions)
├── [app_name]_api/ # REST/GraphQL APIs (Phoenix)
├── [app_name]_jobs/ # Background jobs (Oban workers)
├── [app_name]_events/ # Event streaming (Broadway)
└── [app_name]_admin/ # Admin UI (LiveView)
## Tech Stack
- **Elixir** 1.17+ with OTP 27+
- **Ash Framework** 3.0+ - Declarative domain modeling
- **Oban** 2.17+ - Background job processing
- **Phoenix** 1.7+ - Web framework
- **PostgreSQL** 16+ - Primary database
## Getting Started
[Setup instructions]
## Development
[Common tasks, testing, etc.]
## Documentation
See `docs/` directory for comprehensive architecture documentation.# ❌ NEVER
attribute :amount, :float
# ✅ ALWAYS
attribute :amount, :integer # Store cents: 100_00 = $100.00
attribute :balance, :decimal # Or use Decimal for precision
# Why: 0.1 + 0.2 != 0.3 in floating point!# NEVER DO: Critical Prohibitions
## 1. Never Use Floats for Money
❌ **NEVER**: `attribute :amount, :float`
✅ **ALWAYS**: `attribute :amount, :integer` or `attribute :balance, :decimal`
**Why**: Float precision errors cause incorrect financial calculations
## 2. Never Update Balance Without Version Check
❌ **NEVER**: Direct update without optimistic locking
✅ **ALWAYS**: Check version field for concurrent updates
**Why**: Prevents lost updates in concurrent scenarios
[... 8 more critical prohibitions with code examples ...]# ✅ ALWAYS wrap multi-step operations in transactions
Multi.new()
|> Multi.insert(:transaction, transaction_changeset)
|> Multi.run(:operations, fn _repo, %{transaction: txn} ->
create_operations(txn.id, params)
end)
|> Multi.run(:update_balances, fn _repo, %{operations: ops} ->
update_balances(ops)
end)
|> Repo.transaction()%Task{
id: "tsk_01HQBMB5KTQNDRPQHM3VXDT2E9K", # ULID with prefix
project_id: "prj_01HQBMA5KTQNDRPQHM3VXDT2E9K",
title: "Implement user authentication",
description: "Add JWT-based auth with refresh tokens",
status: :in_progress, # :todo | :in_progress | :blocked | :review | :done
priority: :high, # :low | :medium | :high | :urgent
assignee_id: "usr_01HQBMB5KTQNDRPQHM3VXDT2E9K",
due_date: ~D[2024-02-01],
estimated_hours: 8,
version: 1,
inserted_at: ~U[2024-01-01 00:00:00Z],
updated_at: ~U[2024-01-01 00:00:00Z]
}defmodule TaskManager.Task do
use Ash.Resource,
domain: TaskManager,
data_layer: AshPostgres.DataLayer,
extensions: [AshPaperTrail]
postgres do
table "tasks"
repo TaskManager.Repo
end
attributes do
uuid_v7_primary_key :id, prefix: "tsk"
attribute :title, :string, allow_nil?: false
attribute :description, :string
attribute :status, :atom,
constraints: [one_of: [:todo, :in_progress, :blocked, :review, :done]],
default: :todo
attribute :priority, :atom,
constraints: [one_of: [:low, :medium, :high, :urgent]],
default: :medium
attribute :due_date, :date
attribute :estimated_hours, :integer
attribute :version, :integer, default: 1
timestamps()
end
relationships do
belongs_to :project, TaskManager.Project
belongs_to :assignee, TaskManager.User
has_many :comments, TaskManager.Comment
end
actions do
defaults [:read, :destroy]
create :create do
accept [:title, :description, :status, :priority, :project_id, :assignee_id]
change fn changeset, _ ->
Ash.Changeset.force_change_attribute(changeset, :status, :todo)
end
end
update :update_with_version do
accept [:title, :description, :status, :priority, :assignee_id, :due_date]
require_atomic? false
change optimistic_lock(:version)
end
update :assign do
accept [:assignee_id]
change optimistic_lock(:version)
end
update :transition_status do
accept [:status]
validate fn changeset, _ ->
# Validate state machine transitions
validate_status_transition(changeset)
end
change optimistic_lock(:version)
end
end
enddefmodule TaskManager.Impl.TaskLogic do
@moduledoc """
Pure functions for task business logic.
No database access, no side effects.
"""
@spec can_transition?(atom(), atom()) :: boolean()
def can_transition?(from_status, to_status) do
valid_transitions = %{
todo: [:in_progress, :blocked],
in_progress: [:blocked, :review, :done],
blocked: [:todo, :in_progress],
review: [:in_progress, :done],
done: []
}
to_status in Map.get(valid_transitions, from_status, [])
end
@spec calculate_priority_score(map()) :: integer()
def calculate_priority_score(task) do
base_score = priority_value(task.priority)
urgency_bonus = days_until_due(task.due_date)
dependency_factor = if task.has_blockers?, do: -10, else: 0
base_score + urgency_bonus + dependency_factor
end
defp priority_value(:urgent), do: 100
defp priority_value(:high), do: 75
defp priority_value(:medium), do: 50
defp priority_value(:low), do: 25
defp days_until_due(nil), do: 0
defp days_until_due(due_date) do
diff = Date.diff(due_date, Date.utc_today())
cond do
diff < 0 -> 50 # Overdue
diff <= 3 -> 30 # Within 3 days
diff <= 7 -> 15 # Within a week
true -> 0
end
end
enddefmodule TaskManager.Boundaries.TaskService do
alias Ecto.Multi
alias TaskManager.Impl.TaskLogic
def transition_task(task_id, new_status, opts \\ []) do
Multi.new()
|> Multi.run(:load_task, fn _repo, _changes ->
case Ash.get(Task, task_id) do
{:ok, task} -> {:ok, task}
error -> error
end
end)
|> Multi.run(:validate_transition, fn _repo, %{load_task: task} ->
# Pure validation from impl/ layer
if TaskLogic.can_transition?(task.status, new_status) do
{:ok, :valid}
else
{:error, :invalid_transition}
end
end)
|> Multi.run(:update_task, fn _repo, %{load_task: task} ->
Task.transition_status(task, %{status: new_status})
end)
|> Multi.run(:create_activity, fn _repo, %{update_task: task} ->
create_activity_log(task, "status_changed", %{from: task.status, to: new_status})
end)
|> Multi.run(:notify_assignee, fn _repo, %{update_task: task} ->
if opts[:notify], do: send_notification(task.assignee_id, task)
{:ok, :notified}
end)
|> Multi.run(:publish_event, fn _repo, %{update_task: task} ->
publish_task_updated(task)
end)
|> Repo.transaction()
end
enddef start(_type, _args) do
children = [
{TaskManager.Repo, []},
{Phoenix.PubSub, name: TaskManager.PubSub},
{TaskManager.Runtime.TaskCache, []},
genstage_supervisor_spec(),
{Oban, Application.fetch_env!(:task_manager, Oban)}
]
opts = [strategy: :one_for_one, name: TaskManager.Supervisor]
Supervisor.start_link(children, opts)
enddefmodule TaskManager.Workers.ReminderNotifier do
use Oban.Worker,
queue: :notifications,
max_attempts: 3,
priority: 2
@impl Oban.Worker
def perform(%Oban.Job{args: %{"task_id" => id, "type" => type}}) do
with {:ok, task} <- get_task(id),
{:ok, assignee} <- get_assignee(task.assignee_id),
:ok <- send_reminder(assignee, task, type) do
{:ok, :notified}
end
end
defp send_reminder(assignee, task, "due_soon") do
# Send email/push notification
# Task is due within 24 hours
Notifications.send(assignee.email, "Task Due Soon", render_template(task))
end
defp send_reminder(assignee, task, "overdue") do
# Task is past due date
Notifications.send_urgent(assignee.email, "Overdue Task", render_template(task))
end
enddefmodule TaskManager.Integration.HTTPClient do
def request(method, url, body, opts \\ []) do
timeout = Keyword.get(opts, :timeout, 5_000)
retries = Keyword.get(opts, :retries, 3)
request = build_request(method, url, body)
do_request_with_retry(request, timeout, retries)
end
defp do_request_with_retry(request, timeout, retries_left, attempt \\ 1) do
case Finch.request(request, TaskManager.Finch, receive_timeout: timeout) do
{:ok, %{status: status}} when status in 200..299 ->
{:ok, decode_response(response)}
{:ok, %{status: status}} when status in 500..599 and retries_left > 0 ->
backoff = calculate_backoff(attempt)
Process.sleep(backoff)
do_request_with_retry(request, timeout, retries_left - 1, attempt + 1)
{:error, _} = error ->
error
end
end
defp calculate_backoff(attempt) do
# Exponential backoff: 100ms, 200ms, 400ms, 800ms
trunc(:math.pow(2, attempt - 1) * 100)
end
end# ADR-XXX: [Decision Title]
**Status:** Accepted
**Date:** YYYY-MM-DD
**Deciders:** [Role]
**Context:** [Brief context]
## Context
[Detailed explanation of the situation requiring a decision]
## Decision
[Clear statement of what was decided]
## Rationale
[Why this decision was made - include code examples, metrics, trade-offs]
## Alternatives Considered
### Alternative 1: [Name]
**Implementation:**
```elixir
# Example code# Good example# Bad example
**Minimum ADRs to create:**
1. **ADR-001: Framework Choice** (Ash vs Plain Ecto vs Event Sourcing)
2. **ADR-002: ID Strategy** (ULID vs UUID vs Auto-increment vs Snowflake)
3. **ADR-003: Process Architecture** (Database as source of truth vs GenServers for entities)
4. **Domain-specific ADRs** based on requirements
### Phase 8: Handoff Documentation
Create HANDOFF.md with:
1. **Overview** - Project status, location, ready state
2. **Project Structure** - Annotated directory tree
3. **Documentation Index** - What each file contains
4. **Workflow** - Director → Implementor → Review → Iterate cycle
5. **Implementation Phases** - Break project into 4-week phases
6. **Key Architectural Principles** - DO/DON'T examples
7. **Testing Strategy** - Unit/Integration/Property test patterns
8. **Commit Message Format** - Conventional commits structure
9. **Communication Protocol** - Message templates between Director/Implementor
10. **Troubleshooting** - Common issues and solutions
11. **Success Metrics** - Specific performance targets
12. **Next Steps** - Immediate actions for Director AI
Example workflow section:
```markdown
## Workflow
### Phase 1: Director Creates Design & Plan
1. Read feature request from user
2. Review architecture documents
3. Create design document in `docs/design/`
4. Create implementation plan in `docs/plans/` (Superpowers format)
5. Commit design + plan
6. Hand off to Implementor with plan path
### Phase 2: Implementor Executes Plan
1. Read implementation plan
2. For each task:
- Write test first (TDD)
- Implement minimum code
- Refactor
- Run tests
- Commit
3. Report completion to Director
### Phase 3: Director Reviews
1. Review committed code
2. Check against design
3. Verify guardrails followed
4. Either approve or request changes
### Phase 4: Iterate Until Approved
[Loop until feature is complete]## Project Architecture Complete! 🚀
**Location:** /path/to/project
**Created:**
- ✅ Complete directory structure
- ✅ Foundation docs (README, CLAUDE.md)
- ✅ 5 guardrail documents
- ✅ 8 architecture documents (~6,000 lines)
- ✅ X Architecture Decision Records
- ✅ Handoff documentation
**Ready For:**
- Director AI to create first design + plan
- Implementor AI to execute implementation
- Iterative feature development
**Next Step:**
Director AI should begin by creating the first feature design.# ✅ ALWAYS validate state transitions
def transition_status(task, new_status) do
if TaskLogic.can_transition?(task.status, new_status) do
Task.update(task, %{status: new_status})
else
{:error, :invalid_transition}
end
end
# Define valid transitions
def can_transition?(from_status, to_status) do
valid_transitions = %{
todo: [:in_progress, :blocked],
in_progress: [:blocked, :review, :done],
blocked: [:todo, :in_progress],
review: [:in_progress, :done],
done: []
}
to_status in Map.get(valid_transitions, from_status, [])
end# ✅ ALWAYS check version for concurrent updates
def update_task(task_id, new_attrs) do
task = Repo.get!(Task, task_id)
changeset =
task
|> change(new_attrs)
|> optimistic_lock(:version)
case Repo.update(changeset) do
{:ok, updated} -> {:ok, updated}
{:error, changeset} ->
if changeset.errors[:version] do
{:error, :version_conflict}
else
{:error, changeset}
end
end
end# ❌ DON'T: GenServer per entity
defmodule TaskServer do
use GenServer
# Storing task state in process - DON'T DO THIS
end
# ✅ DO: GenServer for infrastructure
defmodule TaskCache do
use GenServer
# Caching active tasks (transient data, can rebuild from DB)
end
defmodule RateLimiter do
use GenServer
# Tracking API request counts (acceptable to lose on crash)
end# ✅ ALWAYS use Multi for multi-step operations
Multi.new()
|> Multi.insert(:task, task_changeset)
|> Multi.run(:assign, fn _repo, %{task: task} ->
create_assignment(task.id, assignee_id)
end)
|> Multi.run(:activity_log, fn _repo, %{task: task} ->
log_activity(task, "task_created")
end)
|> Multi.run(:publish_event, fn _repo, changes ->
publish_event(changes)
end)
|> Repo.transaction()# ❌ NEVER block request path
def send_notification(task) do
HTTPClient.post("https://notifications.com/api", ...) # BLOCKS!
end
# ✅ ALWAYS enqueue background job
def send_notification(task) do
%{task_id: task.id, type: "assignment"}
|> NotificationWorker.new()
|> Oban.insert()
end