Development
This page is for people working on Yak itself — fixing bugs in the Laravel app, adding new features, or writing new channel drivers. If you just want to run Yak against your repos, see the Setup page.
Local Development Setup
Section titled “Local Development Setup”Prerequisites
Section titled “Prerequisites”| Tool | Version | Notes |
|---|---|---|
| PHP | 8.4+ | With pdo_mysql extension |
| Composer | 2.x | composer --version to verify |
| Node | 20+ | For building frontend assets and running Playwright |
| Docker | 24+ | For MariaDB via docker-compose |
You do NOT need Claude Code CLI, Chromium, or Ansible to develop on Yak. Those are runtime dependencies for a production Yak instance — tests fake all external process calls via Laravel’s Process::fake() and Http::fake().
Getting Started
Section titled “Getting Started”git clone https://github.com/geocodio/yak.gitcd yak
# Install PHP dependenciescomposer install
# Install Node dependencies and build the dashboardnpm installnpm run build
# Start MariaDBdocker compose up -d
# Set up the environmentcp .env.example .envphp artisan key:generate
# Run migrationsphp artisan migrate --seedThe starter kit’s default test user is seeded in database/seeders/DatabaseSeeder.php.
Running The Dev Server
Section titled “Running The Dev Server”composer run devThis starts the Laravel dev server, the queue worker, the scheduler, and Vite all in parallel via concurrently. Equivalent to running:
php artisan servephp artisan queue:listen --tries=1php artisan schedule:worknpm run devOpen http://localhost:8000. The Livewire starter kit’s auth pages work for local development — Google OAuth is only used in production.
Running Tests
Section titled “Running Tests”Yak has four test tiers. The first three run in CI on every push; the fourth is nightly only.
| Tier | Directory | How to run | Speed |
|---|---|---|---|
| Unit | tests/Unit/ | vendor/bin/pest --testsuite=Unit | Seconds |
| Feature | tests/Feature/ | vendor/bin/pest --testsuite=Feature | Seconds |
| Browser | tests/Browser/ | vendor/bin/pest --testsuite=Browser | ~30s |
| Contract | tests/Contract/ | vendor/bin/pest --group=contract | ~60s, requires Claude CLI |
Day-To-Day Commands
Section titled “Day-To-Day Commands”# Everything except contract tests (matches CI)vendor/bin/pest --exclude-group=contract
# Compact output (recommended for iterative work)php artisan test --compact
# A single filephp artisan test --compact tests/Feature/RunYakJobTest.php
# A single test by name filterphp artisan test --compact --filter="creates a task when a valid Sentry webhook arrives"Browser Tests
Section titled “Browser Tests”Browser tests use Pest’s Playwright plugin. On first run:
npx playwright install --with-deps chromiumThen:
vendor/bin/pest --testsuite=BrowserBrowser tests cover the auth flow, Livewire live updates on the task detail page, artifact viewer navigation, signed URL access, and accessibility (assertNoAccessibilityIssues() plus assertNoJavaScriptErrors() on dashboard pages).
Contract Tests
Section titled “Contract Tests”Contract tests validate that real Claude CLI output matches the schema Yak expects. They run nightly against the real CLI — not in the normal test run — because they need Claude CLI installed and an Anthropic API key.
vendor/bin/pest --group=contractIf you change how Yak parses Claude CLI output (ClaudeOutputParser), add a contract test.
Code Style
Section titled “Code Style”Two tools, both enforced in CI.
Laravel Pint for formatting. Run before committing:
vendor/bin/pintOr check without fixing:
vendor/bin/pint --testYak uses the Laravel preset with one override: concat_space is set to one (space before and after .). See pint.json at the repo root.
PHPStan / Larastan
Section titled “PHPStan / Larastan”PHPStan at level 8 (maximum) with the Larastan extension:
vendor/bin/phpstan analyseYak ships with a phpstan-baseline.neon file containing pre-existing errors (mostly Livewire dynamic property access). Do not clear the baseline without approval. New code should not add to it.
Pre-commit
Section titled “Pre-commit”Not enforced. Developers can run Pint on save or set up a git pre-commit hook. CI is the gate.
Architecture Overview For Contributors
Section titled “Architecture Overview For Contributors”See the Architecture page for the full system design. For contributors, the shortest version:
app/Jobs/— the pipeline.RunYakJob,RetryYakJob,ResearchYakJob,SetupYakJob,ClarificationReplyJob,ProcessCIResultJob,CreatePullRequestJob,SendNotificationJob,ProcessWebhookJob,CleanupJob. Each agent job creates an Incus sandbox at the start and destroys it in afinallyblock.app/Jobs/Middleware/—EnsureDailyBudget. Cross-cutting concerns as Laravel job middleware.app/Agents/—SandboxedAgentRunner(theAgentRunnerimplementation),ClaudeCodeOutputParser,StreamEventHandler. The runner executes Claude Code inside the task’s Incus container viaincus exec.app/Services/IncusSandboxManager.php— sandbox lifecycle: clone from snapshot, configure resource limits, push Claude/MCP config, snapshot, promote-to-template, destroy.app/Services/SandboxArtifactCollector.php— pulls.yak-artifacts/from the sandbox before destruction.app/Drivers/— channel driver implementations. Each channel has an input driver, a notification driver, or both.app/Contracts/— the driver interfaces (InputDriver,CIDriver,NotificationDriver) plusCIBuildScannerandAgentRunner.app/Http/Controllers/Webhooks/— one invokable controller per webhook endpoint. Uses theVerifiesWebhookSignaturetrait. Note: there is no Drone CI webhook — Drone is polled viayak:poll-drone-ciinstead.app/Livewire/— dashboard components.Tasks/TaskList,Tasks/TaskDetail,Repos/RepoList,Repos/RepoForm,CostDashboard,Health,HealthRow,Skills,PromptEditor, plusSettings/*andActions/*.app/Models/—YakTask(note:$table = 'tasks'),TaskLog,Artifact,Repository,DailyCost,AiUsage,Prompt,PromptVersion,GitHubInstallationToken,LinearOauthConnection.app/Enums/—TaskStatus(the state machine),TaskMode,NotificationType. The state machine uses theartisan-build/fat-enumscomposer package.app/Services/— external API integrations (GitHub, Linear, Slack, Sentry), detection logic (RepoDetector,RepoRouter), and thePromptResolverthat renders prompts with DB overrides.app/Prompts/—PromptDefinitions(metadata for every prompt slug) andPromptFixtures(sample data used by the in-app editor preview).app/YakPromptBuilder.php— entry point for task/system prompt assembly. Delegates rendering to thePromptsfacade →PromptResolver, which prefers DB-stored overrides (edited via the/promptspage) and falls back to the canonical Blade template.app/GitOperations.php— centralized git commands via theProcessfacade.app/Providers/ChannelServiceProvider.php— registers webhook routes conditionally based on which channels have credentials configured.resources/views/prompts/— Blade templates. These are the defaults for every prompt slug; the in-app editor persists overrides to thepromptstable.docker/— production Docker configuration. The rootDockerfilebuilds from it.
Adding A New Channel Driver
Section titled “Adding A New Channel Driver”Yak’s channel architecture is the primary extension point. Adding a new input source (Jira, GitHub Issues, email, etc.), a new CI system, or a new notification target means implementing one or more of the contracts in app/Contracts/.
The Interfaces
Section titled “The Interfaces”interface InputDriver{ public function parseWebhook(Request $request): ?TaskDescription; // Returns null if the webhook should be ignored.}
// app/Contracts/CIDriver.phpinterface CIDriver{ public function parseBuildResult(Request $request): ?BuildResult; public function fetchFailureOutput(string $buildId): string;}
// app/Contracts/NotificationDriver.phpinterface NotificationDriver{ public function acknowledge(YakTask $task): void; public function progress(YakTask $task, string $message): void; public function result(YakTask $task): void; public function failed(YakTask $task, string $reason): void;}Worked Example: Adding A Jira Input Driver
Section titled “Worked Example: Adding A Jira Input Driver”-
Add configuration
In
config/yak.php, add ajiraentry underchannels:'jira' => ['base_url' => env('JIRA_BASE_URL'),'api_token' => env('JIRA_API_TOKEN'),'webhook_secret' => env('JIRA_WEBHOOK_SECRET'),],The
Channelhelper class (app/Channel.php) auto-detects channels as enabled when their credentials are present. -
Create the input driver
Terminal window php artisan make:class Drivers/JiraInputDriverImplement
InputDriver. Parse the incoming Jira webhook payload into aTaskDescription(source =jira, external_id from issue key, context from issue body). -
Create the webhook controller
Terminal window php artisan make:controller Webhooks/JiraWebhookController --invokableUse the
VerifiesWebhookSignaturetrait for signature checking. Resolve the input driver, parse the request, create aYakTask, dispatchRunYakJob. Look atSlackWebhookControllerfor the canonical pattern. -
Register the route conditionally
Add the channel to the
CHANNEL_CONTROLLERSmap inapp/Providers/ChannelServiceProvider.php:private const CHANNEL_CONTROLLERS = ['slack' => SlackWebhookController::class,'linear' => LinearWebhookController::class,'sentry' => SentryWebhookController::class,'jira' => JiraWebhookController::class,];The provider auto-registers
POST /webhooks/{channel}only when(new Channel($channel))->enabled()returns true. -
Add a notification driver (optional but recommended)
Create
app/Drivers/JiraNotificationDriver.phpimplementingNotificationDriver. Post issue comments via the Jira REST API (useHttp::withToken()). If omitted, notifications fall back to PR comments. -
Create a prompt template
Add
resources/views/prompts/tasks/jira-fix.blade.phpfollowing the pattern oftasks/linear-fix.blade.php. Keep it short. This is the default; operators can override it at runtime via the in-app Prompts editor (thepromptstable). -
Register the slug and wire up rendering
- Add an entry for
tasks-jira-fixtoapp/Prompts/PromptDefinitions.php(view path, label, category, variables). - Add a sample fixture for the editor preview to
app/Prompts/PromptFixtures.php. - Add a render helper in
app/YakPromptBuilder.phpand route thejirasource to it intaskPrompt().
- Add an entry for
-
Write tests
tests/Unit/JiraInputDriverTest.php— parses sample webhook payloads, returns expected task description, rejects invalid payloadstests/Feature/JiraWebhookTest.php— full controller test: valid payload creates task, invalid signature rejected, duplicate external_id rejectedtests/Feature/JiraNotificationTest.php— usesHttp::fake()to assert correct API payloads
-
Add Ansible support (for production deployment)
Create
ansible/roles/channel-jira/with tasks for registering the webhook on the Jira side. Add the channel to the conditional includes inansible/playbook.yml. Followansible/roles/channel-linear/as a template. -
Document it
Add a Jira section to the Channels page following the pattern of Linear and Sentry.
Adding A New CI Driver
Section titled “Adding A New CI Driver”Same pattern, but implement CIDriver (or CIBuildScanner for pull-based systems) instead of InputDriver. GitHub Actions posts check-run results to POST /webhooks/ci/github. Drone has no outbound webhook, so its results are polled by the yak:poll-drone-ci scheduled command (see app/Console/Commands/PollDroneCiCommand.php and app/Services/DroneBuildScanner.php). The repo’s ci_system column is the authority on which driver to use for a given repo.
Testing Conventions
Section titled “Testing Conventions”Factories
Section titled “Factories”Every model has a factory with named states. Factories are the only way to create test data — no raw DB inserts in tests.
$task = YakTask::factory() ->awaitingClarification() ->forRepo('my-app') ->create();Key factory states:
| Model | States |
|---|---|
YakTask | pending, running, awaitingClarification, awaitingCi, retrying, success, failed, expired |
Repository | default, inactive, withAuth, withSentry |
TaskLog | info, warning, error |
Artifact | screenshot, video, research |
Test Helpers
Section titled “Test Helpers”tests/Helpers/ provides reusable helpers loaded via Pest’s uses() in tests/Pest.php:
| Helper | Purpose |
|---|---|
fakeClaudeRun() | Fakes a successful Claude CLI run with configurable result_summary, cost_usd, session_id, num_turns |
fakeClaudeClarification() | Fakes a Claude run that returns clarification JSON |
fakeClaudeError() | Fakes a failed Claude run |
assertSlackThreadReply() | Asserts an HTTP call to Slack chat.postMessage with correct channel/thread/text |
assertLinearActivity() | Asserts a Linear agent session activity was posted |
assertLinearStateUpdate() | Asserts a Linear issue’s state was updated |
Process And HTTP Faking
Section titled “Process And HTTP Faking”All external process calls (Claude CLI, git, docker-compose) use Process::fake(). Patterns matter — specific patterns before wildcards, because Laravel matches in registration order:
Process::fake([ 'claude -p *' => Process::result(json: ['result' => '...', 'session_id' => '...']), '*' => Process::result(),]);External API calls use Http::fake() with URL patterns:
Http::fake([ 'slack.com/api/chat.postMessage' => Http::response(['ok' => true]), 'api.linear.app/graphql' => Http::response(['data' => ['...']]),]);Database
Section titled “Database”All feature tests use SQLite in-memory via RefreshDatabase (configured globally in tests/Pest.php). The application uses MariaDB in development and production, but tests use SQLite in-memory for speed — no test database container needed.
Naming
Section titled “Naming”Pest it() syntax with descriptive names:
it('creates a task when a valid Sentry webhook arrives', function () { ... });it('rejects Sentry webhooks for CSP violations', function () { ... });it('detects clarification JSON and pauses for user reply', function () { ... });Pull Request Process
Section titled “Pull Request Process”-
Fork the repo and create a branch off
main -
Make your changes with tests
-
Run the full pre-flight check:
Terminal window vendor/bin/pintvendor/bin/phpstan analysevendor/bin/pest --exclude-group=contract -
Open a PR using the template (
.github/pull_request_template.md): What, Why, How to test, Checklist -
CI runs Pint, PHPStan, unit tests, feature tests, and browser tests on every push
-
A maintainer reviews, and if all four checks pass, merges
What Not To Touch Without Approval
Section titled “What Not To Touch Without Approval”phpstan-baseline.neon— pre-existing errors, do not cleardocker/supervisord.conf— production config.chief/— local working files, never commit
Reporting Bugs And Requesting Features
Section titled “Reporting Bugs And Requesting Features”- Bug report —
https://github.com/geocodio/yak/issues/new?template=bug_report.yml - Feature request —
https://github.com/geocodio/yak/issues/new?template=feature_request.yml
Include the Yak version (git SHA), the channel involved, steps to reproduce, and relevant logs from docker logs yak --tail 500 or the task’s debug section.