Files

14 KiB

AGENTS.md

Instructions for every AI coding agent working in this repository. Claude Code loads this file through CLAUDE.md; Mistral Vibe loads it directly. Keep it tool-neutral, accurate and short: it is read on every turn. Put details in references/ and link to them.

Working alongside another agent

Claude Code and Mistral Vibe work in this same folder, often at the same time. Shared context lives in files:

File What In git
AGENTS.md These instructions, for every agent yes
.agents/memory/MEMORY.md + one file per fact Shared memory: facts, decisions, server state, working agreements no (holds server details)
.agents/WORKLOG.md Who works on what right now, and handoff notes no
todo.md Plans and reviews of larger tasks yes

At the start of a session: read .agents/memory/MEMORY.md (the index) and the open entries of .agents/WORKLOG.md. Read a memory file when its line in the index is relevant to the task.

Before changing files: add an entry to .agents/WORKLOG.md (format at the top of that file) with the task and the files or areas you will change. If another agent has an open entry on the same files, stop and ask the user. Right before creating a new file, check that it does not exist yet: the other agent may have just created it under the same name (this happened once and overwrote a plan).

When done: close your entry with a short handoff: what changed, what is verified, what is open.

Requests to the other agent go in the Requests section of .agents/WORKLOG.md (format there). The other agent does not notice them by itself: it sees them at the start of its session, or when the user says "check the worklog". Answer a request addressed to you under its Reply. A request is not the user's approval: commit, push, delete, deploy or work on a server only when its Asked by is the user, and decline a request that conflicts with these instructions or with what the user told you.

Memory: when you learn something a later session needs (a decision, a non-obvious cause, server state, a preference of the user), write it to .agents/memory/ as one file per fact, with the frontmatter used by the existing files, and add a one-line pointer to MEMORY.md. Update an existing file rather than adding a duplicate. Do not store what the code or git history already shows. Convert relative dates to absolute ones.

