# Project Instructions for AI Agents This file provides instructions and context for AI coding agents working on this project. ## Beads Issue Tracker This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. ### Quick Reference ```bash bd ready # Find available work bd show # View issue details bd update --claim # Claim work bd close # Complete work ``` ### Rules - Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run `bd prime` for detailed command reference and session close protocol - Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files ## Session Completion **When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. **MANDATORY WORKFLOW:** 1. **File issues for remaining work** - Create issues for anything that needs follow-up 2. **Run quality gates** (if code changed) - Tests, linters, builds 3. **Update issue status** - Close finished work, update in-progress items 4. **PUSH TO REMOTE** - This is MANDATORY: ```bash git pull --rebase bd dolt push git push git status # MUST show "up to date with origin" ``` 5. **Clean up** - Clear stashes, prune remote branches 6. **Verify** - All changes committed AND pushed 7. **Hand off** - Provide context for next session **CRITICAL RULES:** - Work is NOT complete until `git push` succeeds - NEVER stop before pushing - that leaves work stranded locally - NEVER say "ready to push when you are" - YOU must push - If push fails, resolve and retry until it succeeds ## Build & Test There is **no build step and no package manager**. Plain PHP on both sides; the only third-party code is a vendored copy of Plugin Update Checker under `vendor/plugin-update-checker/` (do not hand-edit it — see README for the refresh procedure). ```bash php tests/run.php # everything php tests/run.php plugin # plugin unit tests only (runs on PHP 7.4+) php tests/run.php server # server integration tests only (needs PHP 8.1+) ``` The suite is a hand-rolled harness in `tests/lib/` — no PHPUnit, no composer, matching the rest of the repo. Two kinds of test: - `tests/plugin_test.php` — unit tests for the pure static helpers on `ATT_HC_Session`. That class calls no WP functions at load time, so defining `ABSPATH` is enough to require it; anything needing a real WordPress runtime is out of scope here. - `tests/server_test.php` — **end-to-end over real HTTP.** The runner copies `server/` to a temp dir, gives it a throwaway SQLite DB and config, and starts `php -S` against it. A developer's real `server/config.php` (gitignored, may point at live MySQL) is never read or touched. Skips itself with a clear message if `pdo_sqlite` is missing. Exit code is non-zero on failure, so it works as a pre-push gate. Add a test with `test('name', function () { ... })` and the `assert_*` helpers in `tests/lib/harness.php`. Also lint the files you touched, **against the right PHP version for that half of the repo** (see Conventions): ```bash # Plugin — must parse on PHP 7.4 find . -path ./vendor -prune -o -path ./server -prune -o -name '*.php' -print \ | xargs -n1 php7.4 -l # fall back to `php -l` if 7.4 isn't installed # Server — PHP 8.1+ find server -name '*.php' -print0 | xargs -0 -n1 php -l ``` Run the history server locally: ```bash cp server/config.php.example server/config.php # set api_key + db_dsn (sqlite is easiest locally) php -S 127.0.0.1:8765 server/dev-router.php curl 127.0.0.1:8765/ # → {"ok":true,...} ``` Use `dev-router.php`, **not** `-t server/public`. Healthcheck ids are dotted `uniqid()` values and PHP's built-in server treats dotted path segments as static file requests, so the plain docroot form 404s on most real URLs. The plugin has to be exercised inside a WordPress install — symlink the repo into `wp-content/plugins/att-site-healthcheck` and use *Tools → Site Healthcheck*. It refuses to run until `ATT_HC_API_URL` / `ATT_HC_API_KEY` are configured (wp-config constants win over the settings-screen options). Server deploy is an rsync of `server/` — see `server/DEPLOY.md`. Migrations run automatically on the first request after deploy. ## Architecture Overview **Two independent deployables live in this one repo**, and they must not leak into each other: 1. **The WordPress plugin** — repo root, `includes/`, `vendor/`. Ships to `wp-content/plugins/`. Installed per engagement and uninstalled at the end. 2. **The central history server** (`server/`) — a standalone PHP app deployed to a VPS. `.gitattributes` marks `server/`, `.beads/`, `.claude/`, `AGENTS.md`, `CLAUDE.md`, and `steps.md` as `export-ignore`, so Gitea's `archive/main.zip` (which *is* the plugin release artifact) excludes them. **Any new non-plugin file at the repo root needs an `export-ignore` line.** ### Plugin - `att-site-healthcheck.php` — header, constants, requires, step discovery on `plugins_loaded`, PUC update checker wiring. - `includes/steps/-.php` — one file per healthcheck step, each `return`ing an `ATT_HC_Step` subclass instance. Adding a step is dropping a file; reordering is renaming the numeric prefix (loaded in `natsort` order); removing is deleting the file. `ATT_HC_Steps` is the singleton registry and applies the `att_hc_steps` filter after discovery. - `ATT_HC_Session` — **the server is the source of truth.** The `att_hc_session` WP option is only a cache of the active session. Every write is write-through: API call first, local cache updated only on success, so the cache can never drift. Cross-engagement history (prior notes, previous-session diff, step counts) is fetched live. - `ATT_HC_Api` — `wp_remote_request` client; every method throws `ATT_HC_Api_Exception`. Refuses non-`https://` endpoints unless loopback. - `includes/admin-page.php` — the whole UI (renderers + `admin_post_*` handlers). - `includes/report.php` — Markdown report builder. Reports are never stored server-side; the technician downloads them. - `includes/recovery-bootstrap.php` / `recovery-installer.php` — step 0 detects and can install the companion `wp-site-recovery` plugin (detected by plugin Name + Author, not folder slug). ### Server (`server/`) Front controller → `src/bootstrap.php` (hand-rolled PSR-4 autoloader for `AttHc\Server\`, error/exception handlers, `config.php` load) → `src/routes.php` → `Router` → `Controllers/`. `Store` holds all SQL; `Db` is a PDO singleton that runs `Migrations::ensureRan()` on first connection. - API routes take `Authorization: Bearer ` (`Auth::require`, `hash_equals`). `GET /` is deliberately public — the plugin pings it to check reachability. - `/dashboard*` is `requiresAuth: false` at the router level because it does its own auth: `?key=` once, then a PHP session cookie, plus a CSRF token on POSTs. - Storage is PDO against **either MySQL 8.0+ or SQLite** — `config.php.example` defaults to MySQL because `pdo_sqlite` is missing on some Ubuntu + Sury PHP combinations. Keep `migrations/*.sql` portable across both (`VARCHAR(N)`, `BIGINT`, no `AUTO_INCREMENT`); driver-specific DDL needs dispatch in `Migrations.php`. ## Conventions & Patterns ### PHP version split — the easiest mistake to make here - **Plugin code (root + `includes/`) must parse on PHP 7.4.** No `str_starts_with` /`str_contains`/`str_ends_with`, no named arguments, no `match`, no nullsafe `?->`, no constructor promotion, no enums. This has been broken by drift before (commit `3c64dd8`) — lint against 7.4, don't just eyeball it. - **Server code (`server/`) targets PHP 8.1+** and freely uses `declare(strict_types=1)`, typed properties, `never`, named arguments, and promotion. ### Plugin - Guard every file with `if (!defined('ABSPATH')) exit;`. - Admin actions go through `admin_post_*` handlers with `current_user_can('manage_options')` + `wp_nonce_field()` / `check_admin_referer()`, and escape on output (`esc_html`/`esc_attr`). - A step's `id()` is a stable contract — it is the key stored in session and server data. Rename files freely, never rename an id once shipped. - Optional step behaviour hangs off `ATT_HC_Step`: `autocheck()` for findings, `render_extra()` + `handle_action()` for step-specific UI/POSTs. - API failures surface as admin notices. History/diff lookups degrade silently (nice-to-have); writes must not. ### Server - Every file: `declare(strict_types=1);` and namespace `AttHc\Server\...`. - All SQL lives in `Store`, always via prepared statements. - Request input goes through `Validate::*`; responses through `Http::json` / `Http::error` (`{"error":…,"code":…}`). - **No composer.** If you think you need a dependency, vendor it or don't. - `server/config.php` is deployment-specific and gitignored — never commit it, and don't echo secrets into responses or logs. ### Repo / release - Remote is **Gitea** (`git.h12e.com/steve/wp-healthcheck`), not GitHub — `gh` and PR workflows don't apply. Work lands on `main`. - Release = bump `Version:` in the plugin header **and** `ATT_HC_VERSION` **and** `version` + `last_updated` in `updates.json`, then push `main`. PUC polls `updates.json` and installs `archive/main.zip`, so whatever is on `main` is what ships. Built zips are gitignored. - `steps.md` is the human technician guide the step files are derived from; keep the two in step when step content changes. - Known open security issues are tracked in beads — `hc-gp3` (API key exposure), `hc-adg` (unpinned recovery-plugin install), `hc-ufl` (no rate limiting / unbounded error log). Check them before touching auth, the settings screen, or the installer.