--- name: agent-app-creator description: Create Agent App applications (PocketBase backend, React kit frontend). Scaffolds, develops, validates, and launches local web apps with persistent state and realtime UI. action-sets: - file_operations - code_execution - agent_app --- # Agent App Creator A Agent App is a self-contained local web app: **one PocketBase process** (data, auth, realtime, custom verbs) serving a **React frontend built from a preset kit**. You declare schema, compose UI, wire verbs — the platform owns the rest. ## Step 0: Have a registered project (MANDATORY FIRST) 1. **Task instruction contains `Project ID` + `Project Path`** → the project is already scaffolded. Use those values. **Skip scaffolding.** 2. **No Project ID in your instruction** (user asked in a regular chat) → call `agent_app_scaffold(name, description, auth_mode)` — it scaffolds AND dispatches the build to the project's dedicated session. Tell the user the build started, then end your turn. Do NOT build in the chat session. Pick `auth_mode` from requirements: `none` (personal local tool — default) or `multi-user` (accounts; the kit's LoginGate wraps the app automatically). ## The ownership rule (the gate enforces this) Edit ONLY: | Path | Purpose | |------|---------| | `frontend/src/app/` | all UI code | | `pb/pb_migrations/` | schema — one NEW migration per change | | `pb/pb_hooks/ops.pb.js` + new `*.pb.js` / `*.js` modules | custom verbs + their helpers | | `operations.json` | declarations for those verbs (non-`system` entries) | | `AGENT_APP.md` | your plan/context/index — keep current | NEVER edit `frontend/src/kit/`, `frontend/src/main.tsx`, `frontend/src/config.gen.ts`, `pb/pb_hooks/_system.pb.js`, `manifest.json`, or build configs — the validation gate hashes them and **fails the build** if they changed. Need a variant of a kit component? Wrap it in `app/`: ```tsx // frontend/src/app/components/DueBadge.tsx import { cn } from '../../kit/index.ts'; export function DueBadge({ overdue }: { overdue: boolean }) { /* compose */ } ``` ## Before coding 0. **If `reference/requirements.md` starts with `MARKETPLACE DECISION: install `** — do NOT build. Call `agent_app_marketplace_install(app_id=..., name=..., description=..., will_adapt=)`. It installs INTO this project (same tab and id — never a duplicate). - `adapt: no` — the install completes the build and the system announces it; do NOT send your own summary and do NOT call notify_ready or walk_verify. End the run. - `adapt: yes` — after the install, apply ONLY the adaptations listed under `## Adaptations` (modify flow: edit → `agent_app_notify_ready` → `agent_app_walk_verify`). If the list says "none specified", ask the user what to change (a FINAL `send_message`) instead of guessing. The user explicitly chose reuse over a fresh build — never rebuild what was just installed, even if a later trigger asks you to "continue" it. 1. Read `agent_file_system/GLOBAL_AGENT_APP.md` — colors, fonts, enforced rules. 2. Read `{project_path}/AGENT_APP.md` and `reference/requirements.md`. The creation wizard interviewed the user and synthesized `requirements.md` — it is the **binding spec**: implement it exactly and mirror its checklist into `AGENT_APP.md`. If it is absent, build from the project description; only ask the user (a FINAL `send_message`, `continue_work=false`) when something is genuinely blocking and you cannot reasonably decide it yourself. 3. **Any feature need data from outside the app? Check, then research.** FIRST check the `[INTEGRATIONS this app can use]` block already in your context — if a connected integration's action covers the feature (email = `send_gmail`), use `bridge.callAction`; nothing to research. Only for THIRD-PARTY public APIs: research like an engineer — endpoint, auth, response shape, limits. Spawn a research_agent; never write an integration hook from memory. - User named an API/service → research it. If it needs a key, tenant URL, or account detail you cannot find online, ask the user (final `send_message`) and build the rest of the app while waiting. - No API named → research candidates and pick a **keyless public API** yourself (e.g. Open-Meteo for weather). Choosing the source is your engineering call — no user round-trip. - Nothing usable exists → build the honest empty/offline state and REPORT the blocker in your final message. **Mock or generated data is forbidden** unless requirements explicitly ask for demo data. 4. A Agent App build is substantial work — the standard run protocol applies as-is (scope, plan, execute, verify, deliver); this skill adds nothing to it. `reference/requirements.md` is the binding spec verification checks against; mirror the feature checklist in `AGENT_APP.md`. ## Per feature: schema → verbs → UI **Schema** — add a new file in `pb/pb_migrations/`. **Never edit AND never rename or delete a migration that has been applied** (i.e. after any successful launch): the filename is the identity in the live database. Renaming one makes every boot re-run its "new" replacement into the existing schema — PocketBase exits before serving anything and the app cannot start until the original filename is restored. Fixing a migration's mistake = writing a NEW migration that alters the collection. The ONLY top-level call is `migrate(upFn, downFn)` — the down/rollback function is the **second argument**. A top-level `rollback(...)` does not exist and panics the whole PocketBase process at load. Follow the starter migration's pattern exactly: field types, `autodate` created/updated, and rules matching the project's `authMode` (`manifest.json`): `''` open rules for `none`; `@request.auth.id != ""` (or owner-scoped `owner = @request.auth.id` with a `relation` to `users`) for `multi-user`. **Seeding records in a migration:** `new Record(...)` takes the **Collection OBJECT — never an id string**. Passing `someCollection.id` nil-panics PocketBase internally and can WEDGE the process (alive, silent, never serving). The gate kills and reports it, but write it right: ```js const locations = app.findCollectionByNameOrId('locations'); // the OBJECT const record = new Record(locations); record.set('city_name', 'Manchester'); app.save(record); ``` **Relation fields — the #1 migration mistake:** `collectionId` must be the target collection's **ID, never its name**. Save the target collection first, then reference it: ```js const words = new Collection({ name: 'words', /* … */ }); app.save(words); const reviews = new Collection({ name: 'reviews', fields: [ { name: 'word', type: 'relation', required: true, collectionId: app.findCollectionByNameOrId('words').id, cascadeDelete: true }, /* … */ ], }); app.save(reviews); ``` **Custom verbs** — anything beyond CRUD is a `routerAdd` route in `pb/pb_hooks/ops.pb.js` PLUS a matching entry in `operations.json` (see the working `items.clear-done` example). The gate fails ops without routes and warns about routes without ops. Mark data-deleting ops `"destructive": true`. Plain CRUD needs no verb — the PB API and the kit hooks already cover it. **Request bodies in hooks: `e.requestInfo().body` ONLY** (a pre-parsed object). `toString(e.request.body)` reads a Go stream as EMPTY — your handler will 400 on every request and the error will falsely blame the client. **Naming: kebab-case everywhere, all three places must agree** — the op `name` in operations.json, the `routerAdd` path in pb_hooks, and every frontend call: `"plan.generate"` ↔ `/api/ops/plan-generate` ↔ `fetch('/api/ops/plan-generate')`. Pick the names once, before writing any of the three. **Load-time calls must survive an EMPTY database.** A fresh app has no records: never call ops or filtered queries at page load that 400 without data — gate them behind existence checks (e.g. only call plan ops after a profile exists). The launch verifier fails the app on any first-paint console error. **External data (third-party APIs)** — Agent Apps CAN call the internet, from **hooks only** (never the frontend: browser CORS breaks and keys would be visible). Use `$http.send`. **THE #1 HOOK TRAP — handlers run in ISOLATED VMs.** Code inside a `routerAdd`/`cronAdd`/`onRecord*` callback **cannot see file-level `const`s or functions**: it throws `X is not defined` at REQUEST time, which the gate (registration-time only) cannot catch. Share logic via a plain `.js` module and `require()` it INSIDE each callback — module scope IS visible within the module: ```js // pb/pb_hooks/weather.js — a MODULE (plain .js, not .pb.js) const OPEN_METEO = 'https://api.open-meteo.com/v1/forecast'; // literal → recorded as egress function refreshAll(app) { const res = $http.send({ url: OPEN_METEO + '?latitude=53.48&longitude=-2.24¤t=temperature_2m,wind_speed_10m', method: 'GET', timeout: 20, // ALWAYS set a timeout }); if (res.statusCode !== 200) { throw new Error('weather source returned HTTP ' + res.statusCode); } const data = res.json; // ONLY correct way to read the body — pre-parsed. // res.body is a Go BYTE SLICE: JSON.parse(String(res.body)) throws // "SyntaxError: Unexpected token at the end" on every response. If you // remember fetch-style res.body/JSON.parse, that is the WRONG API here. // …store readings via app.save(...) and return them } module.exports = { refreshAll: refreshAll }; ``` ```js // pb/pb_hooks/ops.pb.js — the route + the scheduled job use the SAME code path routerAdd('POST', '/api/ops/weather-refresh', (e) => { const weather = require(`${__hooks}/weather.js`); // require INSIDE the handler try { return e.json(200, { updated: weather.refreshAll(e.app).length }); } catch (err) { console.error('weather-refresh failed:', err); // → logs/pocketbase.log — ALWAYS return e.json(502, { error: String(err) }); // log the CAUSE before the 502; } // the browser only sees the status }); cronAdd('weatherSync', '*/15 * * * *', () => { const weather = require(`${__hooks}/weather.js`); try { weather.refreshAll($app); } catch (err) { console.error('weatherSync failed:', err); } }); ``` - **Current PB API only:** `app.findRecordsByFilter(...)`, `app.save(...)`, `app.delete(...)`. `$app.dao()` does **NOT exist** in this PocketBase — it throws `Object has no member 'dao'`. If you remember `.dao()` from tutorials, your memory is a major version out of date; copy the working `items.clear-done` example instead. - **PB find helpers THROW on no rows — they never return null.** `findFirstRecordByFilter`/`findRecordById` on zero matches throws NotFound, which surfaces as a bare 404 response. `if (!rec)` after them is dead code. Wrap in try/catch (catch = "not found") or use `findRecordsByFilter(collection, filter, sort, LIMIT, OFFSET)` and check `.length`. Corollary when debugging: **a 404 from a route you declared means your HANDLER threw, not that the route is missing** — check logs/pocketbase.log for the `[handler-error]` line with the real cause. - Keep base URLs as string **literals** in the module (the tooling records the app's external hosts in the manifest from them). - Unreachable source / non-200 → `console.error` the cause, return a clean error; the UI shows its offline/empty state. **NEVER substitute generated or random data for real data** — a mock that renders is a lie that passes review. If the source cannot be reached, the app says so and so do you. - CraftBot's own connected services (Gmail, Slack, Notion, …) are NOT called this way — see `references/INTEGRATIONS.md` (the `_craftbot_bridge.js` helper). Third-party public APIs: direct `$http.send` as above. **UI** — build in `frontend/src/app/`, importing ONLY from `../kit/index.ts`: - Read data with `useCollection('name', { sort: '-created' })` — it is **realtime**; never poll, never reload. - Write with `await getPbClient().call((pb) => pb.collection('name').create(...))` — failures toast automatically. - Components: `Button, Input, Card/CardHeader/CardBody, Dialog, Table, LoginGate`, plus `toast` for feedback and `useAuth()` in multi-user apps. - Style with Tailwind utilities + kit tokens (`var(--agent-app-*)`). Never hardcode colors — theming is host-owned (style packs + dark mode must keep working). - Required UX: empty states with an action, loading states, confirmation dialogs for destructive actions, toasts on CRUD, responsive layout. Update `AGENT_APP.md` after each feature (entities table, ops list, checklist). **App→agent triggers** — when a feature needs the AGENT to react to something happening in the app (a button that asks the agent to act, backend logic that crossed a threshold), declare it in `triggers.json` and fire it via the kit's `fireAgentTrigger` (frontend) or `_triggers_lib.js`'s `fire()` (hooks) — see `references/TRIGGERS.md` for the manifest format, the trust rules, and the design rules (idempotent instructions, generous cooldowns). Declare a trigger only where agent judgment adds value — plain code handles plain events. ## Finish: launch, then verify 1. `agent_app_notify_ready(project_id="")` — runs the gate (**types → build → migrations-on-fresh-db → ops → ownership**), then starts your code in the DEV environment (a copy on a hidden port with a fresh post-migration DB) and health-checks it. Its message gives you the dev URL and dev dir — test and read logs THERE; keep editing in the real project dir (each notify_ready syncs your edits in). On errors: read ALL of them, fix ALL of them, call it again. Success = app RUNNING (in dev) but NOT yet verified. Never start servers manually. 2. **REALITY CHECK — look at what actually exists, not at what you wrote.** Success messages lie by omission; stored state does not. While the app runs: - `GET /api/_a2app/describe` → does every collection show the FIELDS you migrated? A collection showing only `id` means your migration silently did nothing (wrong key, wrong API — the cause doesn't matter, the emptiness is the proof). - Trigger one real data flow (call your refresh/main op), then read a record back (`GET /api/collections//records?perPage=1`) and LOOK at the values. Missing fields, empty strings, all-zero numbers = the write silently failed, whatever the op's status code said. - Any path you CANNOT trigger for real (scheduled email, posts to the user's accounts): **dry-run it** — `callAction(name, sameParams, { confirmIrreversible: true, dryRun: true })` validates grant, params, placeholders and confirmation without executing. A path that was never run NOR dry-run is not done, whatever the code looks like. Reason about ANY mismatch between what you intended and what is stored — fix it before verifying. This catches the failure classes no error message reports. 3. `agent_app_walk_verify(project_id="")` — an independent sub-agent walks the running app in a real (headless) browser against `reference/requirements.md`. **Success announces the app to the user and completes the build.** Failing features come back as a report: fix them, then repeat step 1 and step 3. Test data is fine during the build: you are working in the DEV environment, whose database is disposable — at delivery the platform boots the LIVE app with a fresh database built purely from your migrations, so records you or the verifier created never reach the user. Data your migrations SEED survives (they run on the fresh live DB) — put anything the user must see on first open in a migration, never insert it by hand. Externally-fetched data does not carry over either: an app that syncs from an API must self-populate on an empty DB (fetch at boot or when the collection is empty — never rely on a sync that happened during the build). **HONESTY RULE:** the app is ready ONLY when `agent_app_walk_verify` returns `status: success`. If you cannot make it pass, tell the user the build **failed** and exactly what's blocking. Never claim a broken app is ready, and never present generated data as live data — "live" in your message means the app fetched it from the real source. ## Debugging - Full platform reference (bridge, jobs, kit API): `agent-app/docs/agent-guide.md` (repo-level, read on demand). - The RUNNING instance is the dev copy — its logs live in the dev dir that `agent_app_notify_ready` reported, not in the project dir: `{dev_dir}/logs/frontend_console.log` (console.error/warn + uncaught errors are auto-relayed) and `{dev_dir}/logs/pocketbase.log`. - Data inspection: the PB REST API on the dev port notify_ready returned (`GET /api/collections//records`). `GET /api/_a2app` answers `env: "dev"` if you need to confirm which instance a port is. ### Fix rounds Failing features come back as a fix brief: defect cards with evidence, plus an **ATTEMPT LOG** — every previous round, the cause signature of each defect, what moved between rounds (`cause identical`, `cause changed`, `gone`, `new`) and any streak across them. It reports and stops; reading it is yours, and so is how you spend the round. Two things you can write into that record. Each round is a fresh run that remembers nothing of the last one, so what is not written here is not known next round: - `agent_app_report_finding(project_id, ruled_out=["not the grant — dry-run of send_gmail returns 200"])` — causes you eliminated, and what eliminated them. Quoted back in every later brief. - `agent_app_report_finding(project_id, blocked_question="…")` — ends the work and puts one question to the user. For something you cannot GET (a decision, an account, a credential), not something you have not solved. Repeating a failure does not end the build; only the mission budget does. ## FORBIDDEN - Editing system-managed files (see ownership rule) — the gate will fail - Editing an already-applied migration — add a new one - Custom fetch layers, polling, or page reloads — use the kit's realtime hooks - Hardcoded colors or raw `