208 lines
14 KiB
Markdown
208 lines
14 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```php
|
|
$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.
|