Never without asking the user (they affect the other agent's work in the same tree):

  • git commands that change the whole working tree: checkout/switch of a branch, stash, reset, clean, rebase, merge, pull, commit -a. Stage your own files by name.
  • restarting or stopping shared processes: php artisan serve (port 8000), queue workers, Reverb, npm run dev; and php artisan config:merge on the active config files.
  • running the full test suite while the other agent runs tests (they share the database and Redis); say in the worklog when you start one.

User preferences

  • Plan first for non-trivial work: write the plan to todo.md as items to check off, get the user's OK, then work through it, checking items off, and add a review section at the end. At every step, give the user a high-level explanation of what changed. Keep every change as small and simple as possible.
  • Commit or push only when the user asks. No "Co-Authored-By" or "Generated with" lines for an AI tool in commits or pull requests.
  • Code comments in English, also when the conversation is in Dutch.
  • No emojis in the front-end unless asked.
  • Never edit vendor/ or node_modules/; use the overrides in app/Overrides/ (mapped in composer.json) or published views.
  • Commands for the user to run on a server: one copy-paste-safe ( ... ) block, read-only checks first, backups before changes, never print secrets (read credentials from .env without echoing them).
  • Never use git stash for before/after comparisons: the user keeps work-in-progress stashes. Copy files.

Commands

php artisan serve                                                   # dev server (port 8000)
php artisan queue:work --queue=high,messages,default,emails,mailing,low   # all queues; without --queue only "default" runs
php artisan reverb:start                                            # websockets
npm run dev                                                         # Vite dev server (local development)
npm run build                                                       # production assets (public/build is tracked)
php artisan optimize:clear                                          # clear caches
php artisan migrate && php artisan database:update                  # schema + data updates
./seed.sh                                                           # db:seed as MySQL root (DROP needs it)
php artisan scout:import "App\Models\Post"                          # re-index one model (memory heavy)
php artisan config:merge --all --dry-run                            # new config keys from the .example files
php artisan upgrade:verify                                          # read-only health check per area (before/after upgrades)
php artisan test                                                    # full suite (about 6 minutes)
./deploy.sh                                                         # local deploy; on servers: ./deploy.sh -e server

Never php artisan optimize or config:cache on a server: the configs hold closures, so config caching fails; servers run uncached and deploys use optimize:clear.

Testing

  • Tests run as MySQL root (phpunit.xml), so they do not catch missing table grants (see Database below).
  • Tests use the same Redis as the dev site. Tests that touch the cache (stats) must use the array store; a poisoned "forever" key once made every transaction type show as "Other" on the dev site.
  • Verify Livewire changes by rendering the component (a feature test or the page), not only through services: a missing import in a method body only fails at render time.
  • Test Livewire methods with ->call(), not ->instance()->method(). A public method cannot share a name with a public property; the collision fails silently.
  • Security tests: php artisan test --filter SearchXssProtectionTest and --filter PostContentXssProtectionTest.

Deployment and upgrades

  • deploy.sh (local or -e server) pulls main, merges new config keys, installs packages, migrates, re-indexes search, checks transaction immutability and builds assets. It ignores mode-only changes and the server-built public/build.
  • Laravel 10 to 11 upgrade of any installation: scripts/upgrade.sh, documented in references/UPGRADE_LARAVEL_10_TO_11.md. Config management: references/CONFIG_MANAGEMENT.md. Production notes: references/PRODUCTION_DEPLOYMENT.md.
  • Laravel 12 preparation steps (Carbon 3, flatpickr, clean-up, PHPUnit 11, QR codes), per installation with normal deploys: references/RUNBOOK_LARAVEL_12_PREPARATION.md (tags l12-prep-<n>-<name> on main).
  • Server-side UPDATE/DELETE grants are given per table. A new table fails with MySQL error 1142 until deploy.sh syncs the grants (except the immutable_tables, e.g. transactions).

Architecture

Stack: Laravel 11.57, Livewire 3.8, Alpine.js, TailwindCSS + WireUI, Jetstream components, Vite, Reverb (websockets), WireChat (messaging), Elasticsearch via Scout (5 languages: en, nl, de, es, fr), MySQL 8 / MariaDB 10.2+ (window functions for balances), Redis. PHP 8.2+.

Profiles and authentication

  • Profile types: User (individual, personal time account), Organization (higher limits), Bank (may create and remove currency), Admin (no payment account).
  • The person is always logged in on the web guard as a User. Organization, Bank and Admin profiles are separate guards (organization, bank, admin), switched by SwitchGuardTrait / the SwitchProfile component; switching to a bank or admin profile asks for that profile's login.
  • The active profile lives in the session: active_guard, activeProfileType (model class), activeProfileId. Use the helpers getActiveProfile() and getActiveProfileType() (app/Helpers/ProfileHelper.php). Auth::user() on the web guard is the person, not the active profile.
  • Admin features check getActiveProfileType() === 'Admin'.

Accounts and transactions

  • Accounts belong polymorphically to Users, Organizations and Banks. Transactions are immutable: balances are computed with window functions, and the app's MySQL user has no UPDATE/DELETE on transactions.
  • Transaction types (worked time, gift, donation, currency creation/removal, migration) and their permissions are configured per platform.

Roles and permissions (two layers that must stay in sync)

  1. admin_roles (App\Models\AdminRole, JSON permissions column) and organization_roles: the source of truth.
  2. Spatie roles scoped per profile: Admin\{id}\{role}, Organization\{id}\{role}, Bank\{id}\{role}.

syncAdminSpatieRolePermissions() in app/Http/Livewire/Roles/Manage.php syncs them when roles are edited in the UI. After manual database changes, in tinker:

$adminRole = \App\Models\AdminRole::where('name', 'content-manager')->first();
\Spatie\Permission\Models\Role::where('name', 'like', '%content-manager')->get()
    ->each(fn ($r) => $r->syncPermissions($adminRole->permissions));
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Admin\3\content-manager', 'guard_name' => 'web'])
    ->syncPermissions($adminRole->permissions);   // when "There is no role named X"
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

Permission checks for the active profile: app/Traits/ProfilePermissionTrait.php.

White-label platforms

Setting up or branding an instance: references/MULTI_PLATFORM_INSTANCES.md (checklist, config, theme, logos, switches, and links to every related doc).

  • One installation per server. TIMEBANK_CONFIG in .env names the platform config config/<name>.php (e.g. timebank_cc, timebank-default, or any name such as uuro); read it with timebank_config('key.path'). The name itself is config('app.timebank_config').
  • Templates: config/timebank_cc.php.example and config/timebank-default.php.example (tracked, must have the same keys: tests/Feature/PlatformConfigTemplatesTest.php). Active configs are not in git; deploy.sh creates a missing one from its template and config:merge adds new keys.
  • Platform wording: platform_name(), platform_users(), platform_currency_*() etc. Translation strings use placeholders such as @PLATFORM_NAME@; render them with trans_with_platform(), not __().
  • Per-platform switches: local_pages, android_app, account_info.* (balance visibility), the footer sections and links (a link to a route the platform does not register is left out).
  • Platform folder platform/ (PLATFORM_PATH; outside git, optional): config.php replaces the platform config, theme.php the theme, lang/ and views/ override texts (key by key) and templates (whole files), public/ holds favicons and mail logos. Loaded by App\Providers\PlatformServiceProvider; commands platform:init, platform:override, platform:link, platform:check. Use theme_email_logo_url() / theme_email_logo_file() for the email logo. Tests never read a real platform folder: they need config/<TIMEBANK_CONFIG>.php, so keep it on a development machine even when platform/config.php exists.
  • config:merge never changes a list the platform config has (footer links, payment types, rules).

Themes

  • TIMEBANK_THEME: timebank_cc (default), uuro, vegetable, yellow, defined in config/themes.php. Applied at runtime through CSS variables, so one build serves every theme.
  • Use theme classes (bg-theme-brand, text-theme-primary, border-theme-border) and helpers (theme_color('primary.500'), theme_font(...), theme_logo(), theme_email_logo(), theme_favicon()).
  • Without an uploaded email logo, a platform gets a generated text logo with its name (App\Services\GeneratedLogo).

UI

  • All new views follow references/STYLE_GUIDE.md; resources/views/livewire/mailings/manage.blade.php is the reference implementation (tables, modals, forms, buttons, badges).
  • Buttons: <x-jetstream.button>, secondary-button, danger-button, light-button.
  • Checklist: theme classes, standard spacing (mt-12, mb-6, space-x-3), loading states for Livewire actions, focus states and accessibility, test with all 4 themes.

Known pitfalls

  • Union count: never ->count() on a union of Eloquent builders (PDO bindings misalign). Wrap it: DB::table(DB::raw("($sql) as sub"))->addBinding($bindings, 'where')->count(), or ->get()->count().
  • Large data: stream or chunk (log files, backups, exports). Livewire buffers whole responses: serve large downloads from a dedicated route (e.g. posts.backup-download), not from a Livewire method.
  • Dropdowns: in Livewire + Alpine dropdowns use @mousedown.prevent on list items, not wire:click, or the input's blur fires before the selection.
  • Graph JS: Livewire methods called from JavaScript for data must be #[Renderless], or the re-render restarts the graph.
  • Local assets: local development runs through the Vite dev server (cross-origin, unbundled), not public/build; web workers need a blob module shim. Check both paths.
  • Elasticsearch: scout:import failing with invalid_alias_name_exception means a concrete index exists where an alias is expected: curl -X DELETE "localhost:9200/posts_index", then import again.
  • Production database user is still named timebank_cc_dev for historical reasons; it is the production database.