Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions conformance/harness/src/toolkits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ const TOOLKITS: ToolkitSpec[] = [
noBuild: true,
incompleteBecause: "ships the requirements template unfilled; the spec is the author's to write",
},
// Ships the adapter hooks and a React View, but its `tasks` collection is
// created by hand in PocketBase, and `install` prints "download the
// pocketbase binary" — so a fresh scaffold has neither data model nor runtime.
// Ships the adapter hooks, the starter migration and a React View, but
// `install` prints "download the pocketbase binary" rather than fetching it,
// so a fresh scaffold has no runtime until someone does.
{
id: "blueprint-pocketbase-react",
noBuild: true,
incompleteBecause: "ships adapter hooks and a React View; collections and the binary are added by hand",
incompleteBecause: "ships adapter hooks, a starter migration and a React View; the PocketBase binary is added by hand",
},
// The native-port blueprints (Go, Rust, Ruby): runtime, model and View are
// complete, but each ships the requirements template unfilled — the spec is
Expand Down
2 changes: 1 addition & 1 deletion framework/cli/src/lib/agentState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const RULE_POINTER =

const SKIP_DIRS = new Set([
"node_modules", ".git", ".a2app", ".lui", "dist", "build", "out", "target", "public",
"data", "pb_data", "vendor", "tmp", "log", "logs", "coverage", "__pycache__", ".venv",
"data", "pb_data", "pb_public", "vendor", "tmp", "log", "logs", "coverage", "__pycache__", ".venv",
"venv", ".next", ".nuxt", ".svelte-kit", ".turbo", ".cache",
"test", "tests", "__tests__", "spec",
]);
Expand Down
5 changes: 5 additions & 0 deletions framework/cli/test/agent-state.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,14 @@ try {
const r = app("deps-and-tests", {
"node_modules/pkg/index.js": RUNNER,
"dist/assets/app.js": RUNNER,
"pb/pb_public/assets/index.js": "fetch('/api/_a2app/tasks/' + id);",
"test/ask.test.mjs": RUNNER,
"src/App.jsx": "export default () => <p />;",
});
check("dependencies, build output and tests are not scanned", r.triggers.length, 0);
// PocketBase's compiled View: it holds whatever the source imported, so it
// must not stand in for View code that follows the task.
check("a built bundle does not count as showing the work", r.followers, []);
}

/* ------------------------------- the shipped starters pass their own gate */
Expand All @@ -167,6 +171,7 @@ try {
["blueprint-go-react", true],
["blueprint-rust-react", true],
["blueprint-rails-vue", true],
["blueprint-pocketbase-react", true],
["blueprint-python-fastapi", false],
]) {
// A raw template has no ownership canon yet; the toolkit's systemPaths are
Expand Down
4 changes: 4 additions & 0 deletions toolkits/blueprint-pocketbase-react/a2app.toolkit.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
{
"name": "hooks syntax + rules self-test",
"run": "node --check pb/pb_hooks/_a2app_rules.js && node --check pb/pb_hooks/_a2app_impl.js && node --check pb/pb_hooks/_a2app.pb.js && node pb/pb_hooks/_a2app_rules.js --selftest"
},
{
"name": "operations resolve (every declared op has a runner)",
"run": "node -e \"const fs=require('fs');const ops=fs.existsSync('operations.json')?(JSON.parse(fs.readFileSync('operations.json','utf8')).operations||[]):[];const has=fs.existsSync('pb/pb_hooks/operations.js');const r=has?(require('./pb/pb_hooks/operations.js').runners||{}):{};const missing=ops.filter(o=>typeof r[o.name]!=='function').map(o=>o.name);if(missing.length){console.error('declared in operations.json with no runner in pb/pb_hooks/operations.js: '+missing.join(', '));process.exit(1)}process.stdout.write('operations resolve ok\\n')\""
}
],
"lifecycle": {
Expand Down
41 changes: 29 additions & 12 deletions toolkits/blueprint-pocketbase-react/template/AGENT_APP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
## Plan
PocketBase backend + a React (Vite) frontend, with the
A2App adapter as in-process PocketBase JS hooks (`pb/pb_hooks/_a2app*.js`).
PocketBase serves records natively; the adapter adds identity + describe and a
guard that validates the raw body before PocketBase coerces it. The View lives
in `ui/` and is compiled into `pb/pb_public/` by the pipeline build. Evolve the
app by editing collections (migrations) and `ui/src/` — never the hook files
(system-owned, hash-locked).
PocketBase serves records natively; the adapter adds identity + describe, a
guard that validates the raw body before PocketBase coerces it, declared
operations, and the app→agent task queue. The View lives in `ui/` and is
compiled into `pb/pb_public/` by the pipeline build. Evolve the app by editing
collections (migrations), `operations.json` + `pb/pb_hooks/operations.js`, and
`ui/src/` — never the `_a2app*` hook files (system-owned, hash-locked).

## Modules
Modules are the organizing unit: every entity and every operation belongs to
Expand All @@ -17,23 +18,39 @@ cannot carry a module of their own, so `manifest.json` maps them: each
- **planning** — tasks and the work in front of you.

## Entities
Define collections in PocketBase (via migrations or the admin UI). `describe`
derives entities from the live collection schema, so it cannot drift. A starter
`tasks` collection is expected (title, status, due, created).
Define collections in PocketBase migrations (`pb/pb_migrations/`). `describe`
derives entities from the live collection schema, so it cannot drift.
- **tasks** — a to-do item, created by the starter migration. Fields: `title`
(string, required), `status` (enum: todo/doing/done), `due` (day key
YYYY-MM-DD), `notes` (string), `agentTask` (the queue task an agent is, or
was last, working on; set by `request-triage`, declared read-only in
`pb/pb_hooks/operations.js`).

## Operations
- **archive-done** — archive every completed task. `destructive`.
Declared in `operations.json`, run by the same-named runner in
`pb/pb_hooks/operations.js`.
- **clear-done** — deletes every task whose status is done. `destructive`.
- **count-tasks** — counts the tasks. `readOnly`, `idempotent`.
- **complete-task** — marks one task done. Acts on `tasks`; available while the
task is not already done.
- **request-triage** — asks an agent to work out what one task needs. Queues a
`triage` task (event `task.needs_triage`) and records its id in `agentTask`;
the View shows the work until it is done.

## Conventions
- The adapter is the only agent surface; the guard validates the raw body
(`onRecordCreateRequest`/`onRecordUpdateRequest`), before coercion.
- An operation runner is a transaction: write through `a2app.app`, queue agent
work with `a2app.trigger`, and keep slow outbound calls out of it.
- Day-key text fields named like dates (≤12 chars) are advertised with
`format: YYYY-MM-DD`.
- Migrations are additive; never drop a collection that holds data.
- Migrations are additive; never edit an applied one, never drop a collection
that holds data.
- The design system lives in `ui/public/tokens.css` (kit sheet: foundation →
semantic bridge); components consume ONLY semantic tokens. Shared pieces
(Icon, `useToast`, `useConfirm` — never `window.confirm`) live in `ui/src/`;
compose them, never re-implement per screen.
(Icon, `useToast`, `useConfirm` — never `window.confirm`, `AgentTask.jsx` for
work queued for an agent) live in `ui/src/`; compose them, never re-implement
per screen.
- The View is served BUILT: `npm --prefix ui run build` → `pb/pb_public/`.
Editing `ui/src/` changes nothing a browser sees until the next build.

Expand Down
49 changes: 47 additions & 2 deletions toolkits/blueprint-pocketbase-react/template/operations.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,56 @@
{
"operations": [
{
"name": "archive-done",
"description": "Archive every completed task.",
"name": "clear-done",
"description": "Delete every task whose status is done.",
"destructive": true,
"module": "planning",
"params": {}
},
{
"name": "count-tasks",
"description": "Count the tasks.",
"destructive": false,
"readOnly": true,
"idempotent": true,
"module": "planning",
"params": {}
},
{
"name": "complete-task",
"description": "Mark one task done.",
"destructive": false,
"module": "planning",
"entity": "tasks",
"appliesWhen": {
"field": "status",
"ne": "done"
},
"params": {
"task": {
"type": "ref",
"entity": "tasks",
"required": true
}
}
},
{
"name": "request-triage",
"description": "Ask an agent to work out what this task actually needs.",
"destructive": false,
"module": "planning",
"entity": "tasks",
"appliesWhen": {
"field": "status",
"ne": "done"
},
"params": {
"task": {
"type": "ref",
"entity": "tasks",
"required": true
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@
* The only things a handler may use are the PocketBase globals and `require()`.
* So every handler below is a self-contained one-liner that `require()`s the
* implementation module INSIDE its own body and calls into it; all the logic —
* identity, describe, and the create/update guard — lives in `_a2app_impl.js`.
* identity, describe, the record guards, operations, and the app→agent task
* queue and event log — lives in `_a2app_impl.js`.
* (`require()` is cached per runtime, so the module loads once per pooled VM.)
*
* Runtime: PocketBase JSVM (Goja) on the 0.23+ hook API (binary pinned to
* v0.26.x). `node --check` validates syntax in the gate; this file only executes
* inside `pocketbase serve`.
*/

/* -------------------------------------------------------------- bootstrap */

// The queue's own tables (tasks, events, approval keys) in data.db. Idempotent,
// and after `e.next()` so the database is open.
onBootstrap((e) => {
e.next();
require(`${__hooks}/_a2app_impl.js`).ensureStore(e.app);
});

/* --------------------------------------------------------------- identity */

routerAdd("GET", "/api/_a2app", (e) => require(`${__hooks}/_a2app_impl.js`).identity(e));
Expand Down Expand Up @@ -45,6 +55,27 @@ routerAdd("GET", "/api/_a2app/describe/{p1}/{p2}/{p3}/{p4}", (e) =>
]),
);

/* ------------------------------------------------------------ operations */

// A declared operation (operations.json), run by the app's own runner in
// pb/pb_hooks/operations.js. Destructive ones answer 428 + an approval key first.
routerAdd("POST", "/api/ops/{name}", (e) =>
require(`${__hooks}/_a2app_impl.js`).runOperation(e, e.request.pathValue("name")),
);
routerAdd("GET", "/api/ops/{name}", (e) => require(`${__hooks}/_a2app_impl.js`).operationMethod(e));

/* --------------------------------------------------- app→agent: tasks, events */

routerAdd("GET", "/api/_a2app/tasks", (e) => require(`${__hooks}/_a2app_impl.js`).listTasks(e));
routerAdd("GET", "/api/_a2app/tasks/{id}", (e) =>
require(`${__hooks}/_a2app_impl.js`).getTask(e, e.request.pathValue("id")),
);
routerAdd("POST", "/api/_a2app/tasks/{id}/{action}", (e) =>
require(`${__hooks}/_a2app_impl.js`).taskAction(e, e.request.pathValue("id"), e.request.pathValue("action")),
);
routerAdd("GET", "/api/_a2app/events", (e) => require(`${__hooks}/_a2app_impl.js`).listEvents(e));
routerAdd("GET", "/api/_a2app/context", (e) => require(`${__hooks}/_a2app_impl.js`).context(e));

/* ----------------------------------------------------------------- guard */

// Validate the RAW submitted body before PocketBase coerces it, on both create
Expand Down
Loading
Loading