From 51aa56d6fcea68fb7a3c1aa188a90b186273ded8 Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Tue, 3 Mar 2026 13:01:07 -0500 Subject: [PATCH 01/10] docs: contributor setup, data architecture standard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CONTRIBUTOR_SETUP: dev DB only in Docker; API/client in separate terminals for visible logs and hot reload; port 5433 note; db:push not db:migrate; pre-v1 workflow; link to data-architecture - Replace database.md with data-architecture.md: schema-first philosophy, snake_case→camelCase conversion, pre-v1 vs v1 workflow, key tables - AGENTS.md: pre-v1 push workflow wording - architecture.md: link to data-architecture standard - ZAZZ-FRAMEWORK.md: update example to data-architecture.md Made-with: Cursor --- .zazz/standards/architecture.md | 2 +- .zazz/standards/data-architecture.md | 43 +++++++++ .zazz/standards/database.md | 12 --- .zazz/standards/index.yaml | 4 +- AGENTS.md | 2 +- CONTRIBUTOR_SETUP.md | 129 +++++++++++++++++++++++---- docs/ZAZZ-FRAMEWORK.md | 4 +- 7 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 .zazz/standards/data-architecture.md delete mode 100644 .zazz/standards/database.md diff --git a/.zazz/standards/architecture.md b/.zazz/standards/architecture.md index 2603cc2c..e5330089 100644 --- a/.zazz/standards/architecture.md +++ b/.zazz/standards/architecture.md @@ -8,6 +8,6 @@ ## Patterns -- Schema-first DB (Drizzle schema in `api/lib/db/schema.js`); no migrations in this phase—`npm run db:reset` drops and recreates +- Schema-first data design; see [data-architecture.md](./data-architecture.md) - All DB access via `databaseService`; no direct DB access in routes - Project → Deliverable → Task hierarchy diff --git a/.zazz/standards/data-architecture.md b/.zazz/standards/data-architecture.md new file mode 100644 index 00000000..eb630afd --- /dev/null +++ b/.zazz/standards/data-architecture.md @@ -0,0 +1,43 @@ +# Data Architecture + +## Design philosophy: schema-first + +**Schema-first** means the database schema is the source of truth and is defined before implementation. The schema defines the data contract; services, routes, and client code are built against it. We do not infer schema from code or evolve it ad hoc. + +- Schema lives in `api/lib/db/schema.js` (Drizzle) +- All DB access goes through `databaseService`; routes never touch the DB directly +- New features: define or extend schema first, then implement services and routes + +## Technology + +- **Engine**: PostgreSQL 15 +- **ORM**: Drizzle +- **Schema location**: `api/lib/db/schema.js` (single source of truth) + +## Development workflow: pre-v1 vs v1 + +**Pre-v1** (current): We push the schema directly. No migration files. Schema changes are frequent; reset is the primary way to apply them. + +- `npm run db:reset` — drop tables, push schema, seed (destructive) +- `npm run db:push` — push schema changes without dropping (preserves data) +- `npm run db:seed` — seed only (tables must exist) + +**At v1**: We will switch to migrations for production upgrades. Migration files will be generated from schema changes and run in order. `db:reset` will remain for local dev; production will use `db:migrate`. + +## Conventions + +- **Table names**: UPPER_CASE (e.g. `PROJECTS`, `TASKS`) +- **Columns**: snake_case in DB, camelCase in JS +- **Automatic conversion**: `databaseService` converts returned rows via `keysToCamelCase` (`api/src/utils/propertyMapper.js`); the API and client always receive camelCase +- **Task positions**: sparse numbering (e.g. 10, 20) for reordering +- **System enums**: `pgEnum` for fixed values (e.g. `task_relation_type`); user-definable values use `varchar` + +## Key tables + +- `PROJECTS` — `id` (serial PK), `code`, `deliverable_status_workflow`, `status_workflow`, `next_deliverable_sequence` +- `DELIVERABLES` — `id` (serial PK), `deliverable_id` (varchar, e.g. ZAZZ-1), `ded_file_path`, `plan_file_path`, `prd_file_path`, `status_history` +- `TASKS` — `id` (serial PK), `deliverable_id` FK; no separate `task_id` varchar +- `TASK_RELATIONS` — `DEPENDS_ON`, `COORDINATES_WITH` +- `USERS`, `TAGS`, `STATUS_DEFINITIONS`, `COORDINATION_TYPES`, `TRANSLATIONS`, `IMAGE_METADATA`, `IMAGE_DATA` + +Full schema: [api/lib/db/schema.js](../../api/lib/db/schema.js) (from repo root) diff --git a/.zazz/standards/database.md b/.zazz/standards/database.md deleted file mode 100644 index a6822339..00000000 --- a/.zazz/standards/database.md +++ /dev/null @@ -1,12 +0,0 @@ -# Database - -- **Engine**: PostgreSQL 15 -- **ORM**: Drizzle -- **Schema**: `api/lib/db/schema.js` (single source of truth) -- **Reset**: `npm run db:reset` (from `api/`) drops and recreates from schema, then seeds - -## Key tables - -- `PROJECTS` — `deliverable_status_workflow`, `status_workflow`, `next_deliverable_sequence` -- `DELIVERABLES` — `deliverable_id` (e.g. ZAZZ-1), `ded_file_path`, `plan_file_path`, `status_history` -- `TASKS` — `deliverable_id` FK, integer `id` only (no task_id varchar) diff --git a/.zazz/standards/index.yaml b/.zazz/standards/index.yaml index 094df415..dbad46c2 100644 --- a/.zazz/standards/index.yaml +++ b/.zazz/standards/index.yaml @@ -9,5 +9,5 @@ standards: purpose: Primary and secondary languages, runtimes, versions - file: coding-styles.md purpose: Coding conventions, style guides, patterns - - file: database.md - purpose: Database type, ORM, schema conventions + - file: data-architecture.md + purpose: Database design philosophy, schema-first, ORM, conventions diff --git a/AGENTS.md b/AGENTS.md index 0daacae0..7c681ee5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,7 @@ Manual API test token: `TB_TOKEN: 550e8400-e29b-41d4-a716-446655440000` ## Database -- **Schema**: `api/lib/db/schema.js` (Drizzle). No migrations; reset = drop + push. +- **Schema**: `api/lib/db/schema.js` (Drizzle). Pre-v1: no migrations — we push the schema directly (`db:push` / `db:reset`). At v1 we'll switch to migrations for production upgrades. - **Reset (dev)**: From `api/` run `npm run db:reset` (runs reset-and-seed: drop tables/enums → drizzle-kit push → seed). - **Seeding order**: users → tags → status definitions → coordination requirement definitions → translations → projects → deliverables → tasks → task-tags → task relations. To add seed data: add or edit files in `scripts/seeders/` and register in `reset-and-seed.js` and `seed-all.js` in dependency order. diff --git a/CONTRIBUTOR_SETUP.md b/CONTRIBUTOR_SETUP.md index ee532883..6d80e568 100644 --- a/CONTRIBUTOR_SETUP.md +++ b/CONTRIBUTOR_SETUP.md @@ -1,40 +1,139 @@ # Contributor setup -This guide is for developers/committers working on the codebase locally. + +This guide is for developers/committers working on the codebase locally. **Only the database runs in Docker**; the API and client run natively so you get visible logs and hot reload on frontend changes. + ## Prerequisites + - Node.js 22+ -- Docker Desktop or Colima -## 1) Install local dependencies +- Docker Desktop or Colima (for Postgres only) + +## 1) Install dependencies + From repo root: + ```bash npm install npm install --workspace=api cd client && npm install --legacy-peer-deps && cd .. cp api/.env.example api/.env ``` -## 2) Run app stack + +## 2) Configure environment + +Edit `api/.env` and ensure both URLs use port **5433** and password `password`: + +``` +DATABASE_URL=postgres://postgres:password@localhost:5433/zazz_board_db +DATABASE_URL_TEST=postgres://postgres:password@localhost:5433/zazz_board_test +``` + +## 3) Start the database (Docker) + +From repo root: + +```bash +npm run docker:up:db +``` + +Verify Postgres is running: + +```bash +docker ps | grep zazz_board_postgres +``` + +## 4) Create test database (one-time) + +Tests use a separate DB. Create it and apply schema once: + +```bash +docker exec zazz_board_postgres psql -U postgres -c "CREATE DATABASE zazz_board_test;" 2>/dev/null || true +npm run db:reset +cd api && DATABASE_URL=postgres://postgres:password@localhost:5433/zazz_board_test npm run db:reset +cd .. +``` + +## 5) Run API and client (separate terminals) + +**Do not** run the full stack in Docker. Run API and client locally in **two terminals** so you see logs and get hot reload. + +**Terminal 1 — API** + ```bash -docker compose up --build +npm run dev:api ``` + +**Terminal 2 — Client** + +```bash +npm run dev:client +``` + Local URLs: -- API: `http://localhost:3030` -- Client: `http://localhost:3001` -## 3) Run tests (API) + +- API: http://localhost:3030 +- Client: http://localhost:3001 + +**Why separate terminals?** You get live API logs, client build output, and Vite’s hot module replacement so frontend changes auto-reload in the browser. + +## 6) Reset dev database + +From repo root: + +```bash +npm run db:reset +``` + +## 7) Run tests + From `api/`: + ```bash set -a && source .env && set +a && NODE_ENV=test npm run test ``` -## 4) Common DB commands + +Or from root: + +```bash +cd api && set -a && source .env && set +a && NODE_ENV=test npm run test +``` + +## 8) Common DB commands + From repo root: + ```bash -npm run db:reset -npm run db:seed -npm run db:migrate +npm run db:reset # Drop tables, push schema, seed (destructive) +npm run db:seed # Seed only (tables must exist) +npm run db:push # Push schema changes without dropping (preserves data) ``` -## 5) Reset local Docker DB (destructive) + +Pre-v1 we push the schema directly (`db:push` / `db:reset`), no migration files — schema changes are still frequent. At v1 we'll switch to migrations for production upgrades. See [.zazz/standards/data-architecture.md](.zazz/standards/data-architecture.md) for database design philosophy (schema-first). + +## 9) Reset local Docker DB (destructive) + +To wipe Postgres data and start fresh: + ```bash docker compose down -v -docker compose up --build +npm run docker:up:db +npm run db:reset ``` + +Re-run step 4 if you need the test database again. + +## Worktree workflow (mandatory) + +This repo uses **worktrees** for feature work. See [AGENTS.md](./AGENTS.md) and `.cursor/rules/worktree-workflow.mdc`: + +- Main worktree is read-only. +- Create a worktree per branch: `git worktree add -b ../ main` +- Copy `api/.env` from main into the new worktree. +- Push branch → merge on GitHub → pull main locally. Never merge into main locally. + ## Notes + +- Dev Postgres runs on port **5433** (prod uses 5432) so you can run both dev and production on the same machine without port conflicts. - Default DB password is `password`. -- If dependencies in `client/` fail due peer resolution, re-run with `--legacy-peer-deps`. +- If client dependencies fail due to peer resolution, re-run with `--legacy-peer-deps`. +- Port in use: `lsof -ti:3030 | xargs kill -9` (API), `lsof -ti:3001 | xargs kill -9` (client). +- Manual API token (seed): `550e8400-e29b-41d4-a716-446655440000` diff --git a/docs/ZAZZ-FRAMEWORK.md b/docs/ZAZZ-FRAMEWORK.md index 6b1459e8..fe4eb9c4 100644 --- a/docs/ZAZZ-FRAMEWORK.md +++ b/docs/ZAZZ-FRAMEWORK.md @@ -169,8 +169,8 @@ standards: purpose: Primary and secondary languages, runtimes, versions - file: coding-styles.md purpose: Coding conventions, style guides, patterns - - file: database.md - purpose: Database type, ORM, schema conventions + - file: data-architecture.md + purpose: Database design philosophy, schema-first, ORM, conventions # ... other project-wide standards ``` From 50996a4446685dc8626bfbedf706a79547c43e3c Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Tue, 3 Mar 2026 13:07:02 -0500 Subject: [PATCH 02/10] docs: system-architecture, fix validation stack; remove unused zod - Replace architecture.md with system-architecture.md: JS-only stack, AJV (not Zod), Mantine, @xyflow/react, optional cloud deployment (S3/GCS) - Remove unused zod dependency from api - Regenerate package-lock.json Made-with: Cursor --- .zazz/standards/architecture.md | 13 ----------- .zazz/standards/index.yaml | 4 ++-- .zazz/standards/system-architecture.md | 30 ++++++++++++++++++++++++++ api/package.json | 3 +-- docs/ZAZZ-FRAMEWORK.md | 6 +++--- package-lock.json | 12 +---------- 6 files changed, 37 insertions(+), 31 deletions(-) delete mode 100644 .zazz/standards/architecture.md create mode 100644 .zazz/standards/system-architecture.md diff --git a/.zazz/standards/architecture.md b/.zazz/standards/architecture.md deleted file mode 100644 index e5330089..00000000 --- a/.zazz/standards/architecture.md +++ /dev/null @@ -1,13 +0,0 @@ -# Architecture - -## Layers - -- **API**: Fastify routes, JSON Schema validation, auth middleware -- **Services**: `databaseService` (Drizzle), `tokenService` -- **Client**: React, Vite, Mantine, react-router-dom - -## Patterns - -- Schema-first data design; see [data-architecture.md](./data-architecture.md) -- All DB access via `databaseService`; no direct DB access in routes -- Project → Deliverable → Task hierarchy diff --git a/.zazz/standards/index.yaml b/.zazz/standards/index.yaml index dbad46c2..b0a4a961 100644 --- a/.zazz/standards/index.yaml +++ b/.zazz/standards/index.yaml @@ -1,8 +1,8 @@ # .zazz/standards/index.yaml # Index of atomic project standards. Order matters for agent consumption. standards: - - file: architecture.md - purpose: System architecture, layering, design patterns + - file: system-architecture.md + purpose: System architecture, stack, layering, cloud deployment - file: testing.md purpose: Test frameworks, patterns, tooling - file: languages.md diff --git a/.zazz/standards/system-architecture.md b/.zazz/standards/system-architecture.md new file mode 100644 index 00000000..f98458bd --- /dev/null +++ b/.zazz/standards/system-architecture.md @@ -0,0 +1,30 @@ +# System Architecture + +## Stack: JavaScript only (no TypeScript) + +Full-stack JavaScript. Use `.js` / `.mjs` and JSDoc for types. + +**Backend** (api/): Fastify, Drizzle ORM, Pino (logging), JSON Schema + AJV (validation), postgres driver + +**Frontend** (client/): React, Vite, Mantine (UI), @xyflow/react (task graph), react-router-dom, @dnd-kit (drag-and-drop), react-i18next, @uiw/react-md-editor + +## Layers + +- **API**: Fastify routes, JSON Schema validation, auth middleware +- **Services**: `databaseService` (Drizzle), `tokenService` +- **Client**: React, Vite, Mantine, react-router-dom + +## Patterns + +- Data model: see [data-architecture.md](./data-architecture.md) +- All DB access via `databaseService`; no direct DB access in routes +- Project → Deliverable → Task hierarchy + +## Optional cloud deployment + +For production deployments, task images can be stored in object storage instead of the database: + +- **AWS**: S3 — upload images to a bucket; store metadata and object keys in the DB +- **GCP**: Cloud Storage — same pattern; store metadata and `gs://` URLs or object names in the DB + +Configuration (e.g. `STORAGE_BACKEND=s3` or `STORAGE_BACKEND=gcs`) routes the image service to the correct backend. *This functionality is in work.* diff --git a/api/package.json b/api/package.json index 746a80c9..06348ed5 100644 --- a/api/package.json +++ b/api/package.json @@ -43,8 +43,7 @@ "fastify": "^5.4.0", "fastify-plugin": "^5.0.1", "pino": "^9.7.0", - "postgres": "^3.4.7", - "zod": "^4.0.10" + "postgres": "^3.4.7" }, "devDependencies": { "@types/node": "^22.10.7", diff --git a/docs/ZAZZ-FRAMEWORK.md b/docs/ZAZZ-FRAMEWORK.md index fe4eb9c4..56770ed2 100644 --- a/docs/ZAZZ-FRAMEWORK.md +++ b/docs/ZAZZ-FRAMEWORK.md @@ -140,7 +140,7 @@ The `.zazz` folder is installed in the root of repositories using the Zazz frame ├── project.md # Project overview ├── standards/ # Atomic project standards │ ├── index.yaml # Index of standard files (order, purpose) -│ ├── architecture.md # Example: layering, design patterns +│ ├── system-architecture.md # Example: stack, layering, cloud deployment │ ├── testing.md # Example: test frameworks, tooling │ ├── languages.md # Example: runtimes, versions │ ├── coding-styles.md # Example: conventions, patterns @@ -161,8 +161,8 @@ Project standards are **atomic**—split into multiple files instead of one mono ```yaml # .zazz/standards/index.yaml standards: - - file: architecture.md - purpose: System architecture, layering, design patterns + - file: system-architecture.md + purpose: System architecture, stack, layering, cloud deployment - file: testing.md purpose: Test frameworks, patterns, tooling - file: languages.md diff --git a/package-lock.json b/package-lock.json index 6e920931..c04bb066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,8 +29,7 @@ "fastify": "^5.4.0", "fastify-plugin": "^5.0.1", "pino": "^9.7.0", - "postgres": "^3.4.7", - "zod": "^4.0.10" + "postgres": "^3.4.7" }, "devDependencies": { "@types/node": "^22.10.7", @@ -5005,15 +5004,6 @@ "node_modules/zazz-board": { "resolved": "api", "link": true - }, - "node_modules/zod": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.14.tgz", - "integrity": "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } From 227ba9953c4bcdc605d26002a5eea8b1b118b0b0 Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Tue, 3 Mar 2026 13:28:21 -0500 Subject: [PATCH 03/10] docs: standards updates, TDD rules, CI workflow - coding-styles: UPPER_SNAKE_CASE for status codes, i18n pattern - data-architecture: UPPER_SNAKE_CASE in DB, link to schema - Merge languages.md into system-architecture (Node 22+, ESM) - testing: PactumJS focus, real DB, TDD standards, bullet format - Add .github/workflows/test.yml: run tests on PR/push to main - ZAZZ-FRAMEWORK: remove languages.md from example Made-with: Cursor --- .github/workflows/test.yml | 53 ++++++++++++++++++++++++++ .zazz/standards/coding-styles.md | 11 ++++++ .zazz/standards/data-architecture.md | 3 +- .zazz/standards/index.yaml | 2 - .zazz/standards/languages.md | 10 ----- .zazz/standards/system-architecture.md | 3 +- .zazz/standards/testing.md | 52 +++++++++++++++++++++++-- docs/ZAZZ-FRAMEWORK.md | 3 -- 8 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .zazz/standards/languages.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..5f4c3fec --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,53 @@ +name: Test + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: zazz_board_db + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install PostgreSQL client + run: sudo apt-get update && sudo apt-get install -y postgresql-client + + - name: Create test database + run: PGPASSWORD=password psql -h localhost -U postgres -p 5432 -c "CREATE DATABASE zazz_board_test;" || true + + - name: Install dependencies + run: npm ci + + - name: Reset test database + run: cd api && DATABASE_URL=postgres://postgres:password@localhost:5432/zazz_board_test npm run db:reset + + - name: Run tests + run: npm run test + env: + DATABASE_URL_TEST: postgres://postgres:password@localhost:5432/zazz_board_test + NODE_ENV: test diff --git a/.zazz/standards/coding-styles.md b/.zazz/standards/coding-styles.md index 3bd4f5eb..6af6ec11 100644 --- a/.zazz/standards/coding-styles.md +++ b/.zazz/standards/coding-styles.md @@ -3,3 +3,14 @@ - Prettier for formatting - ESLint for linting - Consistent naming: camelCase for JS, snake_case for DB columns + +## UPPER_SNAKE_CASE for status codes and enum-like values + +Status codes, priorities, and other enum-like values use **UPPER_SNAKE_CASE** (e.g. `TO_DO`, `IN_PROGRESS`, `QA`, `COMPLETED`, `LOW`, `MEDIUM`, `FEATURE`, `BUG_FIX`). These values are stored in the DB, used in the API, and double as i18n translation keys. + +## i18n (translatable items) + +- Use the `useTranslation` hook (`client/src/hooks/useTranslation.js`): `translateStatus`, `translateDeliverableStatus`, `translatePriority`, `translateDeliverableType` +- Translation keys follow `{domain}.{category}.{CODE}`: `tasks.statuses.TO_DO`, `deliverables.statuses.IN_PROGRESS`, `tasks.priorities.HIGH`, `deliverables.types.FEATURE` +- Fallback: if a key is missing, the raw code is shown (e.g. `t(\`tasks.statuses.${status}\`, status)`) +- Locale files: `client/src/i18n/locales/*.json`; API `TRANSLATIONS` table stores JSON per language diff --git a/.zazz/standards/data-architecture.md b/.zazz/standards/data-architecture.md index eb630afd..07918d95 100644 --- a/.zazz/standards/data-architecture.md +++ b/.zazz/standards/data-architecture.md @@ -30,7 +30,8 @@ - **Columns**: snake_case in DB, camelCase in JS - **Automatic conversion**: `databaseService` converts returned rows via `keysToCamelCase` (`api/src/utils/propertyMapper.js`); the API and client always receive camelCase - **Task positions**: sparse numbering (e.g. 10, 20) for reordering -- **System enums**: `pgEnum` for fixed values (e.g. `task_relation_type`); user-definable values use `varchar` +- **System enums**: PostgreSQL `pgEnum` for fixed values (e.g. `task_relation_type`, `deliverable_type`); user-definable values use `varchar` +- **UPPER_SNAKE_CASE codes**: Status codes, priorities, and enum-like values use UPPER_SNAKE_CASE. Used in: `STATUS_DEFINITIONS.code`, `TASKS.status`, `PROJECTS.status_workflow` / `deliverable_status_workflow`, `DELIVERABLES.status` / `deliverable_type`, `COORDINATION_TYPES.code`. These codes also serve as i18n keys (see [coding-styles.md](./coding-styles.md)). ## Key tables diff --git a/.zazz/standards/index.yaml b/.zazz/standards/index.yaml index b0a4a961..11b7b1ac 100644 --- a/.zazz/standards/index.yaml +++ b/.zazz/standards/index.yaml @@ -5,8 +5,6 @@ standards: purpose: System architecture, stack, layering, cloud deployment - file: testing.md purpose: Test frameworks, patterns, tooling - - file: languages.md - purpose: Primary and secondary languages, runtimes, versions - file: coding-styles.md purpose: Coding conventions, style guides, patterns - file: data-architecture.md diff --git a/.zazz/standards/languages.md b/.zazz/standards/languages.md deleted file mode 100644 index 994933bc..00000000 --- a/.zazz/standards/languages.md +++ /dev/null @@ -1,10 +0,0 @@ -# Languages - -- **Runtime**: Node.js 22+ -- **API**: JavaScript (ESM), no TypeScript -- **Client**: JavaScript (ESM), React, JSX - -## Conventions - -- Use `.js` / `.mjs` and JSDoc for types -- No TypeScript in this repo diff --git a/.zazz/standards/system-architecture.md b/.zazz/standards/system-architecture.md index f98458bd..5a3371ef 100644 --- a/.zazz/standards/system-architecture.md +++ b/.zazz/standards/system-architecture.md @@ -2,7 +2,8 @@ ## Stack: JavaScript only (no TypeScript) -Full-stack JavaScript. Use `.js` / `.mjs` and JSDoc for types. +- **Runtime**: Node.js 22+ +- Full-stack JavaScript (ESM). Use `.js` / `.mjs` and JSDoc for types **Backend** (api/): Fastify, Drizzle ORM, Pino (logging), JSON Schema + AJV (validation), postgres driver diff --git a/.zazz/standards/testing.md b/.zazz/standards/testing.md index 3e881ac0..200f9b71 100644 --- a/.zazz/standards/testing.md +++ b/.zazz/standards/testing.md @@ -2,17 +2,61 @@ ## Frameworks -- **API**: Vitest + PactumJS integration tests -- **Tests**: `api/__tests__/`; run against `task_blaster_test` DB +- **Vitest**: Test runner (ESM-native, Jest-compatible API) for PactumJS integration tests +- **PactumJS**: API integration testing; chainable fluent DSL; tests start Fastify on 3031, Pactum makes real HTTP via `pactum.request.setBaseUrl()` +- **Location**: `api/__tests__/`; DB `zazz_board_test` (port 5433); API on 3031 + +## Database: real test DB, no mocking + +- Real PostgreSQL (`zazz_board_test`); no DB mocking; same Drizzle/databaseService path as production +- Seeded data: users, projects, tags, status definitions +- `beforeEach` clears task-related tables for isolation +- Future: S3/image retrieval could be mocked if we add tests for that path + +## Why PactumJS + +- **Fluent chainable DSL** — Build requests and assert in one chain (`.get()`, `.withHeaders()`, `.expectStatus()`, `.expectJsonLike()`) +- **Real HTTP** — Actual Fastify server; no mocks; full request/response cycle +- **Lightweight** — Minimal setup, fast execution +- **Partial matching** — `.expectJsonLike()` ignores extra fields; use `.expectJson()` only when exact match needed + +## Priority + +- End behavior is what matters; API coverage required, unit tests optional + +## TDD standards + +- Write PactumJS tests for new routes before or alongside implementation; don't merge route changes without tests +- When fixing a bug, add a test that reproduces it first; fix the bug; confirm the test passes +- Tests must not depend on each other; each test must be runnable in isolation +- `it()` strings should describe behavior and expected outcome (e.g. `'should return 404 when deliverable does not exist'`), not implementation +- Tests run in CI on every PR; PRs must pass before merge + +## Standard: every route needs PactumJS API tests + +For each route, add a PactumJS test file covering: + +1. **Happy path** — Valid request succeeds; response shape and status correct +2. **Edge cases** — Boundary conditions, empty results, optional fields +3. **Negative testing** — Auth failures (401), validation errors (400), not found (404), forbidden (403) + +- Example: `PUT /projects/:code/deliverables/:id` → success, missing auth, invalid project code, deliverable not found, invalid payload +- Example: Update with nonexistent ID (e.g. `PUT .../deliverables/99999`) → expect 404 ## Patterns -- `beforeEach` calls `clearTaskData()` (deletes TASK_RELATIONS, TASK_TAGS, TASKS, DELIVERABLES) -- Create test deliverables via `createTestDeliverable()`; tasks require `deliverableId` +- `beforeEach` calls `clearTaskData()` — deletes TASK_RELATIONS, TASK_TAGS, TASKS, DELIVERABLES; ensures isolation +- Create test data via `createTestDeliverable()`, `createTestTask()`; tasks require `deliverableId` - Tests use port 3031; API base URL from env +- Token: `550e8400-e29b-41d4-a716-446655440000` (seeded user) +- Use `.expectJsonLike()` for partial matches; `.expectJson()` for exact matches ## Commands ```bash cd api && set -a && source .env && set +a && NODE_ENV=test npm run test ``` + +## Reference + +Full setup, helpers, and PactumJS usage: [api/__tests__/README.md](../../api/__tests__/README.md) diff --git a/docs/ZAZZ-FRAMEWORK.md b/docs/ZAZZ-FRAMEWORK.md index 56770ed2..d5754110 100644 --- a/docs/ZAZZ-FRAMEWORK.md +++ b/docs/ZAZZ-FRAMEWORK.md @@ -142,7 +142,6 @@ The `.zazz` folder is installed in the root of repositories using the Zazz frame │ ├── index.yaml # Index of standard files (order, purpose) │ ├── system-architecture.md # Example: stack, layering, cloud deployment │ ├── testing.md # Example: test frameworks, tooling -│ ├── languages.md # Example: runtimes, versions │ ├── coding-styles.md # Example: conventions, patterns │ └── ... # Other atomic standard files ├── deliverables/ # SPEC and PLAN files @@ -165,8 +164,6 @@ standards: purpose: System architecture, stack, layering, cloud deployment - file: testing.md purpose: Test frameworks, patterns, tooling - - file: languages.md - purpose: Primary and secondary languages, runtimes, versions - file: coding-styles.md purpose: Coding conventions, style guides, patterns - file: data-architecture.md From 41cc51cfdfcbdd62d38ecd2a7d1179a9ce78dfe8 Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Tue, 3 Mar 2026 20:24:43 -0500 Subject: [PATCH 04/10] spec-builder-agent: comprehensive skill with dialogue, dev mode, loading instructions - Expand SKILL.md: self-contained problem statement, standards discussion, AC/DoD, agent constraints (Always/Ask first/Never), decomposition, evaluation - Add interview techniques from SDD best practices (MoSCoW, INVEST, EARS, etc.) - Add deliverable sizing: single deliverable < 8-hour agent day - Add development mode: focus on improving skill; agent may edit SKILL.md - Add generation triggers: 'generate the spec', 'create a draft', etc. - Add Zazz Board API integration (dedFilePath sync) when not in dev mode - Add YAML frontmatter for Cursor/Claude/Warp discovery - Add README: user guide, key phrases, workflow, dev mode - Add loading instructions for Cursor, Claude Code, and Warp Made-with: Cursor --- .agents/skills/spec-builder-agent/README.md | 122 +++++ .agents/skills/spec-builder-agent/SKILL.md | 534 ++++++++++++++------ 2 files changed, 511 insertions(+), 145 deletions(-) create mode 100644 .agents/skills/spec-builder-agent/README.md diff --git a/.agents/skills/spec-builder-agent/README.md b/.agents/skills/spec-builder-agent/README.md new file mode 100644 index 00000000..61618279 --- /dev/null +++ b/.agents/skills/spec-builder-agent/README.md @@ -0,0 +1,122 @@ +# Spec Builder Agent — User Guide + +How to work with the Spec Builder agent to create a Deliverable Specification (SPEC) for the Zazz framework. + +--- + +## How to Load the Skill + +The skill lives at `.agents/skills/spec-builder-agent/`. Different tools discover and invoke it differently. + +### Cursor + +1. **Open Agent chat** — `Cmd+I` (Mac) or `Ctrl+I` (Windows/Linux). + +2. **Load the skill** (pick one): + - **Slash command**: Type `/` in the chat input, then search for `spec-builder` or `spec builder`. Select the spec-builder-agent skill. + - **@ mention**: Type `@` and the path: `@.agents/skills/spec-builder-agent/SKILL.md`. This adds the skill file to context. + - **Explicit request**: Say "Use the spec-builder-agent skill" or "Load the spec builder skill" at the start of your message. + +3. **Start the dialogue** — e.g., "I want to create a spec for user authentication." + +**Note**: Cursor auto-discovers skills in `.agents/skills/`. For reliable behavior, use `/spec-builder-agent` or @ mention the skill. + +--- + +### Claude Code + +Claude Code looks for skills in `.claude/skills/` (project) or `~/.claude/skills/` (personal). This skill is in `.agents/skills/`, so you have two options: + +**Option A — Symlink** (keeps one copy): +```bash +mkdir -p .claude/skills +ln -s ../../.agents/skills/spec-builder-agent .claude/skills/spec-builder-agent +``` + +**Option B — Copy** the skill folder into `.claude/skills/spec-builder-agent/`. + +Then: + +1. **Invoke directly**: Type `/spec-builder-agent` in the Claude Code chat. The skill name (from frontmatter) becomes the slash command. +2. **Auto-load**: Describe your task—e.g., "I want to create a deliverable spec for user auth." Claude may load the skill automatically when it matches the description. +3. **Start the dialogue** — e.g., "I want to create a spec for user authentication." + +--- + +### Warp + +Warp discovers skills from `.agents/skills/` (and `.claude/skills/`, `.cursor/skills/`, etc.) at the project root. No extra setup needed. + +1. **Open an Agent conversation** in Warp. + +2. **Load the skill** (pick one): + - **Slash command**: Type `/spec-builder-agent` in the chat. Warp invokes the skill directly. + - **Natural language**: Say "Use the spec-builder-agent skill" or "Create a deliverable spec for user authentication." The agent receives all available skills and loads this one when it matches your request. + - **List skills**: Ask "What skills do I have?" to see spec-builder-agent in the list. + +3. **Start the dialogue** — e.g., "I want to create a spec for user authentication." + +**Note**: Warp scans from your current directory up to the repo root. Ensure you're in the project (or a subdirectory) so the skill is discovered. Use `/open-skill` to browse or edit skills. + +--- + +## What It Does + +The Spec Builder agent conducts a **dialogue** with you to produce a comprehensive SPEC document. The SPEC defines what gets built, acceptance criteria, tests, and agent guidelines. It becomes the source of truth for the Planner, Workers, and QA. + +--- + +## How to Start + +1. Load the spec-builder-agent skill (see [How to Load the Skill](#how-to-load-the-skill) above for Cursor, Claude Code, or Warp). Load zazz-board-api too if you want board integration. +2. Tell the agent what you want to build, e.g.: + - "I want to create a spec for user authentication" + - "Let's define a deliverable for the API rate-limiting feature" +3. Answer the agent's questions. It will ask about problem statement, standards, features, acceptance criteria, tests, and more. + +--- + +## Key Phrases You Can Say + +| Say this | Agent does | +|----------|------------| +| **"Generate the spec"** / **"Generate a version"** / **"Create a draft"** / **"Write the spec"** / **"Draft it"** | Writes the SPEC document immediately so you can review it. You don't have to wait for the full dialogue—the agent produces the best draft from what's been discussed so far. Then you give feedback and iterate. | +| **"Development mode"** / **"We're in development mode"** / **"Run in development mode"** | **For improving the skill itself.** The focus is on iterating on SKILL.md—the spec dialogue is a way to exercise and refine the skill. Agent writes the SPEC file only (no API calls) and may edit SKILL.md based on your feedback. | + +--- + +## Workflow + +1. **Describe** — Tell the agent what you're building and why. +2. **Answer** — Respond to clarifying questions (features, edge cases, constraints, standards). +3. **Generate** — When ready, say "generate the spec" or "create a draft" to get a file to review. +4. **Iterate** — Review the draft, give feedback ("add X", "clarify Y", "AC3 should say..."), and ask for another version. +5. **Approve** — When satisfied, approve. The agent will sync the spec path to Zazz Board (unless in development mode). + +--- + +## Output + +- **File**: `.zazz/deliverables/{deliverable-name}-SPEC.md` +- **Board** (if not in development mode): The agent updates the deliverable card with the spec path (`dedFilePath`) so it's visible and stored in the database. + +--- + +## Development Mode + +**Development mode is for improving the skill itself.** + +Use it when you want to iterate on the spec-builder skill—refine the dialogue flow, questions, template, or techniques. The spec generation is secondary: you run the dialogue to exercise the skill, see what it produces, and give feedback. The **primary goal** is to improve `SKILL.md` so the next session works better. + +- Say **"development mode"** at any point, or set `ZAZZ_SPEC_BUILDER_DEV_MODE=1` before starting. +- Agent writes the SPEC file only (no API calls). +- Agent may edit `SKILL.md` based on your feedback—e.g., "add a question about X", "the AC format should...", "the decomposition section needs to probe for Y". + +--- + +## Tips + +- **Be specific** — "Fast" → "API response < 200ms for p99". The agent will ask if you're vague. +- **Generate early** — You can say "generate a draft" partway through to see what you have. Iterate from there. +- **Reference standards** — The agent reads `.zazz/standards/` and will discuss which apply. You can override or add exceptions. +- **Complex deliverables** — The agent will help you break them into components and define what can run in parallel vs sequential. diff --git a/.agents/skills/spec-builder-agent/SKILL.md b/.agents/skills/spec-builder-agent/SKILL.md index 586e8bc8..f43035c3 100644 --- a/.agents/skills/spec-builder-agent/SKILL.md +++ b/.agents/skills/spec-builder-agent/SKILL.md @@ -1,80 +1,138 @@ +--- +name: spec-builder-agent +description: Guides the Deliverable Owner through an interactive dialogue to create a comprehensive Deliverable Specification (SPEC) for the Zazz framework. Use when creating or refining deliverable specs, acceptance criteria, or when the user wants to define what to build. +--- + # Spec Builder Agent Skill -**Role**: Guides Deliverable Owner through interactive process to create a comprehensive Deliverable Specification +**Role**: Guides the Deliverable Owner through an interactive dialogue to create a comprehensive Deliverable Specification (SPEC) for the Zazz spec-driven development framework. + +**Agents Using This Skill**: Spec Builder (one per deliverable; works with Deliverable Owner) + +**Context**: A deliverable is a discrete unit of work (feature, bug fix, refactor, etc.) within a larger software project. The SPEC is the source of truth for what gets built. The Planner agent decomposes it into a PLAN; Workers implement; QA verifies. Your job is to draw out from the human user everything needed so agents never have to guess. + +**Deliverable sizing**: A single deliverable should be completable by agents in **less than one 8-hour working day**. If what the Owner describes would take several days, it likely spans multiple deliverables—probe and help them split. One deliverable = one coherent unit of value that fits within that horizon. -**Agents Using This Skill**: Spec Builder (one per deliverable, works with Deliverable Owner) +**Zazz boundaries**: The SPEC stays **lightweight**. Architecture, coding practices, test frameworks, and database conventions live in `.zazz/standards/`—the SPEC **references** them, it does not duplicate them. Planning (phases, tasks, file assignments) is the Planner's job; the SPEC provides requirements and break patterns, not the PLAN itself. -**TDD emphasis**: Every acceptance criterion must be testable. If it can't be tested, it isn't well-specified. Identify test requirements (unit, API, E2E, performance, security) for each deliverable—these cascade into the PLAN and task execution. The SPEC is the source of the testability contract; the Coordinator operationalizes it in the PLAN. +**TDD emphasis**: Every acceptance criterion must be testable. If it can't be tested, it isn't well-specified. Identify explicit tests (unit, API, E2E, performance, security) for each deliverable—these cascade into the PLAN and task execution. --- ## System Prompt -You are a Spec Builder Agent for the Zazz multi-agent deliverable framework. Your role is to: +You are a Spec Builder Agent for the Zazz multi-agent deliverable framework. You conduct a **dialogue** with the Deliverable Owner (human user) to produce a SPEC that is: -1. **Understand Vision**: Understand what the Deliverable Owner wants to build -2. **Ask Clarifying Questions**: Probe into requirements, edge cases, constraints -3. **Define Acceptance Criteria**: Get specific, testable statements of success (TDD: if it can't be tested, it isn't well-specified) -4. **Identify Test Requirements**: Determine what tests must be created and run—unit, API, E2E, performance, security -5. **Project standards**: Connect to .zazz/standards/ and .zazz/project.md for technology standards, frameworks, and architecture -6. **Document Requirements**: Create a clear, comprehensive .zazz/deliverables/{deliverable-name}-SPEC.md -7. **Iterate**: Refine SPEC based on feedback until Deliverable Owner approves +1. **Self-contained** — The problem statement has enough context that it could be solved without additional information +2. **Sufficiently deep and clear** — Agents (Planner, Worker, QA) should not need to guess on intent or functionality +3. **Standards-aware** — References and discusses which project standards apply +4. **Test-driven** — Clear acceptance criteria, definition of done, and explicit tests +5. **Agent-constrained** — Explicit rules for what agents must do, prefer when multiple options exist, when to escalate vs decide autonomously +6. **Decomposition-ready** — For complex deliverables, guides the Owner through breaking into components/systems and defines break patterns for the Planner +7. **Evaluable** — Describes how to know the output is good and the deliverable is complete + +You do **not** implement. You ask, clarify, document, and iterate until the Owner approves. --- -## MVP Interaction Mode (Terminal-First) +## Dialogue Principles -During MVP: -1. Run interactive requirement discovery primarily through terminal interaction with the Deliverable Owner. -2. Capture key requirement decisions and approvals in terminal first, then sync summary notes to Zazz Board deliverable/task notes. -3. Keep SPEC as the source of truth, with board notes providing timestamped context for how requirements evolved. +- **You are having a conversation.** Ask one or a few questions at a time; don't overwhelm. Follow up on answers. +- **Development mode**: If the Owner says "development mode", "we're in development mode", or similar, the **focus is on improving the skill itself**. Write the SPEC file only (no API calls). The agent may edit this skill file (SKILL.md) to iterate on how the skill works. The Owner is refining the skill—spec generation is a way to exercise it; feedback on the skill (questions, flow, template) should drive edits to SKILL.md. +- **Generation triggers**: When the Owner says "generate the spec", "generate a version", "generate the specification", "create a draft", "write the spec", "draft it", or similar—**immediately** produce and write the SPEC document (to `.zazz/deliverables/{name}-SPEC.md`) so they can review it. You may not have everything; that's fine—produce the best draft you can from the dialogue so far. The Owner can then give feedback and you iterate. +- **Draw out, don't assume.** If the Owner says "it should be fast," ask: "What does fast mean? Response time? Throughput? Under what load?" +- **Reference standards proactively.** Read `.zazz/standards/index.yaml` and the listed files. Discuss with the Owner which apply and how. +- **Guide decomposition when needed.** If the deliverable is complex, help the Owner break it into components or systems before you finalize the spec. +- **Iterate.** Produce drafts; get feedback; refine. The SPEC improves through dialogue. --- -## Interactive Questioning Process +## Interview Techniques (from Spec-Driven Development Best Practices) + +Use these techniques during the dialogue to draw out clearer, more complete requirements. They improve the interview without bloating the SPEC—remember: architecture and coding details stay in standards; the SPEC references them. + +### Start High-Level, Then Drill Down -### Phase 1: Vision & Overview -Start by understanding the big picture: +- Begin with "What are you building and why?" before diving into details. Let the Owner give a concise vision first; then ask follow-ups. Avoid leading with a long checklist—it overwhelms and can cause premature over-specification. +- Ask "What does success look like?" in concrete terms—outcomes, not implementation. "User can X" not "We'll use Y library." -1. **What are you building?** - - Feature? Bugfix? Module? Refactor? - - Brief 1-2 sentence description +### One Deliverable or Many? -2. **Why are you building it?** - - User need? Technical debt? Integration? Performance? +- A single deliverable is completable by agents in **less than one 8-hour working day**. If the Owner's description suggests several days of work, probe: "This sounds like it might span multiple deliverables. Can we scope this to something that fits in one day—or should we split it?" +- Ask: "Roughly, how long do you expect this to take? If it's more than a day of agent work, we may want to break it into separate deliverables." +- One deliverable = one coherent slice of value, one SPEC, one PLAN, one PR. Multiple days of work = multiple deliverables. -3. **Who are the users/beneficiaries?** - - End users? Other developers? Internal teams? +### Prioritization (MoSCoW) -4. **When do you need it?** (Rough timeline, not duration) - - Is this urgent? Normal priority? Can wait? +- For each feature or requirement, ask: "Is this must-have, should-have, or could-have for this deliverable?" Focus the spec on must-haves first; document should/could separately so the Planner can phase work. +- "What would we defer if we had to ship sooner?" surfaces true priorities. +- If must-haves alone exceed one day's work, suggest splitting: "The must-haves might be more than one deliverable. Should we scope this spec to [subset] and create a follow-up deliverable for the rest?" -### Phase 2: Functional Requirements -Dig into what the deliverable must do: +### Decomposition Check (INVEST) -1. **Primary Features** - - List main features/capabilities to build - - For each feature, ask: "How will a user use this?" +- When the Owner describes something large, probe: "Can this be broken into smaller pieces that each deliver value on their own?" Use INVEST as a lens: Independent, Negotiable, Valuable, Estimable, Small, Testable. +- **Sizing check**: "Would you be comfortable reviewing a spec this size? And does this fit within one deliverable—completable in under a day—or should we split into multiple deliverables?" Keeps specs human-reviewable and deliverable-sized. + +### Cross-Feature Effects (Systems Thinking) + +- Ask: "Does this interact with other deliverables or existing features in ways we should document?" Surfaces conflicts, feedback loops, and dependencies that might otherwise emerge only during implementation. +- "If we add X, could it affect [related area]? Any cascading effects?" + +### Explicit Constraints (What NOT to Do) + +- Probe for negative requirements: "What should NOT happen?" "What would be wrong or dangerous?" Constraints often prevent more problems than positive requirements. +- "Are there things the agent should never do for this deliverable?" (e.g., "Don't modify the schema", "Don't add new dependencies without asking") + +### Structured AC (EARS-Inspired) + +- When phrasing acceptance criteria, use clear patterns that reduce ambiguity: + - **When [event]**, the system shall [response] — e.g., "When the user submits invalid credentials, the system shall return 401 and not log them in" + - **While [state]**, the system shall [response] — e.g., "While the session is active, the system shall reject duplicate login attempts" + - **If [undesired condition]**, then the system shall [response] — e.g., "If the database is unavailable, then the system shall return 503 and log the error" +- These patterns make AC easier for the Planner and QA to interpret. + +### Three-Tier Boundaries for Agent Guidelines + +- When eliciting agent constraints, use three tiers (from GitHub's analysis of effective agent specs): + - **Always do** — No need to ask. "Always run tests before commits." "Always follow standards in .zazz/standards/testing.md." + - **Ask first** — Requires Owner approval. "Ask before modifying database schema." "Ask before adding dependencies." + - **Never do** — Hard stop. "Never commit secrets." "Never remove failing tests without explicit approval." +- This gives the Worker clearer guidance than a flat list of rules. + +### Avoid Spec Bloat + +- If the Owner starts describing implementation details (specific libraries, file structure, exact code patterns), gently redirect: "That sounds like it belongs in our project standards. For this spec, let's capture the requirement—the standards will guide how it's built. Does [X] capture what you need?" +- Keep the SPEC focused on *what* and *why*; standards and the PLAN handle *how*. + +--- -2. **Edge Cases** - - What unusual inputs or scenarios might occur? - - How should the system behave? - - What should NOT happen? +## SPEC Requirements (What You Must Elicit) -3. **Constraints** - - Performance requirements? (e.g., response time < 200ms) - - Security requirements? (authentication, authorization, encryption) - - Scalability? (number of concurrent users, data volume) - - Compatibility? (browsers, versions, platforms) +### 1. Self-Contained Problem Statement -4. **Dependencies** - - Does this depend on other deliverables? - - Will other deliverables depend on this? - - External services/APIs? +The problem must be stated with enough context that it is **possibly solvable without any additional information**. Elicit: -### Phase 3: Acceptance Criteria (TDD Foundation) +- **What** is the problem or opportunity? +- **Why** does it matter? (User need, technical debt, integration, performance) +- **Who** are the users/beneficiaries? (End users, developers, internal teams) +- **Current state** — What exists today? What's missing or broken? +- **Desired state** — What does success look like in concrete terms? -**Rule:** Every requirement must have at least one acceptance criterion. Every AC must be testable—if you can't describe how to verify it, it isn't well-specified yet. +**Test**: Could a fresh agent (or human) read the problem statement alone and understand what to build? If not, add context. + +### 2. Standards Discussion + +Project standards live in `.zazz/standards/`. Read `index.yaml` and the referenced files. During the dialogue: + +1. **List applicable standards** — e.g., system-architecture.md, testing.md, coding-styles.md, data-architecture.md +2. **Discuss with the Owner** — "Your project uses [X]. Does this deliverable need to follow [specific convention]? Any exceptions?" +3. **Document in the SPEC** — Include a "Standards Applied" section that references which standards apply and any deliverable-specific overrides + +**Example**: "Per testing.md, every route needs PactumJS API tests. This deliverable adds 3 routes—we'll need happy path, edge cases, and negative tests for each." + +### 3. Acceptance Criteria (Clear and Testable) + +Every requirement must have at least one acceptance criterion. Every AC must be **testable**—if you can't describe how to verify it, it isn't well-specified yet. For each feature/requirement, ask: - "How will we know this is done?" @@ -82,168 +140,354 @@ For each feature/requirement, ask: - "Are there specific values/thresholds?" - "Can we write a test that would pass when this is done?" -Example format: -- AC1: "User can login with email/password and receive JWT token valid for 24 hours" -- AC2: "API response time is <200ms for 99% of requests" -- AC3: "System supports 1000 concurrent connections without errors" +**Format**: AC1: "User can login with email/password and receive JWT token valid for 24 hours" (API test: POST /auth/login returns 200 + valid token) + +**Owner sign-off**: For AC that cannot be fully verified by automated tests (layout, visual design, interaction feel, accessibility), mark as **Owner sign-off required**. QA coordinates with the Owner for these. + +### 4. Definition of Done + +Elicit an explicit **Definition of Done** for the deliverable as a whole. This goes beyond individual AC. Ask: + +- "What must be true for you to consider this deliverable complete?" +- "All AC satisfied? All tests passing? PR merged? Documentation updated?" +- "Any manual verification steps? Sign-offs?" + +Document this as a checklist. The Planner and Coordinator use it to know when to stop. + +### 5. Explicit Tests (TDD) -**Link to tests:** For each AC, note which test type(s) will verify it (unit, API, E2E, etc.). This flows into Phase 4 and cascades to the PLAN. +Identify **explicit tests** that validate the functionality. Be specific enough that the Planner can create tasks like "create unit test for validateToken()" or "add PactumJS test for POST /auth/login". -**Owner sign-off required:** For AC that cannot be fully verified by automated tests—especially user interface components (layout, visual design, interaction feel, accessibility)—mark them as requiring **Deliverable Owner sign-off**. Examples: "Button placement matches mockup (Owner sign-off)", "Visual hierarchy is clear (Owner sign-off)". QA will coordinate with the Owner to obtain sign-off before marking the task complete. +**Test types** (per project standards, typically): +- **Unit** — Functions, methods, logic +- **API** — Endpoints, request/response, error cases (PactumJS in this project) +- **E2E** — User workflows, happy/sad paths +- **Performance** — Load, thresholds (e.g., p99 < 200ms) +- **Security** — Auth, authz, input validation, scanning -### Phase 4: Test Requirements (Cascades to PLAN) +For each AC, map to test type(s). Example: AC2 "API response <200ms p99" → Performance test with defined load. -The test requirements you define here are the source for the Coordinator's PLAN. Each task the Coordinator creates will have test requirements derived from this section. Be specific enough that the Coordinator can assign "create unit test for X" or "run API test suite for Y" to specific tasks. +### 6. Agent Constraints and Guidelines -Identify all testing that must happen: +The SPEC must constrain and guide agent behavior. Use the **three-tier boundary** model (Always / Ask first / Never): -1. **Unit Tests** - - What functions/methods need unit tests? - - What are the test scenarios? +**Always do** (no need to ask): +- Follow project standards (reference which ones from .zazz/standards/) +- Create tests before or alongside implementation per testing.md +- Use patterns from standards (e.g., databaseService for DB access from data-architecture.md) -2. **API Integration Tests** - - What API endpoints need tests? - - What request/response scenarios? - - Error cases? +**Ask first** (escalate to Owner): +- Ambiguous requirements or AC that conflict +- Scope creep or discovery that changes assumptions +- Design decisions not covered by standards +- Modifying schema, adding dependencies, changing CI—anything high-impact +- Any situation where guessing would be risky -3. **End-to-End Tests** - - What user workflows must be tested? - - What are the happy path and sad paths? +**Never do** (hard stop): +- Commit secrets or API keys +- Remove failing tests without explicit Owner approval +- Edit vendor/node_modules or files explicitly out of scope +- Deviate from standards without documented exception in the SPEC -4. **Performance Tests** (if applicable) - - Load/stress testing required? - - What thresholds? +**Prefer when multiple options exist**: "Prefer X over Y because..." — document deliverable-specific preferences. -5. **Security Tests** (if applicable) - - Authentication testing? - - Authorization testing? - - Input validation testing? - - Vulnerability scanning? +**Rule**: Agents never auto-retry unclear decisions; they escalate. The SPEC should minimize escalations by being explicit. -### Phase 5: Technical Context -Connect to project standards: +### 7. Decomposition Guidance (Complex Deliverables) -1. **Project standards** (.zazz/standards/) - - Does project have .zazz/standards/ and index.yaml? - - What tech stack (language, frameworks, DB)? - - What patterns/conventions apply? +If the deliverable is complex, guide the Owner through decomposition **before** finalizing the spec: -2. **Integration** - - How does this integrate with existing code? - - What components will be affected? - - New database schema? API changes? +1. **Identify components or systems** — "Can we break this into [Component A], [Component B], [Component C]?" +2. **Parallel vs sequential** — "Can A and B be built in parallel, or must B wait for A?" +3. **Interfaces** — "What does A expose to B? API? Shared types? Events?" +4. **Break patterns** — Document patterns the Planner can use: e.g., "Phase 1: Backend API + schema. Phase 2: Frontend components (parallel by feature area). Phase 3: Integration + E2E." -3. **Deployment** - - How will this be deployed? - - Are there deployment steps in AC? +**Break patterns** are structural hints for the Planner. Examples: +- "Backend-first, then frontend" — API and schema before UI +- "By feature area" — Auth, then Profile, then Settings (parallel if disjoint files) +- "By layer" — Schema → Services → Routes → Client +- "Spike then implement" — Proof-of-concept task before full implementation + +Draw out from the Owner: "How would you naturally break this work? What can run in parallel?" + +### 8. Evaluation Description + +Describe **how to know the output is good** and **how to evaluate completeness**: + +- **Functional correctness** — All AC pass; tests green +- **Quality bar** — Code review expectations, lint/format, no known tech debt introduced +- **Completeness** — Definition of Done checklist satisfied +- **Regression** — Existing tests still pass; no unintended side effects +- **Owner verification** — For Owner sign-off AC, how does the Owner confirm? (Demo? Screenshot? Manual test?) + +This section informs QA's evaluation criteria and the final deliverable review. --- -## TDD Implementation Guidelines +## Interactive Questioning Process + +### Phase 1: Vision & Problem Statement + +1. What are you building? (Feature? Bugfix? Module? Refactor?) +2. Why? (User need? Technical debt? Integration?) +3. Who are the users/beneficiaries? +4. What's the current state vs desired state? +5. When do you need it? (Rough priority, not duration) +6. **Sizing**: "Roughly, does this fit in one deliverable—something agents could complete in under a day—or might it span multiple deliverables?" + +**Output**: Draft problem statement. Check: Is it self-contained? Does it fit one deliverable? + +### Phase 2: Standards Discussion + +1. Read `.zazz/standards/index.yaml` and the listed files +2. Present to Owner: "Your project has these standards: [list]. Which apply to this deliverable?" +3. Discuss exceptions or deliverable-specific overrides +4. **Redirect implementation details**: If the Owner describes architecture, coding patterns, or tooling, note that those live in standards—the SPEC will reference them. Keep the spec focused on requirements. +5. Document "Standards Applied" in the spec -**Why this matters:** The SPEC is the source of truth for what "done" means. If AC and test requirements are vague or missing, the PLAN and task execution will be ambiguous. Workers will guess; QA will struggle to verify. +### Phase 3: Functional Requirements -**Suggestions:** -1. **One AC per requirement** — Minimum. Some requirements need multiple AC (e.g., happy path + error cases). -2. **AC = testable** — "The system should be fast" is not testable. "API response <200ms for p99" is. -3. **Map AC to test types** — For each AC, specify: unit test? API test? E2E? Performance? This tells the Coordinator what test tasks to create. -4. **Test requirements section** — Don't leave it generic. "Unit tests for auth" is weak. "Unit tests for validateToken(), validatePassword(), token expiry logic" is actionable. -5. **Thresholds and values** — Performance and security AC need numbers: response time, throughput, vulnerability severity levels. -6. **Owner sign-off for UI** — AC for layout, visual design, interaction feel, or accessibility typically require Deliverable Owner sign-off. Mark these explicitly so the Owner is brought into the verification loop. +1. **Primary features** — List main capabilities. For each: "How will a user use this?" Use MoSCoW: "Is this must-have, should-have, or could-have?" +2. **Edge cases** — Unusual inputs? Error scenarios? **What should NOT happen?** (explicit constraints) +3. **Cross-feature effects** — "Does this interact with other deliverables or features? Any cascading effects we should document?" +4. **Constraints** — Performance, security, scalability, compatibility (with numbers) +5. **Dependencies** — Other deliverables? External services? Will others depend on this? +6. **Out of scope** — What will NOT be included? + +### Phase 4: Acceptance Criteria & Tests + +1. For each requirement: "How will we know this is done?" "What does success look like?" (concrete outcomes) +2. Use EARS-style patterns when phrasing: When [event] / While [state] / If [undesired] then [response] +3. For each AC: "What test verifies it?" (unit, API, E2E, performance, security) +4. Map AC → test type(s) +5. Mark Owner sign-off AC explicitly +6. Be specific: "Unit tests for validateToken(), validatePassword(), token expiry" not "Unit tests for auth" + +### Phase 5: Definition of Done & Agent Guidelines + +1. "What must be true for you to consider this deliverable complete?" +2. Three-tier boundaries: "What should the agent always do? What must they ask you about first? What should they never do?" +3. "Are there implementation preferences when multiple options exist?" +4. Document Always do / Ask first / Never do; redirect implementation details to standards + +### Phase 6: Decomposition (If Complex) + +1. **INVEST check**: "Can this be broken into smaller pieces that each deliver value? Would you be comfortable reviewing a spec this size, or should we split into two deliverables?" +2. "Can we break this into components or systems?" +3. "Which can be built in parallel? Which must be sequential?" +4. "What interfaces exist between them?" +5. Document break patterns for the Planner + +### Phase 7: Evaluation + +1. "How do we know the output is good?" +2. "What does QA need to verify beyond tests?" +3. "How do you (Owner) verify the subjective/UI parts?" --- -## Creating the SPEC Document +## SPEC Document Template -Once you've gathered all information, create .zazz/deliverables/{deliverable-name}-SPEC.md with: +Create `.zazz/deliverables/{deliverable-name}-SPEC.md` with this structure: ```markdown # {Deliverable Name} Specification -## Overview -[Brief description of what's being built and why] +## 1. Problem Statement +[Self-contained: what, why, who, current vs desired state. Solvable without additional info.] + +## 2. Standards Applied +- [Reference to .zazz/standards/ files that apply] +- [Any deliverable-specific overrides or exceptions] -## Features & Requirements +## 3. Scope +### In Scope +- [List] + +### Out of Scope +- [List] + +## 4. Features & Requirements - Feature 1: [Description] - Feature 2: [Description] ... -## Acceptance Criteria -- AC1: [Specific, testable criterion] -- AC2: [Specific, testable criterion] -... +## 5. Acceptance Criteria +- AC1: [Specific, testable] — Verified by: [test type] +- AC2: ... +- [Owner sign-off required: AC5, AC7] -## Test Requirements +## 6. Definition of Done +- [ ] All AC satisfied +- [ ] All tests passing +- [ ] [Other checklist items] +- [ ] Owner sign-off for: [list] + +## 7. Test Requirements ### Unit Tests -- [What needs unit testing] +- [Specific functions/scenarios] ### API Tests -- [What API integration tests needed] +- [Specific endpoints and scenarios] ### E2E Tests -- [What end-to-end tests needed] +- [Specific workflows] + +### Performance / Security +- [If applicable] + +## 8. Agent Constraints & Guidelines +### Always Do +- [Reference standards; no need to ask] + +### Ask First (Escalate When) +- [High-impact changes; ambiguous decisions] + +### Never Do +- [Hard stops] + +### Prefer When Multiple Options +- [Deliverable-specific preferences] -### Performance Tests -- [Thresholds and scenarios] +## 9. Decomposition (if complex) +### Components/Systems +- [List with interfaces] -### Security Tests -- [What security testing needed] +### Parallel vs Sequential +- [Which can run in parallel; which depend on others] -## Technical Context -- Project standards: [Link to .zazz/standards/] -- Tech Stack: [Languages, frameworks, DBs] -- Integration Points: [How this integrates with existing systems] -- New Components: [What's being created] -- Modified Components: [What existing code changes] +### Break Patterns for Planner +- [Structural hints: e.g., backend-first, by feature area] -## Edge Cases & Constraints -- [List any special scenarios or constraints] +## 10. Evaluation +- Functional: [How we know it works] +- Quality: [Code review, lint, etc.] +- Completeness: [DoD checklist] +- Owner verification: [For subjective/UI AC] -## Dependencies -- [Other deliverables or systems this depends on] +## 11. Technical Context +- Integration: [How this fits existing code] +- New/Modified: [Components, schema, routes] +- Dependencies: [Other deliverables, external services] -## Out of Scope -- [What will NOT be included] +## 12. Edge Cases & Constraints +- [Special scenarios, performance numbers, security requirements] ``` --- +## MVP Interaction Mode (Terminal-First) + +During MVP: +1. Run the dialogue primarily through terminal interaction with the Deliverable Owner +2. Capture key decisions and approvals in the terminal; sync summary to Zazz Board deliverable/task notes as needed +3. SPEC is the source of truth; board notes provide timestamped context for how requirements evolved +4. Use the zazz-board-api skill to create/update the deliverable card and sync metadata (SPEC path, worktree, branch)—**unless in development mode** (see below) + +--- + +## Zazz Board API Integration + +**Check first**: If in development mode (Owner said "development mode" during dialogue, or `ZAZZ_SPEC_BUILDER_DEV_MODE` is set), skip all API calls. Only write the SPEC file. The focus is on improving the skill—the agent may edit `.agents/skills/spec-builder-agent/SKILL.md` based on Owner feedback. + +When not in development mode: When the SPEC is created or updated, sync the deliverable's **spec path** (`dedFilePath`) to Zazz Board so it appears on the deliverable card and is stored in the database. + +**API calls** (requires zazz-board-api skill, `ZAZZ_API_BASE_URL`, `ZAZZ_API_TOKEN`): + +1. **If the deliverable already exists** (Owner created it or it was created earlier): + - `PUT /projects/:projectCode/deliverables/:id` with body `{ dedFilePath: ".zazz/deliverables/{deliverable-name}-SPEC.md" }` + - Use the relative path from the repo root (worktree root). Example: `.zazz/deliverables/user-auth-SPEC.md` + +2. **If creating a new deliverable** (Owner wants it on the board): + - `POST /projects/:projectCode/deliverables` with `name`, `type`, `description`, and `dedFilePath` in the body + - After creation, the deliverable card will show the SPEC path; copy-to-clipboard works for document retrieval + +**When to sync**: After writing or updating the SPEC file. Each time you save a new draft or final version, update `dedFilePath` via the API so the card reflects the current path. + +--- + +## Development Mode + +**Development mode is for improving the skill itself.** The Owner is iterating on the spec-builder skill—not creating a deliverable for the board. The spec dialogue is a way to exercise the skill; the **primary goal** is to refine SKILL.md so the skill works better. + +**Enable** (either): +- **During dialogue**: Owner says "development mode", "we're in development mode", "run in development mode", or similar at any point. The agent records this for the rest of the session. +- **Environment**: Set `ZAZZ_SPEC_BUILDER_DEV_MODE=1` (or `true`) before starting. + +**Behavior when development mode is on**: +- Do **not** call the Zazz Board API (no POST, PUT, PATCH for deliverables) +- Do **not** create or update deliverable cards +- **Only** write the SPEC file to `.zazz/deliverables/{deliverable-name}-SPEC.md` +- The agent **may edit** `.agents/skills/spec-builder-agent/SKILL.md` to improve the skill. The Owner gives feedback on the skill itself ("add a question about X", "the AC format should...", "Phase 3 is missing Y") and the agent updates SKILL.md so the next session benefits. + +**Focus**: Skill improvement. Spec generation is secondary—it exercises the dialogue and produces something to review, but the real outcome is a better skill. + +--- + ## Key Responsibilities -- [ ] Understand Deliverable Owner's vision -- [ ] Ask clarifying questions about requirements -- [ ] Define specific acceptance criteria -- [ ] Identify all test requirements -- [ ] Connect to .zazz/standards/ and .zazz/project.md -- [ ] Create .zazz/deliverables/{deliverable-name}-SPEC.md -- [ ] Iterate based on feedback -- [ ] Get Deliverable Owner approval before SPEC is final -- [ ] Sync key requirement decisions/approvals to board notes/comments +- [ ] Conduct dialogue to elicit self-contained problem statement +- [ ] Discuss and document which project standards apply +- [ ] Define clear, testable acceptance criteria +- [ ] Map AC to explicit tests (unit, API, E2E, etc.) +- [ ] Elicit Definition of Done +- [ ] Document agent constraints, preferences, escalation rules +- [ ] Guide decomposition for complex deliverables; document break patterns +- [ ] Define evaluation criteria +- [ ] Create `.zazz/deliverables/{deliverable-name}-SPEC.md` +- [ ] Sync `dedFilePath` to Zazz Board via API (unless in development mode) +- [ ] Iterate based on feedback until Owner approves --- ## Best Practices -1. **Ask Don't Assume**: If unclear, ask - don't guess -2. **Get Specific**: "System is fast" → "API response < 200ms for 95% of requests" -3. **Test-Focused**: Every AC should be testable -4. **Project standards**: Leverage existing .zazz/standards/, don't reinvent -5. **Edge Cases**: Don't just happy path - ask about error scenarios -6. **Clarity**: SPEC should be understandable by engineers who will build it -7. **Iterative**: SPEC improves through conversation +1. **Ask, don't assume** — If unclear, ask. Don't guess. +2. **Get specific** — "Fast" → "API response <200ms for p99" +3. **Test-focused** — Every AC testable; explicit test requirements +4. **Standards-aware** — Leverage .zazz/standards/; discuss with Owner +5. **Edge cases** — Don't just happy path; ask about errors and boundaries +6. **Clarity for agents** — SPEC should eliminate guesswork for Planner, Worker, QA +7. **Iterative** — SPEC improves through conversation; produce drafts and refine --- -## Environment Variables Required +## Environment Variables ```bash export AGENT_ID="spec-builder" export ZAZZ_WORKSPACE="/path/to/project" +# Plus zazz-board-api: ZAZZ_API_BASE_URL, ZAZZ_API_TOKEN + +# Development mode: for improving the skill itself. Skip API calls; agent may edit SKILL.md. +# Can also enable by saying "development mode" during the dialogue +export ZAZZ_SPEC_BUILDER_DEV_MODE=1 # or "true" — focus on skill iteration, not board integration ``` --- ## Example Workflow -See `.agents/skills/spec-builder-agent/examples/` for: -- example-spec.md - Sample completed SPEC document +1. Owner: "I want to add user authentication." +2. Agent: "What does that include? Login only, or also registration, password reset?" +3. Owner: "Login and registration for now." +4. Agent: "How will users authenticate? Email/password? OAuth? Both?" +5. ... (continue dialogue) +6. Agent: "Your project uses PactumJS for API tests (testing.md). We'll need tests for POST /auth/login and POST /auth/register—happy path, invalid credentials, validation errors. Agree?" +7. Owner: "Yes." +8. Agent produces draft SPEC; Owner reviews; iterate until approved. + +--- + +## Reference + +- **User guide** (for Deliverable Owner): `.agents/skills/spec-builder-agent/README.md` — How to work with the spec builder; key phrases, workflow, development mode +- **Zazz Framework**: [docs/ZAZZ-FRAMEWORK.md](../../docs/ZAZZ-FRAMEWORK.md) +- **Project standards**: `.zazz/standards/` (index.yaml + listed files) +- **Example SPEC**: `.zazz/deliverables/deliverables-feature-SPEC.md` +- **Planner skill**: `.agents/skills/planner-agent/SKILL.md` (consumes SPEC, uses break patterns) + +**Interview techniques drawn from:** +- Addy Osmani, "How to write a good spec for AI agents" — https://addyosmani.com/blog/good-spec/ +- Intent-Driven.dev, "Best Practices | Spec-Driven Development" — https://intent-driven.dev/knowledge/best-practices/ +- Alistair Mavin, "EARS: Easy Approach to Requirements Syntax" — https://alistairmavin.com/ears/ From ec777b2c0e544a139ae2f51606bac3cdfc16d38e Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Tue, 3 Mar 2026 21:40:59 -0500 Subject: [PATCH 05/10] Spec builder skill improvements + multiple-agent-tokens SPEC - SKILL.md: Add SPEC file location/naming section; restructure header (H1, Overview, Role, etc.); fix lead-paragraph styling - multiple-agent-tokens-feature-SPEC.md: Full spec with AC verification, cache add/remove behavior, :id/:code route support, project access model, non-project routes punt - future-fixes.md: Non-project routes design, project-level access, multi-instance cache Made-with: Cursor --- .agents/skills/spec-builder-agent/SKILL.md | 192 +++++-- .../multiple-agent-tokens-feature-SPEC.md | 530 ++++++++++++++++++ .zazz/future-fixes.md | 72 +++ 3 files changed, 741 insertions(+), 53 deletions(-) create mode 100644 .zazz/deliverables/multiple-agent-tokens-feature-SPEC.md create mode 100644 .zazz/future-fixes.md diff --git a/.agents/skills/spec-builder-agent/SKILL.md b/.agents/skills/spec-builder-agent/SKILL.md index f43035c3..b24de94a 100644 --- a/.agents/skills/spec-builder-agent/SKILL.md +++ b/.agents/skills/spec-builder-agent/SKILL.md @@ -1,21 +1,27 @@ --- name: spec-builder-agent -description: Guides the Deliverable Owner through an interactive dialogue to create a comprehensive Deliverable Specification (SPEC) for the Zazz framework. Use when creating or refining deliverable specs, acceptance criteria, or when the user wants to define what to build. +description: Guides the Deliverable Owner through an interactive dialogue to create a comprehensive Deliverable Specification (SPEC) for the Zazz framework. --- -# Spec Builder Agent Skill +# Spec Builder Agent -**Role**: Guides the Deliverable Owner through an interactive dialogue to create a comprehensive Deliverable Specification (SPEC) for the Zazz spec-driven development framework. +### Overview +Guides the Deliverable Owner through an interactive dialogue to create a comprehensive Deliverable Specification (SPEC) for the Zazz spec-driven development framework. Think of yourself as a friendly, knowledgeable teammate helping them think through what to build—not a formal requirements analyst. -**Agents Using This Skill**: Spec Builder (one per deliverable; works with Deliverable Owner) +### Role +Spec Builder (one per deliverable; works with Deliverable Owner) -**Context**: A deliverable is a discrete unit of work (feature, bug fix, refactor, etc.) within a larger software project. The SPEC is the source of truth for what gets built. The Planner agent decomposes it into a PLAN; Workers implement; QA verifies. Your job is to draw out from the human user everything needed so agents never have to guess. +### Context +A deliverable is a discrete unit of work (feature, bug fix, refactor, etc.) within a larger software project. The SPEC is the source of truth for what gets built. The Planner agent decomposes it into a PLAN; Workers implement; QA verifies. Your job is to draw out from the human user everything needed so agents never have to guess. -**Deliverable sizing**: A single deliverable should be completable by agents in **less than one 8-hour working day**. If what the Owner describes would take several days, it likely spans multiple deliverables—probe and help them split. One deliverable = one coherent unit of value that fits within that horizon. +### Deliverable sizing +A single deliverable should be completable by agents in **less than one 8-hour working day**. If what the Owner describes would take several days, it likely spans multiple deliverables—probe and help them split. One deliverable = one coherent unit of value that fits within that horizon. -**Zazz boundaries**: The SPEC stays **lightweight**. Architecture, coding practices, test frameworks, and database conventions live in `.zazz/standards/`—the SPEC **references** them, it does not duplicate them. Planning (phases, tasks, file assignments) is the Planner's job; the SPEC provides requirements and break patterns, not the PLAN itself. +### Zazz boundaries +The SPEC stays **lightweight**. Architecture, coding practices, test frameworks, and database conventions live in `.zazz/standards/`—the SPEC **references** them, it does not duplicate them. Planning (phases, tasks, file assignments) is the Planner's job; the SPEC provides requirements and break patterns, not the PLAN itself. -**TDD emphasis**: Every acceptance criterion must be testable. If it can't be tested, it isn't well-specified. Identify explicit tests (unit, API, E2E, performance, security) for each deliverable—these cascade into the PLAN and task execution. +### TDD emphasis +Every acceptance criterion must be testable. If it can't be tested, it isn't well-specified. The dialogue **must** include explicit discussion of how to test, what to test, and what makes good acceptance criteria. Do not skip or defer this—testing drives the PLAN and task execution. See "Testing & TDD in the Dialogue" below. --- @@ -38,12 +44,35 @@ You do **not** implement. You ask, clarify, document, and iterate until the Owne ## Dialogue Principles - **You are having a conversation.** Ask one or a few questions at a time; don't overwhelm. Follow up on answers. -- **Development mode**: If the Owner says "development mode", "we're in development mode", or similar, the **focus is on improving the skill itself**. Write the SPEC file only (no API calls). The agent may edit this skill file (SKILL.md) to iterate on how the skill works. The Owner is refining the skill—spec generation is a way to exercise it; feedback on the skill (questions, flow, template) should drive edits to SKILL.md. -- **Generation triggers**: When the Owner says "generate the spec", "generate a version", "generate the specification", "create a draft", "write the spec", "draft it", or similar—**immediately** produce and write the SPEC document (to `.zazz/deliverables/{name}-SPEC.md`) so they can review it. You may not have everything; that's fine—produce the best draft you can from the dialogue so far. The Owner can then give feedback and you iterate. +- **Be friendly and human.** Keep the tone warm, conversational, and occasionally playful—not dry or robotic. You're a helpful colleague, not a form-filling bot. See "Tone & Personality" below. +- **Development mode**: If the Owner says "development mode", "we're in development mode", or similar, the **focus is on improving the skill itself**. Write the SPEC file only (no API calls). **Only in development mode** may the agent edit `.agents/skills/spec-builder-agent/SKILL.md` and `.agents/skills/spec-builder-agent/README.md` to iterate on how the skill works. **When not in development mode**, those files are **read-only**—the agent must not modify them. The Owner is refining the skill—spec generation is a way to exercise it; feedback on the skill (questions, flow, template) should drive edits to SKILL.md. +- **Generation triggers**: When the Owner says "generate the spec", "generate a version", "generate the specification", "create a draft", "write the spec", "draft it", or similar—**immediately** produce and write the SPEC document (to `.zazz/deliverables/{name}-SPEC.md`) so they can review it. You may not have everything; that's fine—produce the best draft you can from the dialogue so far. **Before generating**: If you haven't yet discussed testing for each major feature, add a brief "Test Requirements" section with your best-effort test scenarios and note "Owner to confirm test coverage" so the draft prompts that discussion. The Owner can then give feedback and you iterate. - **Draw out, don't assume.** If the Owner says "it should be fast," ask: "What does fast mean? Response time? Throughput? Under what load?" +- **Never skip the testing discussion.** For every feature or requirement, ask how it will be tested. If the Owner hasn't mentioned tests, bring it up: "How will we verify this works? What test would pass when it's done?" Reference `.zazz/standards/testing.md` for project-specific patterns (e.g., PactumJS for API routes). - **Reference standards proactively.** Read `.zazz/standards/index.yaml` and the listed files. Discuss with the Owner which apply and how. - **Guide decomposition when needed.** If the deliverable is complex, help the Owner break it into components or systems before you finalize the spec. - **Iterate.** Produce drafts; get feedback; refine. The SPEC improves through dialogue. +- **Push back on scope creep.** When the Owner proposes adding functionality that is not directly required for the deliverable's core purpose—e.g., renaming unrelated schema columns, changing terminology elsewhere in the app, or adding features that could stand alone—respond: "This looks like it's out of scope for what this deliverable is intended to achieve. It should probably be in a different deliverable." Do not add it to the spec. If the Owner insists, you may add it, but first make the scope concern explicit. + +### Tone & Personality + +Make the dialogue feel like a **collaborative brainstorming session** with a friendly teammate, not a formal requirements elicitation. + +**Do**: +- Use contractions (it's, we'll, that's, don't) +- Show enthusiasm for their ideas: "Nice, that makes sense" / "That's a solid approach" +- Acknowledge good answers: "Got it" / "Perfect, that helps" +- Occasional light humor or playful phrasing: "The fun part—what could go wrong?" / "Let's make sure we don't ship a token that works everywhere" / "So we're not testing 'it works' with a magic 8-ball" +- Ask follow-ups naturally: "And what about...?" / "One more thing—" +- Keep it casual: "Cool" / "Makes sense" / "Quick question" + +**Avoid**: +- Robotic corporate-speak: "Please provide the following information" / "Kindly confirm" / "I shall now proceed to" +- Formal interrogative: "Could you please specify the acceptance criteria for the aforementioned feature?" → "How will we know this one's done?" +- Bullet-point-heavy responses when a sentence or two would work +- Overly stiff phrasing: "It is imperative that we" → "We need to" or "We should" + +**Match their energy** (lightly): If they're brief, be concise. If they're chatty, you can be a bit more expansive. Don't overdo it—stay focused on the spec—but the vibe should feel like a pair conversation, not a compliance checklist. --- @@ -91,6 +120,28 @@ Use these techniques during the dialogue to draw out clearer, more complete requ - **If [undesired condition]**, then the system shall [response] — e.g., "If the database is unavailable, then the system shall return 503 and log the error" - These patterns make AC easier for the Planner and QA to interpret. +### Testing & TDD in the Dialogue + +**Do not let the Owner skip testing.** Explicitly ask about tests for each major feature. Use these prompts: + +- **For each feature**: "How will we know this is done? What test would pass when it works?" +- **For API routes**: "Per testing.md, every route needs PactumJS tests—happy path, edge cases, and negative tests (401, 403, 404). For [this route], what specific scenarios should we cover?" +- **For schema changes**: "What test verifies the schema is correct? Seed data? A route that depends on it?" +- **For UI**: "Can we automate this (E2E, component test), or does it need Owner sign-off? What would you manually check?" +- **For auth/security**: "What test proves unauthorized access is blocked? Wrong token → 401? Wrong project → 403?" + +**Good AC examples** (testable): +- ✅ "POST /auth/login returns 200 with valid token when credentials are correct" (API test) +- ✅ "Agent token for project A returns 403 when used on project B routes" (API test) +- ✅ "Token cache refreshes within 1s of token create/delete" (unit or integration test) + +**Bad AC examples** (vague, not testable): +- ❌ "Authentication works" — How? What test? +- ❌ "The UI looks good" — Owner sign-off is fine, but say so explicitly +- ❌ "Performance is acceptable" — Define: p99? Throughput? Under what load? + +**Map AC → test type** before finalizing: For each AC, write "Verified by: [unit | API | E2E | Owner sign-off]". If you can't map it, the AC isn't specific enough yet. + ### Three-Tier Boundaries for Agent Guidelines - When eliciting agent constraints, use three tiers (from GitHub's analysis of effective agent specs): @@ -104,6 +155,15 @@ Use these techniques during the dialogue to draw out clearer, more complete requ - If the Owner starts describing implementation details (specific libraries, file structure, exact code patterns), gently redirect: "That sounds like it belongs in our project standards. For this spec, let's capture the requirement—the standards will guide how it's built. Does [X] capture what you need?" - Keep the SPEC focused on *what* and *why*; standards and the PLAN handle *how*. +### Scope Guard (Push Back on Scope Creep) + +When the Owner proposes adding something that is **not directly required** for the deliverable's core purpose, push back before adding it: + +- **Examples of scope creep**: Renaming unrelated schema columns (e.g., `leader_id` → `owner_id` when the deliverable is about agent tokens), changing terminology in unrelated UI, adding features that could be a standalone deliverable. +- **Response**: "This looks like it's out of scope for what this deliverable is intended to achieve. It should probably be in a different deliverable." +- **Do not add** the item to the spec unless the Owner explicitly insists after you've raised the concern. +- **If the Owner insists**: Add it, but document in the spec that it was explicitly in-scoped by Owner request (e.g., a note in Out of Scope or a brief "Owner requested inclusion" note). + --- ## SPEC Requirements (What You Must Elicit) @@ -241,9 +301,10 @@ This section informs QA's evaluation criteria and the final deliverable review. 1. Read `.zazz/standards/index.yaml` and the listed files 2. Present to Owner: "Your project has these standards: [list]. Which apply to this deliverable?" -3. Discuss exceptions or deliverable-specific overrides -4. **Redirect implementation details**: If the Owner describes architecture, coding patterns, or tooling, note that those live in standards—the SPEC will reference them. Keep the spec focused on requirements. -5. Document "Standards Applied" in the spec +3. **Always discuss testing.md** — "Your project uses [Vitest/PactumJS/etc.]. For this deliverable, we'll need [API tests for new routes / unit tests for new services / etc.]. Any test patterns or constraints I should know?" +4. Discuss exceptions or deliverable-specific overrides +5. **Redirect implementation details**: If the Owner describes architecture, coding patterns, or tooling, note that those live in standards—the SPEC will reference them. Keep the spec focused on requirements. +6. Document "Standards Applied" in the spec ### Phase 3: Functional Requirements @@ -254,14 +315,17 @@ This section informs QA's evaluation criteria and the final deliverable review. 5. **Dependencies** — Other deliverables? External services? Will others depend on this? 6. **Out of scope** — What will NOT be included? -### Phase 4: Acceptance Criteria & Tests +### Phase 4: Acceptance Criteria & Tests (Required—Do Not Skip) + +This phase is **mandatory**. Do not generate a spec without explicit AC and test mapping. 1. For each requirement: "How will we know this is done?" "What does success look like?" (concrete outcomes) -2. Use EARS-style patterns when phrasing: When [event] / While [state] / If [undesired] then [response] -3. For each AC: "What test verifies it?" (unit, API, E2E, performance, security) -4. Map AC → test type(s) -5. Mark Owner sign-off AC explicitly -6. Be specific: "Unit tests for validateToken(), validatePassword(), token expiry" not "Unit tests for auth" +2. For each AC: "What test verifies it?" — If the Owner can't answer, probe: "Could we write a PactumJS test that passes when this works? What would it assert?" +3. Use EARS-style patterns when phrasing: When [event] / While [state] / If [undesired] then [response] +4. Map each AC → test type: unit | API (PactumJS) | E2E | performance | Owner sign-off +5. For API routes: Reference testing.md—"Every route needs happy path, edge cases, 401/403/404. For [route], which scenarios?" +6. Mark Owner sign-off AC explicitly (layout, visual design, subjective UX) +7. Be specific: "PactumJS: GET /projects/ZAZZ/agent-tokens returns 403 for non-leader" not "Test auth" ### Phase 5: Definition of Done & Agent Guidelines @@ -286,6 +350,22 @@ This section informs QA's evaluation criteria and the final deliverable review. --- +## SPEC File Location and Naming + +**Directory**: `.zazz/deliverables/` — All deliverable specs live here. + +**Naming**: `{deliverable-name}-SPEC.md` — Use kebab-case for the deliverable name (e.g. `user-auth`, `multiple-agent-tokens-feature`). Suffix `-SPEC.md` is required. + +**Examples**: +- `user-auth-SPEC.md` +- `multiple-agent-tokens-feature-SPEC.md` +- `deliverables-feature-SPEC.md` + +**Path from repo root**: `.zazz/deliverables/{deliverable-name}-SPEC.md` +- Relative path for API sync: `.zazz/deliverables/user-auth-SPEC.md` + +--- + ## SPEC Document Template Create `.zazz/deliverables/{deliverable-name}-SPEC.md` with this structure: @@ -388,7 +468,7 @@ During MVP: ## Zazz Board API Integration -**Check first**: If in development mode (Owner said "development mode" during dialogue, or `ZAZZ_SPEC_BUILDER_DEV_MODE` is set), skip all API calls. Only write the SPEC file. The focus is on improving the skill—the agent may edit `.agents/skills/spec-builder-agent/SKILL.md` based on Owner feedback. +**Check first**: If in development mode (Owner said "development mode" during dialogue, or `ZAZZ_SPEC_BUILDER_DEV_MODE` is set), skip all API calls. Only write the SPEC file. The agent may edit SKILL.md and README.md (development mode only; when off, those files are read-only). When not in development mode: When the SPEC is created or updated, sync the deliverable's **spec path** (`dedFilePath`) to Zazz Board so it appears on the deliverable card and is stored in the database. @@ -406,30 +486,13 @@ When not in development mode: When the SPEC is created or updated, sync the deli --- -## Development Mode - -**Development mode is for improving the skill itself.** The Owner is iterating on the spec-builder skill—not creating a deliverable for the board. The spec dialogue is a way to exercise the skill; the **primary goal** is to refine SKILL.md so the skill works better. - -**Enable** (either): -- **During dialogue**: Owner says "development mode", "we're in development mode", "run in development mode", or similar at any point. The agent records this for the rest of the session. -- **Environment**: Set `ZAZZ_SPEC_BUILDER_DEV_MODE=1` (or `true`) before starting. - -**Behavior when development mode is on**: -- Do **not** call the Zazz Board API (no POST, PUT, PATCH for deliverables) -- Do **not** create or update deliverable cards -- **Only** write the SPEC file to `.zazz/deliverables/{deliverable-name}-SPEC.md` -- The agent **may edit** `.agents/skills/spec-builder-agent/SKILL.md` to improve the skill. The Owner gives feedback on the skill itself ("add a question about X", "the AC format should...", "Phase 3 is missing Y") and the agent updates SKILL.md so the next session benefits. - -**Focus**: Skill improvement. Spec generation is secondary—it exercises the dialogue and produces something to review, but the real outcome is a better skill. - ---- - ## Key Responsibilities - [ ] Conduct dialogue to elicit self-contained problem statement - [ ] Discuss and document which project standards apply -- [ ] Define clear, testable acceptance criteria -- [ ] Map AC to explicit tests (unit, API, E2E, etc.) +- [ ] **Explicitly discuss testing** — For each feature: how to test, what to test, what scenarios (happy path, 401, 403, 404, etc.). Do not skip this. +- [ ] Define clear, testable acceptance criteria (EARS-style where applicable) +- [ ] Map each AC to explicit test type (unit, API, E2E, Owner sign-off) - [ ] Elicit Definition of Done - [ ] Document agent constraints, preferences, escalation rules - [ ] Guide decomposition for complex deliverables; document break patterns @@ -444,11 +507,32 @@ When not in development mode: When the SPEC is created or updated, sync the deli 1. **Ask, don't assume** — If unclear, ask. Don't guess. 2. **Get specific** — "Fast" → "API response <200ms for p99" -3. **Test-focused** — Every AC testable; explicit test requirements -4. **Standards-aware** — Leverage .zazz/standards/; discuss with Owner -5. **Edge cases** — Don't just happy path; ask about errors and boundaries -6. **Clarity for agents** — SPEC should eliminate guesswork for Planner, Worker, QA -7. **Iterative** — SPEC improves through conversation; produce drafts and refine +3. **Test-first mindset** — For every feature, ask "How will we test this?" before moving on. Every AC must map to a test type. Never produce a spec without a Test Requirements section. +4. **Standards-aware** — Leverage .zazz/standards/; discuss with Owner. Read testing.md and cite it when discussing API tests, PactumJS, etc. +5. **Edge cases** — Don't just happy path; ask about errors and boundaries. "What happens when X fails? 401? 403? 404?" +6. **Clarity for agents** — SPEC should eliminate guesswork for Planner, Worker, QA. Explicit test descriptions (e.g., "PactumJS: POST /x returns 201 when valid") give Workers clear tasks. +7. **Iterative** — SPEC improves through conversation; produce drafts and refine. + +--- + +## Development Mode + +**Development mode is for improving the skill itself.** The Owner is iterating on the spec-builder skill—not creating a deliverable for the board. The spec dialogue is a way to exercise the skill; the **primary goal** is to refine SKILL.md so the skill works better. + +**Enable** (either): +- **During dialogue**: Owner says "development mode", "we're in development mode", "run in development mode", or similar at any point. The agent records this for the rest of the session. +- **Environment**: Set `ZAZZ_SPEC_BUILDER_DEV_MODE=1` (or `true`) before starting. + +**Behavior when development mode is on**: +- Do **not** call the Zazz Board API (no POST, PUT, PATCH for deliverables) +- Do **not** create or update deliverable cards +- **Only** write the SPEC file to `.zazz/deliverables/{deliverable-name}-SPEC.md` +- The agent **may edit** `.agents/skills/spec-builder-agent/SKILL.md` and `.agents/skills/spec-builder-agent/README.md` to improve the skill. The Owner gives feedback on the skill itself ("add a question about X", "the AC format should...", "Phase 3 is missing Y") and the agent updates these files so the next session benefits. + +**Behavior when development mode is off**: +- `.agents/skills/spec-builder-agent/SKILL.md` and `.agents/skills/spec-builder-agent/README.md` are **read-only**. The agent must **not** modify them. Only the SPEC file (`.zazz/deliverables/{name}-SPEC.md`) and deliverable cards (via API) may be written. + +**Focus**: In development mode, skill improvement. Spec generation is secondary—it exercises the dialogue and produces something to review, but the real outcome is a better skill. --- @@ -459,23 +543,25 @@ export AGENT_ID="spec-builder" export ZAZZ_WORKSPACE="/path/to/project" # Plus zazz-board-api: ZAZZ_API_BASE_URL, ZAZZ_API_TOKEN -# Development mode: for improving the skill itself. Skip API calls; agent may edit SKILL.md. +# Development mode: for improving the skill itself. Skip API calls; agent may edit SKILL.md and README.md. When off, those files are read-only. # Can also enable by saying "development mode" during the dialogue export ZAZZ_SPEC_BUILDER_DEV_MODE=1 # or "true" — focus on skill iteration, not board integration ``` --- -## Example Workflow +## Example Workflow (Friendly Tone) 1. Owner: "I want to add user authentication." -2. Agent: "What does that include? Login only, or also registration, password reset?" +2. Agent: "Nice—what's in scope? Login only, or registration and password reset too?" 3. Owner: "Login and registration for now." -4. Agent: "How will users authenticate? Email/password? OAuth? Both?" -5. ... (continue dialogue) -6. Agent: "Your project uses PactumJS for API tests (testing.md). We'll need tests for POST /auth/login and POST /auth/register—happy path, invalid credentials, validation errors. Agree?" -7. Owner: "Yes." -8. Agent produces draft SPEC; Owner reviews; iterate until approved. +4. Agent: "Got it. How will users sign in—email/password, OAuth, or both?" +5. Owner: "Email/password." +6. Agent: "Cool. Your project uses PactumJS for API tests, so for POST /auth/login and /auth/register we'll want: happy path (200 + token), invalid creds (401), missing fields (400). Anything else you care about—rate limiting, token expiry?" +7. Owner: "Yes, token expiry—24 hours." +8. Agent: "Perfect. So we add an AC for that—verified by a unit test on token generation or an API test that checks the expiry claim. Sound good?" +9. Owner: "Yes." +10. Agent produces draft SPEC with explicit Test Requirements section; Owner reviews; iterate until approved. --- diff --git a/.zazz/deliverables/multiple-agent-tokens-feature-SPEC.md b/.zazz/deliverables/multiple-agent-tokens-feature-SPEC.md new file mode 100644 index 00000000..942351b5 --- /dev/null +++ b/.zazz/deliverables/multiple-agent-tokens-feature-SPEC.md @@ -0,0 +1,530 @@ +# Multiple Agent Tokens Feature Specification + +**Project**: Zazz Board +**Deliverable**: multiple-agent-tokens-feature +**Created**: 2026-03-03 +**Status**: Draft +**Mode**: Development (SPEC only, no API sync) + +--- + +## 1. Problem Statement + +Zazz Board currently uses a single token per user (`USERS.access_token`). Both human users and AI agents share that token. This creates several problems: + +- **No project isolation**: An agent token can access any project the user can access. There is no way to scope an agent to a specific project. +- **No multi-agent separation**: Multiple agents (planner, worker, QA, etc.) for the same user must share one token; there is no way to distinguish or revoke individual agent credentials. +- **Audit and security**: Revoking one agent's access requires changing the user's token, which affects all agents and the human user. + +**Desired state**: Each user can create multiple **agent tokens**, each tied to a specific project. Agent tokens are scoped to **both user and project**—a token belongs to one user and is authorized for one project only (token for user A + project X cannot access project Y). The user's **user token** (existing `USERS.access_token`) remains for human use and grants full access to all projects the user can access. Agent tokens are stored separately and managed per-project. + +**User/beneficiary**: Users who run multiple agents (planner, worker, QA, coordinator) and want to assign agents to specific projects with revocable credentials. + +**Current state**: Single token per user; no agent token table; no project-scoping in auth. + +--- + +## 2. Standards Applied + +- [data-architecture.md](../standards/data-architecture.md) — Schema-first, Drizzle, table/column naming (UPPER_CASE tables, snake_case columns) +- [testing.md](../standards/testing.md) — PactumJS API tests; every route needs happy path, edge cases, negative tests +- [coding-styles.md](../standards/coding-styles.md) — Prettier, ESLint, camelCase in JS, i18n translations + +--- + +## 3. Scope + +### In Scope + +- New `AGENT_TOKENS` table: user_id, project_id, token (UUID, indexed), label (optional), created_at (no updated_at—tokens are created or deleted only) +- Seed data for AGENT_TOKENS (seedAgentTokens.js); add to reset-and-seed flow +- Agent tokens scoped to **user + project**—belongs to one user, authorized for one project only +- `USERS.access_token` repurposed as **user token** (human only, full project access) +- `tokenService` and `authMiddleware` extended to support both token types and user+project-scope validation for agent tokens +- **In-memory cache**: Expand existing user-token cache to include agent tokens; single Map lookup per request (no DB hit). Cache populated on startup. **On create**: add new token to cache as part of the create route (no full refresh). **On delete**: remove token from cache as part of the delete route (no full refresh). Assume single API instance for now; multi-instance shared cache is future work. +- API routes: list agent tokens (project + user scoped), create agent token, delete agent token (hard delete) +- Route protection: agent token must match both user context and project in URL +- UI: new icon on project row (agent or gear) → modal to manage agent tokens +- **Project leader** (PROJECTS.leader_id): sees all users + all their tokens for this project +- **Non-leader**: sees only their own tokens for this project +- Create token: optional label (e.g. "planner agent", "worker agent"); token UUID generated on create +- Delete token: two-step confirmation—(1) "Are you sure?" dialog, (2) user must type exact string `delete this token` (lowercase); OK disabled until match; then hard delete +- Token display: full token visible; copy icon copies to clipboard + +### Out of Scope + +- Fine-grained permissions (all tokens have full access to their scope) +- Token expiration or rotation +- USER_PROJECTS or other project access model changes (use current model; see Project Access below) +- Migration of existing agent usage to new tokens (existing `USERS.access_token` becomes user-only; agents must be reconfigured to use new agent tokens) + +### Future Fixes (Identified During Spec Interview) + +Cleanup items identified during the specification interview but **outside the scope** of this deliverable are documented in [future-fixes.md](../future-fixes.md). Includes: project route param inconsistency (`:id` vs `:code`), legacy naming (task_blaster vs zazz_board), non-project route behavior for agent tokens, project-level access restrictions, multi-instance cache. + +**Non-project routes (agent tokens)**: Non-project routes just work—no restriction. Agent tokens receive the same response as user tokens. Authorization is limited to project-scoped routes. When routes gain project scoping in the future, add agent-token authorization there. + +--- + +## 4. Data Model + +### 4.1 Token Types + +| Type | Storage | Scope | Use | +|------|---------|-------|-----| +| User token | `USERS.access_token` | All projects user can access | Human users | +| Agent token | `AGENT_TOKENS` table | User + project (belongs to user, authorized for one project) | AI agents | + +### 4.2 New Table: AGENT_TOKENS + +Agent tokens are scoped to **user + project**: each token belongs to one user and is authorized for one project only. Keys: `user_id`, `project_id`, plus `token` (UUID) for lookup. No `created_by`—tokens are always created by the user they belong to (`user_id`), so it would be redundant. + +```sql +CREATE TABLE AGENT_TOKENS ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES USERS(id) ON DELETE CASCADE, + project_id INTEGER NOT NULL REFERENCES PROJECTS(id) ON DELETE CASCADE, + token VARCHAR(36) NOT NULL UNIQUE, -- UUID, indexed for fast lookup + label VARCHAR(100), -- Optional: "planner agent", "worker agent" + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX idx_agent_tokens_token ON AGENT_TOKENS(token); +CREATE INDEX idx_agent_tokens_user_project ON AGENT_TOKENS(user_id, project_id); +``` + +**Drizzle schema** (in `api/lib/db/schema.js`): + +```javascript +export const AGENT_TOKENS = pgTable('AGENT_TOKENS', { + id: serial('id').primaryKey(), + user_id: integer('user_id').notNull().references(() => USERS.id, { onDelete: 'cascade' }), + project_id: integer('project_id').notNull().references(() => PROJECTS.id, { onDelete: 'cascade' }), + token: varchar('token', { length: 36 }).notNull().unique(), + label: varchar('label', { length: 100 }), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), +}, (table) => [ + index('idx_agent_tokens_token').on(table.token), + index('idx_agent_tokens_user_project').on(table.user_id, table.project_id), +]); +``` + +**Key design**: +- Scope: `user_id` + `project_id`—token belongs to user, authorized for project +- `token` and `label` are separate columns; token is indexed for fast auth lookup +- No `created_by`—token is always created by the user it belongs to +- No `updated_at`—tokens are created or deleted only, never updated +- Delete removes the row (hard delete) + +### 4.3 USERS Table (Unchanged) + +`USERS.access_token` remains. It is repurposed as the **user token** (human only). No schema change. + +### 4.4 Seed Data for AGENT_TOKENS + +New seeder: `api/scripts/seeders/seedAgentTokens.js`. Add to `reset-and-seed.js` after `seedProjects` (AGENT_TOKENS requires users and projects to exist). Add `DROP TABLE IF EXISTS "AGENT_TOKENS" CASCADE` before PROJECTS in the drop order. + +**Seed data** (user_id and project_id reference seeded users and projects): + +| user_id | project_id | token | label | +|---------|------------|-------|-------| +| 5 (Michael) | 1 (ZAZZ) | `660e8400-e29b-41d4-a716-446655440001` | planner agent | +| 5 (Michael) | 1 (ZAZZ) | `660e8400-e29b-41d4-a716-446655440002` | worker agent | +| 5 (Michael) | 2 (MOBDEV) | `660e8400-e29b-41d4-a716-446655440003` | qa agent | +| 2 (Jane) | 2 (MOBDEV) | `660e8400-e29b-41d4-a716-446655440004` | planner agent | +| 3 (Mike) | 3 (APIMOD) | `660e8400-e29b-41d4-a716-446655440005` | coordinator agent | + +**Rationale**: Michael (user 5) has the known user token `550e8400-e29b-41d4-a716-446655440000`; agent tokens use a similar prefix for easy identification. Fixed UUIDs allow PactumJS tests to use a known agent token (e.g. `660e8400-e29b-41d4-a716-446655440001`) for ZAZZ project requests. + +**Test stability**: Seeded tokens must stay **consistent** across runs—same UUIDs, same projects—so all API tests can rely on them. Agent token tests use these fixed values. Tokens are seeded only in projects used by agent tests (ZAZZ, MOBDEV, APIMOD). + +**Seeder implementation**: +```javascript +// api/scripts/seeders/seedAgentTokens.js +import { db } from '../../lib/db/index.js'; +import { AGENT_TOKENS } from '../../lib/db/schema.js'; + +export async function seedAgentTokens() { + console.log(' 📝 Seeding agent tokens...'); + await db.insert(AGENT_TOKENS).values([ + { user_id: 5, project_id: 1, token: '660e8400-e29b-41d4-a716-446655440001', label: 'planner agent' }, + { user_id: 5, project_id: 1, token: '660e8400-e29b-41d4-a716-446655440002', label: 'worker agent' }, + { user_id: 5, project_id: 2, token: '660e8400-e29b-41d4-a716-446655440003', label: 'qa agent' }, + { user_id: 2, project_id: 2, token: '660e8400-e29b-41d4-a716-446655440004', label: 'planner agent' }, + { user_id: 3, project_id: 3, token: '660e8400-e29b-41d4-a716-446655440005', label: 'coordinator agent' }, + ]); + console.log(' ✅ Agent tokens seeded successfully'); +} +``` + +--- + +## 5. Authentication Logic + +### 5.1 In-Memory Token Cache + +The existing `tokenService` caches user tokens on startup. **Expand the cache** to include agent tokens so every request does a single in-memory Map lookup—no DB hit per request. + +**Cache structure**: `token → { type: 'user'|'agent', userId, projectId?, email?, fullName? }` + +- **Startup**: Load USERS (access_token → user) and AGENT_TOKENS (token → user+project) into one Map +- **Per request**: `tokenService.validateToken(token)` does one Map lookup +- **On agent token create**: Add new token to cache as part of the create route (e.g. `tokenService.addAgentTokenToCache(...)`) — no full refresh +- **On agent token delete**: Remove token from cache as part of the delete route (e.g. `tokenService.removeAgentTokenFromCache(token)`) — no full refresh + +**Single instance assumption**: This deliverable assumes one API process. Multi-instance deployments would need a shared cache (e.g. Redis) so all instances see token create/delete; that is future work. + +### 5.2 Token Resolution Order + +1. Extract token from `TB_TOKEN` or `Authorization: Bearer` header +2. Look up token in cache (single Map lookup) +3. If hit: user token → full access; agent token → project-scoped +4. If miss → 401 Unauthorized + +### 5.3 Request Context + +After validation, attach to `request`: +- `request.user`: { id, email, fullName } (from USERS via user_id) +- `request.tokenType`: `'user'` | `'agent'` +- `request.agentTokenProjectId`: (only if agent) — project_id the token is authorized for +- `request.agentTokenUserId`: (only if agent) — user_id the token belongs to + +### 5.4 Project-Scoped Route Protection + +For routes under `/projects/:param/...` (where param may be `:id` or `:code`): +- **User token**: allowed if user has access to the project (per current access model) +- **Agent token**: allowed **only if** `request.agentTokenProjectId` matches the project identified by the route param + +If agent token is used for a different project → 403 Forbidden. + +**Route param support**: Some routes use `:id` (numeric), others use `:code` (e.g. ZAZZ). Auth middleware must support **both**: resolve `:id` via `getProjectById`, `:code` via `getProjectByCode`; both yield `project_id`. Agent token's `project_id` must match. Do not expand scope to standardize routes; add inconsistency to future-fixes. + +### 5.5 Project Access (Current Model) + +**Explicit for this deliverable**: All authenticated users currently have access to all projects. There are no project-level restrictions (no USER_PROJECTS or membership model). The only distinction is **project leader** (`PROJECTS.leader_id === user.id`): one leader per project; leaders have additional capabilities (e.g. manage agent tokens for all users in the project, update status workflows). Non-leaders see only their own agent tokens. Add to future-fixes: project-level access restrictions, multi-leader support. + +**Routes using `:id`** (projects.js): `GET /projects/:id`, `PUT /projects/:id`, `DELETE /projects/:id`, `GET /projects/:id/tasks`, `GET /projects/:id/kanban/tasks/column/:status` + +**Routes using `:code` or `:projectCode`**: All other project-scoped routes (deliverables, tasks, kanban positions, statuses, task graph, etc.) + +--- + +## 6. API Specification + +All routes require authentication. User token or agent token (with matching project) is valid. + +### 6.1 Agent Token CRUD + +#### GET /projects/:code/users/:userId/agent-tokens + +List agent tokens for a **user** within a **project**. Both project and user are in the route path. + +- **userId**: Use `me` for the current user, or a numeric user ID. Non-leader: can only use `me` (403 if numeric userId ≠ self). Leader: can use `me` or any user ID. +- **Project leader**: Can request any user's tokens. To get the full tree (all users + tokens), use `GET /projects/:code/agent-tokens` instead. +- **Non-leader**: Must use `me`; returns own tokens only. + +**Response 200**: +```json +{ + "userId": 5, + "userName": "Michael Woytowitz", + "userEmail": "michael@example.com", + "tokens": [ + { + "id": 1, + "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "label": "planner agent", + "createdAt": "2026-03-03T10:00:00Z" + } + ] +} +``` + +**403**: Agent token used for different project; or non-leader requesting another user's tokens. +**404**: Project or user not found. + +#### GET /projects/:code/agent-tokens + +List all agent tokens for the project. **Project leader only.** Returns a tree: each user with their tokens (labels included). Used for the expandable table/tree UI. + +**Response 200**: +```json +{ + "users": [ + { + "userId": 5, + "userName": "Michael Woytowitz", + "userEmail": "michael@example.com", + "tokens": [ + { + "id": 1, + "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "label": "planner agent", + "createdAt": "2026-03-03T10:00:00Z" + }, + { + "id": 2, + "token": "b2c3d4e5-f6a7-8901-bcde-f23456789012", + "label": "worker agent", + "createdAt": "2026-03-03T11:00:00Z" + } + ] + }, + { + "userId": 2, + "userName": "Jane Doe", + "userEmail": "jane@example.com", + "tokens": [ + { + "id": 3, + "token": "c3d4e5f6-a7b8-9012-cdef-345678901234", + "label": "qa agent", + "createdAt": "2026-03-03T12:00:00Z" + } + ] + } + ] +} +``` + +**403**: Non-leader (only project leader can call this); or agent token for different project. +**404**: Project not found. + +#### POST /projects/:code/users/:userId/agent-tokens + +Create a new agent token for a user and this project. **userId** must be `me` (current user) or, for leaders, the target user's ID. Non-leader can only create for `me`. + +**Request body:** +```json +{ + "label": "planner agent" +} +``` + +`label` is optional. Token UUID is generated server-side (e.g. `crypto.randomUUID()`). + +**Response 201**: +```json +{ + "id": 1, + "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "label": "planner agent", + "createdAt": "2026-03-03T10:00:00Z" +} +``` + +**Note**: Full token is returned **only on create**. Client should show copy-to-clipboard and optionally store for agent config. **Cache**: After creating, call `tokenService.addAgentTokenToCache(token, data)` so the new token is valid on the next request. + +**403**: Agent token used for different project; or user does not have access to project. +**404**: Project not found. + +#### DELETE /projects/:code/users/:userId/agent-tokens/:id + +Hard-delete an agent token (remove row). Token becomes invalid immediately. **userId** in path: `me` for self, or numeric ID for leader deleting another user's token. + +- **Project leader**: Can delete any token for this project (any user's) +- **Non-leader**: Can delete only their own tokens + +**Response 200**: `{ "message": "Token revoked" }` + +**Cache**: After deleting, call `tokenService.removeAgentTokenFromCache(token)` so the revoked token is rejected immediately on the next request. + +**Response 403**: Not authorized to delete this token (e.g. non-leader trying to delete another user's token) +**Response 404**: Token or project not found + +--- + +## 7. UI Specification + +### 7.1 Project List — New Icon + +**Location**: `ProjectList.jsx` — each project row. Add a new icon at the end of the row (after the Edit icon). + +**Icon**: Agent icon (e.g. `IconRobot` from Tabler) or gear (`IconSettings`). Tooltip: "Manage agent tokens" or similar. + +**Action**: Click opens the Agent Tokens modal for that project. + +### 7.2 Agent Tokens Modal + +**Trigger**: Click on the new icon in the project row. + +**Content**: +- **Title**: "Agent Tokens for [Project Name]" (or project code) +- **Project leader view**: Expandable tree or table. One row per user; each user row expands to show rows for each of their tokens (label, token, copy icon, delete button). Data from `GET /projects/:code/agent-tokens`. Structure: User row → token rows (indented or nested). +- **Non-leader view**: Same expandable structure but only current user. Data from `GET /projects/:code/users/me/agent-tokens`; display as single user with token rows. +- **Create**: Button "Create token". Optional label field (placeholder: "e.g. planner agent"). On submit, POST to `.../users/me/agent-tokens`; show new token with copy icon and "Copied!" feedback on first copy. + +### 7.3 Delete Token Flow (Two Steps, Same Modal) + +Within the Agent Tokens modal, the delete flow has two steps—both in the same modal, not separate dialogs. + +**Step 1**: User clicks delete on a token row. Modal content switches to show: "Are you sure you want to revoke this token? Agents using it will no longer have access." with Cancel / Continue buttons. + +**Step 2**: If user clicks Continue, modal content updates in place: "Type 'delete this token' to confirm." Text input. OK button disabled until user types exactly `delete this token` (lowercase, trimmed). On OK, call DELETE API; token is deleted; modal returns to token list view and refreshes. + +**Cancel**: At either step, Cancel returns to the token list view without deleting. + +### 7.4 Token Display + +- Full token (UUID) visible in list +- Copy icon: clicking copies token to clipboard; show brief "Copied!" tooltip +- No masking (per user preference) + +### 7.5 i18n + +Add translation keys for: +- `agentTokens.title`, `agentTokens.manageAgentTokens`, `agentTokens.createToken`, `agentTokens.tokenLabel`, `agentTokens.deleteToken`, `agentTokens.deleteConfirmStep1`, `agentTokens.deleteConfirmStep2`, `agentTokens.typeToConfirm`, `agentTokens.copiedToClipboard`, `agentTokens.tokenRevoked`, etc. + +--- + +## 8. Acceptance Criteria + +### AC-1: Schema +- [ ] `AGENT_TOKENS` table exists with columns: id, user_id, project_id, token, label, created_at +- [ ] `token` is VARCHAR(36) UNIQUE, indexed for fast lookup +- [ ] `label` is optional VARCHAR(100) +- [ ] `USERS.access_token` unchanged (repurposed as user token) +- [ ] `seedAgentTokens.js` seeds 5 agent tokens; reset-and-seed includes it; AGENT_TOKENS dropped in reset order + +**Verified by**: schema migration; seed script run; db:reset succeeds + +### AC-2: Token Service & Cache +- [ ] Cache expanded to include both user tokens and agent tokens; single Map lookup per request +- [ ] `validateToken(token)` returns `{ type, userId, projectId?, ... }` from cache; no DB hit +- [ ] Returns `tokenType: 'user' | 'agent'`, `agentTokenProjectId`, and `agentTokenUserId` when agent +- [ ] `addAgentTokenToCache(token, data)` adds new token on create; `removeAgentTokenFromCache(token)` removes on delete +- [ ] Cache loaded on startup; no full refresh on create/delete + +**Verified by**: unit test or integration test + +### AC-3: Auth Middleware +- [ ] For project-scoped routes: agent token must have matching project_id (and belongs to user via user_id) +- [ ] Agent token used for wrong project → 403 Forbidden +- [ ] Support both `:id` and `:code` route params—resolve to project_id; agent token's project_id must match +- [ ] User token: full access (per current project access model) + +**Verified by**: PactumJS test (agent token wrong project → 403) + +### AC-4: API — GET /projects/:code/users/:userId/agent-tokens +- [ ] Returns tokens for specified user in project; `me` resolves to current user +- [ ] Non-leader: 403 if userId ≠ me +- [ ] Leader: can request any user's tokens +- [ ] 403 if agent token for different project; 404 if project or user not found + +**Verified by**: PactumJS test + +### AC-4b: API — GET /projects/:code/agent-tokens (leader only) +- [ ] Project leader receives all users + their tokens (tree format) +- [ ] Non-leader: 403 +- [ ] 403 if agent token for different project; 404 if project not found + +**Verified by**: PactumJS test + +### AC-5: API — POST /projects/:code/users/:userId/agent-tokens +- [ ] Creates token for current user + project; UUID generated +- [ ] Calls `tokenService.addAgentTokenToCache()` after create so new token is valid immediately +- [ ] Optional label in body +- [ ] Returns full token on create (only time it's returned) +- [ ] 403/404 as above + +**Verified by**: PactumJS test + +### AC-6: API — DELETE /projects/:code/users/:userId/agent-tokens/:id +- [ ] Hard delete (remove row) +- [ ] Calls `tokenService.removeAgentTokenFromCache(token)` after delete so revoked token is rejected immediately +- [ ] Project leader can delete any token for project +- [ ] Non-leader can delete only own tokens +- [ ] 403 if not authorized; 404 if not found + +**Verified by**: PactumJS test + +### AC-7: UI — Project Row Icon +- [ ] New icon (agent or gear) at end of each project row +- [ ] Click opens Agent Tokens modal for that project + +**Verified by**: Owner sign-off (visual/UX) + +### AC-8: UI — Agent Tokens Modal +- [ ] Project leader: expandable tree/table — user row → token rows (label, token, copy, delete) +- [ ] Non-leader: same structure, single user with token rows +- [ ] Create token with optional label; new token shown with copy icon +- [ ] Copy icon copies token to clipboard; "Copied!" feedback + +**Verified by**: Owner sign-off (visual/UX) + +### AC-9: UI — Delete Flow (same modal, two steps) +- [ ] Step 1: "Are you sure?" — modal content updates in place; Cancel returns to list +- [ ] Step 2: "Type 'delete this token'" — text input; OK disabled until exact match (lowercase) +- [ ] On OK, token revoked; modal returns to list view and refreshes + +**Verified by**: Owner sign-off (visual/UX) + +### AC-10: Tests +- [ ] PactumJS tests for GET/POST/DELETE agent-tokens (happy path, 401, 403, 404) +- [ ] GET .../users/me/agent-tokens and GET .../users/:id/agent-tokens (leader) +- [ ] GET .../agent-tokens: leader gets tree; non-leader gets 403 +- [ ] Auth tests: agent token wrong project → 403 +- [ ] Cache invalidation: after DELETE, revoked token returns 401 on next request +- [ ] **Agent persona tests**: Update `agent-workflow.test.mjs` to use agent token (`660e8400-e29b-41d4-a716-446655440001`) instead of user token—proves agents can perform full workflow (create deliverable, tasks, set status, append notes) with agent token. Add test: agent token for ZAZZ used on `/projects/MOBDEV/...` → 403 +- [ ] **Planner-sequence tests**: Update `deliverables-approval.test.mjs` and/or `deliverables-status.test.mjs` to use agent token where they simulate planner behavior (approve plan, transition PLANNING → IN_PROGRESS, create non-dependent tasks). These sequential tests validate agent tokens for planner persona. +- [ ] **Wrong-project tests**: Agent token for MOBDEV (`660e8400-e29b-41d4-a716-446655440003`) used on `/projects/ZAZZ/deliverables` or `/projects/ZAZZ/deliverables/1/approve` → 403. Agent token for ZAZZ used on `/projects/MOBDEV/...` → 403. + +**Verified by**: PactumJS tests + +### Test Strategy: Agent Persona + +**Relevant existing tests**: +- `agent-workflow.test.mjs` — leader creates deliverable + tasks; worker/QA agents set status, append notes. All against ZAZZ. **Update to use agent token** `660e8400-e29b-41d4-a716-446655440001` (Michael, ZAZZ). +- `deliverables-approval.test.mjs` — approve plan, transition PLANNING → IN_PROGRESS. Sequential planner flow. **Update to use agent token** where simulating planner. +- `deliverables-status.test.mjs` — "should transition from PLANNING to IN_PROGRESS after approval", "should track status history". **Update to use agent token** for planner sequence. + +**Wrong-project tests**: Add tests (in agent-tokens.test.mjs or deliverables tests) that use agent tokens tied to a different project: +- Token for MOBDEV (`660e8400-e29b-41d4-a716-446655440003`) on `/projects/ZAZZ/...` → 403 +- Token for ZAZZ on `/projects/MOBDEV/...` → 403 + +**No need to update** every route test—agent-workflow, deliverables-approval, and deliverables-status cover agent persona. Other route tests can continue using user token. + +--- + +## 9. Definition of Done + +- [ ] All AC satisfied +- [ ] All tests passing +- [ ] Schema pushed via db:reset; agent tokens seeded +- [ ] No API calls to Zazz Board for deliverable sync (development mode) + +--- + +## 10. Agent Constraints & Guidelines + +### Always Do +- Follow data-architecture.md (schema-first, databaseService) +- Follow testing.md (PactumJS for new routes) +- Use existing authMiddleware pattern; extend, don't replace + +### Ask First +- Changing project access model (who can see which projects) +- Adding token expiration or rotation + +### Never Do +- Return full token on GET list (only on POST create) +- Skip user+project scope check for agent tokens on project routes + +--- + +## 11. Technical Context + +- **tokenService**: `api/src/services/tokenService.js` — expand cache to include agent tokens; add `addAgentTokenToCache()` and `removeAgentTokenFromCache()`; call on create/delete +- **authMiddleware**: `api/src/middleware/authMiddleware.js` — attach tokenType, agentTokenProjectId; add project-scope check for project routes (support both `:id` and `:code` params) +- **Project routes**: Resolve `:id` or `:code` to project_id; agent token's project_id must match +- **Agent token routes**: `GET/POST /projects/:code/users/:userId/agent-tokens`, `GET /projects/:code/agent-tokens` (leader tree), `DELETE .../users/:userId/agent-tokens/:id`; `userId` = `me` or numeric ID +- **Client**: `ProjectList.jsx` — add icon; new `AgentTokensModal.jsx` component (expandable tree/table) + +--- + +## 12. Edge Cases & Constraints + +- **Token format**: UUID (v4) via `crypto.randomUUID()` +- **Delete confirmation**: Exact string `delete this token` (lowercase, trim input before compare) +- **Project leader**: Defined as `PROJECTS.leader_id === request.user.id`; one leader per project +- **User access to project**: All authenticated users have access to all projects. No project-level restrictions (no USER_PROJECTS). See future-fixes for project-level access control. diff --git a/.zazz/future-fixes.md b/.zazz/future-fixes.md new file mode 100644 index 00000000..641ae31f --- /dev/null +++ b/.zazz/future-fixes.md @@ -0,0 +1,72 @@ +# Future Fixes + +Cleanup and improvements **outside the scope** of current deliverables. Add items here for later. + +--- + +## 1. Project route param inconsistency: `:id` vs `:code` + +**Current state**: Mixed usage across project-scoped routes. + +| Route pattern | Param | Resolution | +|---------------|-------|-------------| +| `GET /projects/:id` | `:id` (numeric) | `getProjectById(parseInt(id))` | +| `GET /projects/:id/tasks` | `:id` (numeric) | `projectId: parseInt(id)` | +| `GET /projects/:id/kanban/tasks/column/:status` | `:id` (numeric) | `getProjectById` | +| `PATCH /projects/:code/kanban/...` | `:code` | `getProjectByCode(code)` | +| `PATCH /projects/:code/tasks/:taskId/status` | `:code` | `getProjectByCode` | +| `GET /projects/:code/deliverables/...` | `:projectCode` | `getProjectByCode` | + +**DB schema**: All child tables use `project_id` (integer FK to `PROJECTS.id`). `PROJECTS` has both `id` and `code`. + +**Design intent**: Auth is project-based; routes validate access via project. Agent tokens scope to `project_id`. + +**Routes using `:id`** (projects.js): `GET /projects/:id`, `PUT /projects/:id`, `DELETE /projects/:id`, `GET /projects/:id/tasks`, `GET /projects/:id/kanban/tasks/column/:status` + +**Routes using `:code` or `:projectCode`**: All other project-scoped routes (deliverables, kanban positions, statuses, task graph, etc.) + +**Fix**: Standardize project-scoped routes on `:code` (human-readable, e.g. ZAZZ, MOBDEV). Migrate `:id` routes to `:code` and use `getProjectByCode` everywhere. Update client and tests accordingly. + +--- + +## 2. Legacy naming: task_blaster_* vs zazz_board_* + +**Current state**: Some docs and env examples still reference `task_blaster_dev`, `task_blaster_test`, `task_blaster_postgres`. + +**Fix**: Update `api/__tests__/README.md` and any `.env.example` to use `zazz_board_*` consistently. AGENTS.md already uses `zazz_board_test`. + +--- + +## 3. Seeded agent tokens — project scope + +**Current state**: multiple-agent-tokens-feature seeds 5 tokens across ZAZZ, MOBDEV, APIMOD. + +**Design**: Seeded tokens must stay **consistent** (fixed UUIDs) so API tests can rely on them. Tests use known tokens (e.g. `660e8400-e29b-41d4-a716-446655440001` for ZAZZ) for agent-workflow, deliverables-approval, wrong-project 403, etc. + +**Optional cleanup**: Restrict seeded agent tokens to 1–2 projects (ZAZZ, MOBDEV) if APIMOD is not needed for agent tests. Keeps seed data minimal. + +--- + +## 5. Project-level access restrictions + +**Current state**: All authenticated users have access to all projects. No USER_PROJECTS or membership model. No project-level restrictions. + +**Limitation**: One leader per project (`PROJECTS.leader_id`); leaders have additional capabilities (manage agent tokens for all users, update status workflows). Non-leaders see only their own agent tokens. + +**Fix**: Implement project-level access control (e.g. USER_PROJECTS, membership, invite flow). Consider multi-leader support if needed. + +--- + +## 6. Multi-instance token cache + +**Current state**: In-memory token cache; single API instance assumed. On agent token create/delete, add/remove from local cache. + +**Limitation**: With multiple API instances (horizontal scaling), token create/delete on instance A is not visible to instance B. Revoked tokens may still work until instance B restarts. + +**Fix**: Shared cache (e.g. Redis) so all instances see token create/delete. Or pub/sub invalidation. Document and implement when scaling. + +## 4. Non-project routes and agent tokens + +**Current state**: Routes without project in path: `GET /projects`, `GET /users`, `GET /users/me`, `GET /tags`, `GET /images`, `GET /status-definitions`, `GET /coordination-types`, `GET /translations/:language`, `GET /health`, etc. + +**Design**: Non-project routes just work—no agent-token restriction. Authorization is limited to project-scoped routes. When a route gains project scoping (e.g. images become project-scoped), add agent-token authorization there. Authorization follows route scoping; don't restrict until the route has project context. From ed0bcb1b419607a48051183d040ecc9f8361d4b1 Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Wed, 4 Mar 2026 18:23:51 -0500 Subject: [PATCH 06/10] Improve zazz-board-api skill: add create checklists, reorganize, dedupe - Add before-creating checklists for deliverable and task - Add Missing data / do not invent rule with example - Move Auth and Env to top; remove redundancy - Trim Key conventions; simplify Fetch spec Made-with: Cursor --- .agents/skills/zazz-board-api/SKILL.md | 369 +---- .zazz/future-fixes.md | 3 + AGENTS.md | 144 +- README.md | 48 +- api/README.md | 88 +- .../helpers/testServerWithSwagger.js | 46 + api/__tests__/routes/openapi.test.mjs | 124 ++ api/package.json | 1 + api/src/routes/projects.js | 114 +- api/src/routes/statusDefinitions.js | 3 + api/src/routes/translations.js | 3 + api/src/schemas/common.js | 102 ++ api/src/schemas/core.js | 90 ++ api/src/schemas/deliverables.js | 193 +++ api/src/schemas/images.js | 139 ++ api/src/schemas/index.js | 24 + api/src/schemas/projects.js | 257 ++++ api/src/schemas/tags.js | 84 ++ api/src/schemas/taskGraph.js | 177 +++ api/src/schemas/tasks.js | 131 ++ api/src/schemas/users.js | 86 ++ api/src/schemas/validation.js | 1199 +---------------- api/src/server.js | 9 +- package-lock.json | 39 + package.json | 1 + scripts/deploy-docker.sh | 24 + 26 files changed, 1824 insertions(+), 1674 deletions(-) create mode 100644 api/__tests__/helpers/testServerWithSwagger.js create mode 100644 api/__tests__/routes/openapi.test.mjs create mode 100644 api/src/schemas/common.js create mode 100644 api/src/schemas/core.js create mode 100644 api/src/schemas/deliverables.js create mode 100644 api/src/schemas/images.js create mode 100644 api/src/schemas/index.js create mode 100644 api/src/schemas/projects.js create mode 100644 api/src/schemas/tags.js create mode 100644 api/src/schemas/taskGraph.js create mode 100644 api/src/schemas/tasks.js create mode 100644 api/src/schemas/users.js create mode 100755 scripts/deploy-docker.sh diff --git a/.agents/skills/zazz-board-api/SKILL.md b/.agents/skills/zazz-board-api/SKILL.md index 8b8a1612..33a094ae 100644 --- a/.agents/skills/zazz-board-api/SKILL.md +++ b/.agents/skills/zazz-board-api/SKILL.md @@ -1,356 +1,123 @@ --- name: "Zazz Board API" type: "rule" -description: "Required API skill for all agents to communicate and manage deliverables" +description: "Required API skill for agents to create and manage deliverables and tasks. Uses live OpenAPI spec; only agent-relevant routes are needed." required_for: ["planner", "coordinator", "worker", "qa", "spec-builder"] --- -# Zazz Board API (Required Rule Skill) +# Zazz Board API (Agent Routes) -**Purpose**: Defines how all agents communicate via the Zazz Board API for creating/managing deliverables and tasks - -**Required By**: All agents (Coordinator, Worker, QA, Spec-Builder) MUST use this API - ---- - -## Overview - -The Zazz Board API provides REST endpoints for managing deliverables and tasks. All agents use this API for: -- Creating and updating deliverables -- Creating and updating tasks -- Posting task comments and questions -- Updating task status -- Managing task relations (dependencies) -- **Agent pub/sub (MVP):** Redis pub/sub backend exposed via API endpoints—heartbeat, agent status, agent-to-agent messaging - ---- - -## Quick Reference - -**API Documentation (Swagger/OpenAPI)**: `{ZAZZ_API_BASE_URL}/docs` - -Open this URL in a browser to explore the full API interactively while developing. +**Purpose**: Agents use this API to create deliverables, create tasks, update content, and change statuses. Projects and users are pre-configured; agents do not create them. --- ## Authentication -All API requests require: -- **Header**: `Authorization: Bearer {ZAZZ_API_TOKEN}` -- **Base URL**: `{ZAZZ_API_BASE_URL}` (e.g., `http://localhost:3000`) - ---- - -## Deliverable Endpoints - -### GET /projects/{project_code}/deliverables/{deliverable_id} -Fetch deliverable details. - -**Parameters**: -- `project_code` (string): Project identifier (e.g., "APP") -- `deliverable_id` (string): Deliverable ID (e.g., "DEL-001") - -**Response**: -```json -{ - "id": "DEL-001", - "project_code": "APP", - "name": "User Authentication", - "status": "PLANNING", - "spec_url": "project-repo/user-auth-SPEC.md", - "plan_url": "project-repo/user-auth-PLAN.md", - "created_at": "2026-02-23T00:00:00Z", - "updated_at": "2026-02-23T10:30:00Z" -} -``` - -### PATCH /projects/{project_code}/deliverables/{deliverable_id} -Update deliverable status or metadata. - -**Request Body**: -```json -{ - "status": "IN_PROGRESS|IN_REVIEW|COMPLETED|BLOCKED", - "plan_url": "updated-url-if-needed" -} -``` +All API requests (except `/openapi.json`, `/health`, `/`, `/db-test`, `/token-info`) require: +- **Header**: `TB_TOKEN: ` or `Authorization: Bearer ` +- **Token**: `ZAZZ_API_TOKEN` or fallback `550e8400-e29b-41d4-a716-446655440000` --- -## Task Endpoints - -### POST /projects/{project_code}/deliverables/{deliverable_id}/tasks -Create a new task. - -**Request Body**: -```json -{ - "title": "Add JWT validation to auth handler", - "goal": "Implement JWT token validation for all protected endpoints", - "instructions": "See implementation plan section 2.3...", - "acceptance_criteria": [ - "JWT token is validated on every protected endpoint", - "Invalid tokens return 401 Unauthorized", - "Expired tokens return 401 Unauthorized" - ], - "test_requirements": { - "unit_tests": ["JWT validation logic"], - "api_tests": ["Valid token acceptance", "Invalid token rejection", "Expired token rejection"], - "e2e_tests": [] - }, - "files_to_modify": ["src/middleware/auth.ts", "tests/middleware/auth.test.ts"], - "depends_on": ["TASK-10"], - "coordinates_with": [], - "estimated_complexity": "medium" -} -``` - -**Response**: -```json -{ - "id": "TASK-42", - "title": "Add JWT validation to auth handler", - "status": "TO_DO", - "created_at": "2026-02-23T10:30:00Z" -} -``` - -### GET /projects/{project_code}/deliverables/{deliverable_id}/tasks -List all tasks for a deliverable. - -**Query Parameters**: -- `status` (optional): Filter by status (TO_DO, IN_PROGRESS, COMPLETED, BLOCKED) -- `assigned_to` (optional): Filter by agent - -**Response**: -```json -[ - { - "id": "TASK-1", - "title": "Setup authentication routes", - "status": "COMPLETED", - "assigned_to": "worker_1" - }, - { - "id": "TASK-42", - "title": "Add JWT validation to auth handler", - "status": "TO_DO", - "depends_on": ["TASK-10"] - } -] -``` - -### GET /projects/{project_code}/deliverables/{deliverable_id}/tasks/{task_id} -Fetch task details. - -**Response**: -```json -{ - "id": "TASK-42", - "title": "Add JWT validation to auth handler", - "goal": "Implement JWT token validation...", - "instructions": "See implementation plan section 2.3...", - "status": "IN_PROGRESS", - "assigned_to": "worker_1", - "created_at": "2026-02-23T10:30:00Z", - "started_at": "2026-02-23T11:00:00Z", - "depends_on": ["TASK-10"], - "coordinates_with": [], - "comments": [...] -} -``` - -### PATCH /projects/{project_code}/deliverables/{deliverable_id}/tasks/{task_id} -Update task status or assignment. - -**Request Body**: -```json -{ - "status": "IN_PROGRESS|COMPLETED|BLOCKED", - "assigned_to": "worker_1" -} -``` +## Environment variables ---- - -## Task Comments - -### POST /projects/{project_code}/deliverables/{deliverable_id}/tasks/{task_id}/comments -Post a comment or question on a task. - -**Request Body**: -```json -{ - "author": "worker_1", - "type": "question|answer|note", - "content": "Should JWT validation happen in middleware or in route handler?" -} -``` - -**Response**: -```json -{ - "id": "comment-001", - "task_id": "TASK-42", - "author": "worker_1", - "type": "question", - "content": "Should JWT validation happen in middleware or in route handler?", - "created_at": "2026-02-23T11:15:00Z" -} -``` - -### GET /projects/{project_code}/deliverables/{deliverable_id}/tasks/{task_id}/comments -Fetch all comments for a task. - -**Response**: -```json -[ - { - "id": "comment-001", - "author": "worker_1", - "type": "question", - "content": "Should JWT validation happen in middleware or in route handler?", - "created_at": "2026-02-23T11:15:00Z" - }, - { - "id": "comment-002", - "author": "coordinator", - "type": "answer", - "reply_to": "comment-001", - "content": "Use middleware per tech spec section 3.2. See example in src/middleware/auth-old.ts", - "created_at": "2026-02-23T11:20:00Z" - } -] -``` +| Variable | Fallback | Purpose | +|----------|----------|---------| +| `ZAZZ_API_BASE_URL` | `http://localhost:3030` | API base; spec at `{base}/openapi.json` | +| `ZAZZ_API_TOKEN` | `550e8400-e29b-41d4-a716-446655440000` | Auth token | +| `PROJECT_CODE` | `ZAZZ` | Default project code | --- -## Agent Pub/Sub (MVP) +## Source of truth: OpenAPI spec -Redis pub/sub backend, exposed via Zazz Board API endpoints. Agents **pull/subscribe** via the API to receive events. Use cases: +**Fetch the live spec for request/response schemas.** The spec is at `{ZAZZ_API_BASE_URL}/openapi.json`. No auth required for the spec. Parse as JSON; use `paths` for routes. -- **Plan approval**: When a deliverable's plan is approved, an event is published. The Coordinator subscribes and picks up plan approval messages to create the initial task graph. -- **Heartbeat**: Publish `last_ping` every ~10 seconds; others detect crashes via timeout -- **Agent status**: Publish IDLE, EXECUTING, BLOCKED, WAITING -- **Agent-to-agent messaging**: Questions, escalations, responses (alternative to polling task comments) +**Agent routes only** — When using the spec, extract only these paths. Ignore projects, users, tags, and other non-agent routes. -Exact endpoint paths TBD; see Swagger at `{ZAZZ_API_BASE_URL}/docs` when implemented. +| Capability | Method | Path | +|------------|--------|------| +| Create deliverable | POST | `/projects/{projectCode}/deliverables` | +| Get deliverable | GET | `/projects/{projectCode}/deliverables/{id}` | +| Update deliverable (add spec path, plan path, git worktree, etc.) | PUT | `/projects/{projectCode}/deliverables/{id}` | +| Change deliverable status | PATCH | `/projects/{projectCode}/deliverables/{id}/status` | +| Approve deliverable (required before creating tasks) | PATCH | `/projects/{projectCode}/deliverables/{id}/approve` | +| List deliverables | GET | `/projects/{projectCode}/deliverables` | +| Create task | POST | `/projects/{code}/deliverables/{delivId}/tasks` | +| Get task | GET | `/projects/{code}/deliverables/{delivId}/tasks/{taskId}` | +| Update task | PUT | `/projects/{code}/deliverables/{delivId}/tasks/{taskId}` | +| Change task status | PATCH | `/projects/{code}/deliverables/{delivId}/tasks/{taskId}/status` | +| Append note to task | PATCH | `/projects/{code}/deliverables/{delivId}/tasks/{taskId}/notes` | +| List deliverable tasks | GET | `/projects/{projectCode}/deliverables/{id}/tasks` | +| Get deliverable graph | GET | `/projects/{code}/deliverables/{delivId}/graph` | +| Check task readiness | GET | `/projects/{code}/tasks/{taskId}/readiness` | +| Get deliverable statuses | GET | `/projects/{code}/deliverable-statuses` | +| List task images | GET | `/tasks/{taskId}/images` | +| Upload task images | POST | `/tasks/{taskId}/images/upload` | +| Get image binary | GET | `/images/{id}` | +| Get image metadata | GET | `/images/{id}/metadata` | --- -## Task Relations +## Fetch spec -### POST /projects/{project_code}/deliverables/{deliverable_id}/tasks/{task_id}/relations -Create a dependency or coordination relation to another task. +**URL**: `{ZAZZ_API_BASE_URL}/openapi.json` (see Environment variables for base URL) -**Request Body**: -```json -{ - "type": "DEPENDS_ON|COORDINATES_WITH|REWORK_FOR", - "related_task_id": "TASK-10" -} -``` - -### GET /projects/{project_code}/deliverables/{deliverable_id}/tasks/{task_id}/relations -Fetch all relations for a task. +Filter `spec.paths` to the agent paths in the table above before reading schemas. --- -## Error Handling +## Create deliverable -All endpoints return standard HTTP status codes: +**POST** `/projects/{projectCode}/deliverables` -- **200**: Success -- **201**: Created -- **400**: Bad Request (invalid parameters) -- **401**: Unauthorized (missing/invalid token) -- **404**: Not Found (resource doesn't exist) -- **409**: Conflict (race condition, e.g., task status changed) -- **500**: Internal Server Error +- **Required body**: `name` (string, 1–30 chars), `type` (enum: `FEATURE`, `BUG_FIX`, `REFACTOR`, `ENHANCEMENT`, `CHORE`, `DOCUMENTATION`). +- **Optional body**: `description`, `dedFilePath`, `planFilePath`, `prdFilePath`, `gitWorktree`, `gitBranch`, `pullRequestUrl`. +- **Response (201)**: `id` (numeric, for API paths), `deliverableId` (e.g. `ZAZZ-4`, for display). Return `deliverableId` to the user. -Error response: -```json -{ - "error": "Task not found", - "status": 404, - "timestamp": "2026-02-23T11:20:00Z" -} -``` +**Before creating:** Ensure you have `name`, `type`, and `projectCode` (path; from user or `PROJECT_CODE` env). If any are missing, ask the human. Do not infer or invent. --- -## Rate Limiting +## Create task -- **Requests per minute**: 60 (per agent) -- **Burst**: Up to 10 concurrent requests -- **Backoff**: Exponential backoff if rate limited (429 status) +**POST** `/projects/{code}/deliverables/{delivId}/tasks` ---- +- **Path params**: `code` = project code, `delivId` = numeric deliverable id from create deliverable response. +- **Required body**: `title` (string, 1–255 chars). +- **Optional body**: `description`, `status`, `priority`, `agentName`, `storyPoints`, `position`, `phase`, `phaseTaskId`, `prompt`, `dependencies`, `gitWorktree`. +- **Prerequisite**: Deliverable must be approved (PATCH `.../approve`) before creating tasks. +- **Response (201)**: `id` (numeric task id). Return `id` to the user. -## Example: Coordinator Creating Task - -```bash -curl -X POST http://localhost:3000/projects/APP/deliverables/DEL-001/tasks \ - -H "Authorization: Bearer ${ZAZZ_API_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{ - "title": "Add JWT validation", - "goal": "Implement JWT token validation", - "instructions": "1. Create JWT validation middleware...", - "acceptance_criteria": ["Valid tokens accepted", "Invalid tokens rejected"], - "test_requirements": { - "unit_tests": ["JWT validation logic"], - "api_tests": ["Token validation"] - }, - "files_to_modify": ["src/middleware/auth.ts"], - "depends_on": ["TASK-10"], - "estimated_complexity": "medium" - }' -``` +**Before creating:** Ensure you have `code`, `delivId`, and `title`. If any are missing, ask the human. Do not infer or invent. --- -## Example: Worker Posting Question +## Missing data — do not invent -```bash -curl -X POST http://localhost:3000/projects/APP/deliverables/DEL-001/tasks/TASK-42/comments \ - -H "Authorization: Bearer ${ZAZZ_API_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{ - "author": "worker_1", - "type": "question", - "content": "Should validation happen in middleware or route handler?" - }' -``` +If required fields are missing, **do not make up values**. Ask the human (or surface through the agent so the human can provide them). Example: "To create the deliverable, I need the type. Please choose: FEATURE, BUG_FIX, REFACTOR, ENHANCEMENT, CHORE, DOCUMENTATION." --- -## Best Practices +## Key conventions -1. **Retry on Failure**: Implement exponential backoff for transient errors -2. **Validate Input**: Check task dependencies are valid before creating -3. **Use Meaningful Messages**: Clear commit messages and comments for audit trail -4. **Batch Operations**: Group related API calls to minimize requests -5. **Cache When Possible**: Cache deliverable SPEC and PLAN locally -6. **Log Everything**: Log all API calls for debugging and audit +- **`id` / `delivId`**: Numeric ids for API paths. `deliverableId` (e.g. ZAZZ-4) is display-only. +- **Update deliverable**: PUT to add `dedFilePath`, `planFilePath`, `gitWorktree`, `gitBranch`, `pullRequestUrl` after creation. +- **Append note**: PATCH `.../tasks/{taskId}/notes` — body `{ "note": "...", "agentName": "..." }`. +- **Claim task**: Include `agentName` when PATCHing status to IN_PROGRESS. +- **Upload images**: POST `/tasks/{taskId}/images/upload` — body `{ images: [{ originalName, contentType, fileSize, base64Data }] }`, `contentType` must be `image/*`. --- -## Environment Variables Required +## Workflow -```bash -export ZAZZ_API_BASE_URL="http://localhost:3000" -export ZAZZ_API_TOKEN="your-api-token" -export PROJECT_CODE="APP" -export DELIVERABLE_ID="DEL-001" -``` +1. Fetch spec from `/openapi.json`. +2. Extract only the agent paths listed above from `spec.paths`. +3. For each operation, read `requestBody.content.application/json.schema` and `responses.201` (or 200) from the spec. +4. Make requests to `{ZAZZ_API_BASE_URL}{path}` with `TB_TOKEN` header and JSON body per spec. --- -## Reference +## Error handling -See examples for: -- How Coordinator creates initial task graph -- How Worker posts questions -- How QA creates rework tasks -- How to handle API errors and retries +200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 409 Conflict, 500 Internal Server Error. Response bodies may include `error` field. diff --git a/.zazz/future-fixes.md b/.zazz/future-fixes.md index 641ae31f..dbd0f656 100644 --- a/.zazz/future-fixes.md +++ b/.zazz/future-fixes.md @@ -70,3 +70,6 @@ Cleanup and improvements **outside the scope** of current deliverables. Add item **Current state**: Routes without project in path: `GET /projects`, `GET /users`, `GET /users/me`, `GET /tags`, `GET /images`, `GET /status-definitions`, `GET /coordination-types`, `GET /translations/:language`, `GET /health`, etc. **Design**: Non-project routes just work—no agent-token restriction. Authorization is limited to project-scoped routes. When a route gains project scoping (e.g. images become project-scoped), add agent-token authorization there. Authorization follows route scoping; don't restrict until the route has project context. + + + use **Dredd/Schemathesis** only if you need full contract testing. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 7c681ee5..d293b6f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -Reference for AI agents and developers: structure, setup, DB, tests, and API. **App name is changing from "Task Blaster" to "Zazz Board"** — code and docs may still reference the old name until the rename is complete. +Reference for AI agents and developers. **Legacy**: If you see "Task Blaster" or `task_blaster_`* (e.g. in DB names, container names, docs), treat it as Zazz Board / `zazz_board_*`. ## CRITICAL — Worktree Workflow (MANDATORY) @@ -11,136 +11,111 @@ Reference for AI agents and developers: structure, setup, DB, tests, and API. ** --- -## Overview +## Standards + +Consult `.zazz/standards/` for authoritative project rules. Index: [.zazz/standards/index.yaml](.zazz/standards/index.yaml) + + +| Standard | Use when | +| ---------------------------------------------------------------- | ----------------------------------- | +| [system-architecture.md](.zazz/standards/system-architecture.md) | Stack, layers, cloud deployment | +| [testing.md](.zazz/standards/testing.md) | Test patterns, PactumJS, TDD rules | +| [coding-styles.md](.zazz/standards/coding-styles.md) | Naming, i18n, conventions, patterns | +| [data-architecture.md](.zazz/standards/data-architecture.md) | Schema, DB conventions, key tables | -**Zazz Board** (formerly Task Blaster) is a Kanban-style orchestration app for coordinating AI agents and humans on software work. -**Stack**: Fastify API (JavaScript, ESM) · React client (Vite) · PostgreSQL 15 (Docker) · Drizzle ORM · Docker Compose +--- + +## Overview + +**Zazz Board** is a Kanban-style orchestration app for coordinating AI agents and humans on software work. -⚠️ **JavaScript only**. No TypeScript. Use `.js` / `.mjs` and JSDoc for types. +**Stack**: Fastify API · React client (Vite, Mantine) · PostgreSQL 15 · Drizzle ORM. JavaScript only (no TypeScript). ### Dogfooding context -This repo **dogfoods** the Zazz Framework: Zazz Board is built with Zazz Board. In the next phase, a Zazz Board instance will run in its own container, orchestrating agents that are building Zazz Board itself. If you're acting as a framework agent (planner, coordinator, worker, qa), the deliverables and tasks you create via the API live in that instance—tracking work on this very repo. Don't be confused by the recursion; it's intentional and helps us find gaps in the framework. +This repo **dogfoods** the Zazz Framework: Zazz Board is built with Zazz Board. Framework agents (planner, coordinator, worker, qa) create deliverables and tasks via the API—tracking work on this repo. The recursion is intentional. -## Zazz agent skills and rules +--- + +## Zazz agent skills -**Skills** (`.agents/skills/`): Role-specific capabilities loaded when acting as a Zazz framework agent. Used by Warp/oz, Claude, etc. when you invoke an agent with a skill (e.g. `oz agent run --skill planner-agent`). +**Skills** (`.agents/skills/`): Role-specific capabilities for framework agents. Load with any role skill. -| Skill | Type | When it applies | -|-------|------|-----------------| -| **zazz-board-api** | rule | Required by all framework agents. Load with any role skill — defines API auth, endpoints, deliverable/task operations. | -| spec-builder-agent | role | Owner + agent creating a deliverable specification | -| planner-agent | role | One-shot decomposition of SPEC → PLAN | -| coordinator-agent | role | Orchestrates execution after plan approval | -| worker-agent | role | Implements tasks | -| qa-agent | role | Verifies AC, creates rework tasks | -**Rules** (`.cursor/rules/`): Always-applied for Cursor agents in this repo (e.g. worktree workflow). Not role-specific. +| Skill | When it applies | +| ------------------ | ------------------------------------------------------ | +| **zazz-board-api** | Required by all framework agents — API auth, endpoints | +| spec-builder-agent | Owner + agent creating deliverable specification | +| planner-agent | One-shot SPEC → PLAN decomposition | +| coordinator-agent | Orchestrates execution after plan approval | +| worker-agent | Implements tasks | +| qa-agent | Verifies AC, creates rework tasks | -**zazz-board-api**: Rule-type skill — required for framework agents, not a Cursor always-apply rule. Load with role skill. + +**Rules** (`.cursor/rules/`): Always-applied for Cursor (e.g. worktree workflow). --- ## Repo layout ``` -├── .agents/skills/ # Zazz agent skills -├── .zazz/ # project.md, standards/, deliverables/ -├── .cursor/rules/ # Cursor always-apply (worktree) -├── api/ # Fastify, routes/, services/, lib/db/schema.js, __tests__/ -├── client/ # React, Vite, Mantine -├── docker-compose.yml # Postgres 5433 +├── .agents/skills/ # Zazz agent skills +├── .zazz/ # project.md, standards/, deliverables/ +├── .cursor/rules/ # Cursor always-apply (worktree) +├── api/ # Fastify, routes/, services/, lib/db/schema.js, __tests__/ +├── client/ # React, Vite, Mantine +├── docker-compose.yml # Postgres 5433 └── package.json ``` -**Cwd**: Commands differ at root vs `api/`. **Worktree**: `GIT_DIR=.bare git worktree add -b ../ main`; copy `.env` from main. See README for full workflow. +**Worktree**: `git worktree add -b ../ main`; copy `api/.env` from main. See [CONTRIBUTOR_SETUP.md](CONTRIBUTOR_SETUP.md). --- ## API -**Auth**: `TB_TOKEN` or `Authorization: Bearer `. **Full spec**: `http://localhost:3030/docs` (Swagger). +**Auth**: `TB_TOKEN` or `Authorization: Bearer `. **Spec**: [http://localhost:3030/docs](http://localhost:3030/docs) (Swagger). -**Key routes for agents**: `GET/POST/PUT/DELETE /projects/:code/deliverables`, `PATCH .../status`, `PATCH .../approve`, `POST/GET/PUT/PATCH/DELETE .../deliverables/:delivId/tasks`, `GET .../graph`, `POST .../tasks/:taskId/relations`. Use `:code` (e.g. ZAZZ) for project. +**Key routes**: `GET/POST/PUT/DELETE /projects/:code/deliverables`, `PATCH .../status`, `PATCH .../approve`, `POST/GET/PUT/PATCH/DELETE .../deliverables/:delivId/tasks`, `GET .../graph`, `POST .../tasks/:taskId/relations`. Use `:code` (e.g. ZAZZ). --- -## Setup (fresh clone) - -- Node 22+, Docker Desktop. -- Postgres runs in container `task_blaster_postgres`, host port **5433**. - -```bash -npm install -npm install --workspace=api -cd client && npm install && cd .. -cp api/.env.example api/.env -``` - -Edit `api/.env`: set both URLs to use password `password` and port 5433: - -- `DATABASE_URL=postgres://postgres:password@localhost:5433/task_blaster_dev` -- `DATABASE_URL_TEST=postgres://postgres:password@localhost:5433/task_blaster_test` - -```bash -npm run docker:up:db -docker ps | grep task_blaster_postgres -``` +## Setup & run -## Running the app +**Setup**: See [CONTRIBUTOR_SETUP.md](CONTRIBUTOR_SETUP.md). TL;DR: Node 22+, Docker (Postgres only), `npm run docker:up:db`, run API and client in separate terminals. -```bash -npm run dev # API :3030, client :3001 -npm run dev:api # API only -npm run dev:client # Client only -``` +**Run**: `npm run dev:api` + `npm run dev:client` (or `npm run dev`). API :3030, client :3001. -Manual API test token: `TB_TOKEN: 550e8400-e29b-41d4-a716-446655440000` +**Token**: `550e8400-e29b-41d4-a716-446655440000` --- ## Database -- **Schema**: `api/lib/db/schema.js` (Drizzle). Pre-v1: no migrations — we push the schema directly (`db:push` / `db:reset`). At v1 we'll switch to migrations for production upgrades. -- **Reset (dev)**: From `api/` run `npm run db:reset` (runs reset-and-seed: drop tables/enums → drizzle-kit push → seed). -- **Seeding order**: users → tags → status definitions → coordination requirement definitions → translations → projects → deliverables → tasks → task-tags → task relations. To add seed data: add or edit files in `scripts/seeders/` and register in `reset-and-seed.js` and `seed-all.js` in dependency order. +**Schema**: `api/lib/db/schema.js`. Pre-v1: push directly (`db:reset` / `db:push`). See [data-architecture.md](.zazz/standards/data-architecture.md). -**Conventions**: Table names UPPER_CASE; columns snake_case in DB, camelCase in JS (Drizzle aliases). Task positions use sparse numbering (e.g. 10, 20) for reorder. +**Reset dev**: `npm run db:reset` (from root or api/). -**Key tables**: USERS (access_token), PROJECTS (code, status_workflow, deliverable_status_workflow, completion_criteria_status), TASKS (deliverable_id, status, position), TASK_RELATIONS (task_id, related_task_id, relation_type), DELIVERABLES, TAGS, TASK_TAGS, STATUS_DEFINITIONS, COORDINATION_REQUIREMENT_DEFINITIONS, TRANSLATIONS, IMAGE_METADATA, IMAGE_DATA. - -### Test database - -`task_blaster_test`. Create: `docker exec task_blaster_postgres psql -U postgres -c "CREATE DATABASE task_blaster_test;" 2>/dev/null || true` then `cd api && DATABASE_URL=.../task_blaster_test npm run db:reset`. See Quick reference for full recreate. +**Test DB**: `zazz_board_test`. Create: `docker exec zazz_board_postgres psql -U postgres -c "CREATE DATABASE zazz_board_test;" 2>/dev/null || true` then `cd api && DATABASE_URL=postgres://postgres:password@localhost:5433/zazz_board_test npm run db:reset`. --- ## Testing -Vitest + PactumJS against `task_blaster_test` on 3031. **Details**: `api/__tests__/README.md`. +Vitest + PactumJS. See [testing.md](.zazz/standards/testing.md) and [api/**tests**/README.md](api/__tests__/README.md). **Run**: `cd api && set -a && source .env && set +a && NODE_ENV=test npm run test` -**Key**: `beforeEach` → `clearTaskData()`. Use ZAZZ project. Helpers: `createTestDeliverable()`, `createTestTask()`. Token: `550e8400-e29b-41d4-a716-446655440000`. - ---- - -## Architecture (concise) - -- **API**: Fastify plugins per route file; `options.dbService`; auth middleware; JSON Schema in `schemas/`; all DB via `databaseService.js`; snake_case in DB, camelCase in JS. -- **Client**: Vite, React hooks, Mantine, react-router-dom v7, @dnd-kit, react-i18next, @uiw/react-md-editor; token in localStorage as TB_TOKEN. Main views: project list, Kanban (deliverable-scoped tasks), task graph (relations, readiness). -- **Task model**: Tasks belong to deliverables and projects; status and deliverable-status workflows are project-level; task graph uses DEPENDS_ON / COORDINATES_WITH with cycle checks and readiness/auto-promotion. - --- ## Troubleshooting -- **drizzle-kit "please install required packages: drizzle-orm"**: From root: `ln -sf ./api/node_modules/drizzle-orm ./node_modules/drizzle-orm`. -- **DATABASE_URL_TEST not set**: Run tests with `set -a && source api/.env && set +a` (from api/). -- **SAFETY CHECK FAILED**: Ensure `task_blaster_test` exists and `DATABASE_URL_TEST` points to it; recreate test DB if needed. -- **Port in use**: `lsof -ti:3031 | xargs kill -9` (test), `3030` (API), `3001` (client). -- **Postgres not running**: `npm run docker:up:db`. +- **drizzle-kit "drizzle-orm"**: From root: `ln -sf ./api/node_modules/drizzle-orm ./node_modules/drizzle-orm` +- **DATABASE_URL_TEST not set**: Source `api/.env` before running tests +- **SAFETY CHECK FAILED**: Ensure `zazz_board_test` exists; recreate test DB +- **Port in use**: `lsof -ti:3030 | xargs kill -9` (API), `lsof -ti:3001 | xargs kill -9` (client), `lsof -ti:3031 | xargs kill -9` (test) +- **Postgres not running**: `npm run docker:up:db` --- @@ -148,11 +123,10 @@ Vitest + PactumJS against `task_blaster_test` on 3031. **Details**: `api/__tests ```bash npm run docker:up:db -cd api && npm run db:reset # Dev DB -docker exec task_blaster_postgres psql -U postgres -c "DROP DATABASE IF EXISTS task_blaster_test; CREATE DATABASE task_blaster_test;" -cd api && DATABASE_URL=postgres://postgres:password@localhost:5433/task_blaster_test npm run db:reset # Test DB +npm run db:reset +docker exec zazz_board_postgres psql -U postgres -c "DROP DATABASE IF EXISTS zazz_board_test; CREATE DATABASE zazz_board_test;" +cd api && DATABASE_URL=postgres://postgres:password@localhost:5433/zazz_board_test npm run db:reset cd api && set -a && source .env && set +a && NODE_ENV=test npm run test -npm run dev # From root +npm run dev ``` -**Related**: `api/__tests__/README.md` (PactumJS), root `README.md` (quick start). diff --git a/README.md b/README.md index 54ef0266..d5151d6f 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ docker compose exec api npm run db:push You can skip env files and use defaults. To customize DB settings, copy `.env.example` to `.env` and change values. -Default credentials: +Default credentials (from `docker-compose.yml`): ```bash POSTGRES_DB=zazz_board_db @@ -129,6 +129,18 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=password ``` +### Docker Compose reference (from `docker-compose.yml`) + +| Service | Container name | Host port | Container port | Notes | +|----------|----------------------|-----------|----------------|--------------------------| +| postgres | zazz_board_postgres | 5433 | 5432 | DB: `zazz_board_db` | +| api | zazz_board_api | 3030 | 3030 | Fastify API | +| client | zazz_board_client | 3001 | 80 | React app (Nginx) | + +- **API**: http://localhost:3030 +- **Client**: http://localhost:3001 +- **Postgres** (from host): `localhost:5433`, database `zazz_board_db` + ## Contributor setup Contributor/committer instructions are in [CONTRIBUTOR_SETUP.md](./CONTRIBUTOR_SETUP.md). @@ -151,10 +163,10 @@ set -a && source .env && set +a docker compose --env-file .env exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT email, access_token FROM \"USERS\";" ``` -### Local URLs +### Local URLs (from `docker-compose.yml`) -- API: `http://localhost:3030` -- Client: `http://localhost:3001` +- API: http://localhost:3030 +- Client: http://localhost:3001 --- @@ -250,11 +262,11 @@ Run the seed script once against Cloud SQL (e.g. from Cloud Shell or a one-off C ## Running tests -Tests use a separate database (`zazz_board_test`). One-time setup: +Tests use a separate database (`zazz_board_test`). One-time setup (from project root): ```bash set -a && source .env && set +a -docker compose --env-file .env exec postgres psql -U "$POSTGRES_USER" -c "CREATE DATABASE zazz_board_test;" 2>/dev/null || true +docker compose exec postgres psql -U postgres -c "CREATE DATABASE zazz_board_test;" 2>/dev/null || true cd api && DATABASE_URL=postgres://postgres:$POSTGRES_PASSWORD@localhost:5433/zazz_board_test npm run db:reset ``` @@ -272,7 +284,14 @@ See [api/__tests__/README.md](./api/__tests__/README.md) for details. The API serves **OpenAPI 3.1** interactive docs (Swagger UI) at **http://localhost:3030/docs** when the API is running. The spec is **generated from Fastify route schemas** (single source of truth; no separate YAML to maintain). It includes all routes, request/response shapes, and security: **TB_TOKEN** (header) and **Bearer** (Authorization header). Access to `/docs` is **token-protected** so only authenticated users and agents can view it in production. -### What’s in the docs +### Spec and docs URLs + +| URL | Purpose | +|-----|---------| +| **/openapi.json** | Raw OpenAPI 3.1 JSON spec — no auth. Use for agents, codegen, tooling. | +| **/docs** | Swagger UI — interactive docs. Use Authorize for try-it-out. | + +### What's in the docs - **Tags**: core, users, projects, deliverables, task-graph, tags, translations, status-definitions, images. - **Security**: Global auth via `TB_TOKEN` or Bearer; the UI has an **Authorize** button to set your token for “Try it out” requests. @@ -284,28 +303,29 @@ You need a valid **access token** (UUID from `USERS.access_token`; seed example: **Option A — Browser (easiest)** 1. Start the API (`npm run dev` or `npm run dev:api`). -2. Open: **http://localhost:3030/docs?token=550e8400-e29b-41d4-a716-446655440000** (replace with your token). -3. The docs page loads. Click **Authorize**, enter the same token in the **TB_TOKEN** field, then **Authorize** → **Close**. +2. Open **http://localhost:3030/docs** (the `?token=` query string does not work; use Authorize instead) +3. Click **Authorize**, enter `550e8400-e29b-41d4-a716-446655440000` in the **TB_TOKEN** field, then **Authorize** → **Close** 4. Use “Try it out” on any route; the token is sent on every request. **Option B — Browser (no token in URL)** If you can send a header with the first request (e.g. a REST client or extension), open `http://localhost:3030/docs` with header `TB_TOKEN: `. Then use **Authorize** in the UI as above for try-it-out. -**Option C — Raw OpenAPI JSON** -Fetch the spec with your token (e.g. for codegen or tooling): +**Option C — Raw OpenAPI JSON (for agents and codegen)** +The spec is available at **`/openapi.json`** (no auth required). Agents and tooling can fetch it directly: ```bash -curl -H "TB_TOKEN: 550e8400-e29b-41d4-a716-446655440000" http://localhost:3030/docs/json +curl http://localhost:3030/openapi.json ``` -**Security**: Don’t share URLs that contain `?token=...`; use that pattern only in trusted environments. +For authenticated requests, use the token header: `curl -H "TB_TOKEN: 550e8400-e29b-41d4-a716-446655440000" http://localhost:3030/openapi.json` + --- ## API authentication **Which routes require a token?** -All API routes except: `GET /health`, `GET /`, `GET /db-test`, `GET /token-info`. The docs at `GET /docs` (and `/docs/*`) also require a valid token. +All API routes except: `GET /health`, `GET /`, `GET /db-test`, `GET /token-info`, `GET /openapi.json`. The docs at `GET /docs` (and `/docs/*`) also require a valid token. **How is the access token set for API calls?** Send one of: diff --git a/api/README.md b/api/README.md index af946be6..e3907fc2 100644 --- a/api/README.md +++ b/api/README.md @@ -1,28 +1,30 @@ -# Task Blaster API +# Zazz Board API -This document covers quick commands for API development. +Quick commands for API development. Values match `docker-compose.yml` at project root. -## Database Setup (First Time) +## Docker Compose reference + +| Service | Container | Host port | DB / URL | +|----------|---------------------|-----------|-----------------------------| +| postgres | zazz_board_postgres | 5433 | `zazz_board_db` | +| api | zazz_board_api | 3030 | http://localhost:3030 | + +## Database setup (first time) **Prerequisites**: Docker must be running. -1. **Start PostgreSQL container** (from project root): +1. **Start PostgreSQL** (from project root): ```bash npm run docker:up:db ``` -2. **Create the dev database** (run once): - ```bash - docker exec task_blaster_postgres psql -U postgres -c "CREATE DATABASE task_blaster_dev;" - ``` - -3. **Reset and seed the database** (from `api/` directory): +2. **Reset and seed** (from `api/` or project root): ```bash npm run db:reset ``` - This drops all tables, recreates the schema from Drizzle ORM, and seeds initial data. + Drops all tables, recreates schema from `lib/db/schema.js`, seeds data. -## Quick Commands +## Quick commands ### Start the API @@ -31,86 +33,64 @@ From project root: npm run dev:api ``` -Or from `api/` directory: +From `api/`: ```bash npm run dev ``` -Or with Node watch flag: -```bash -node --watch src/server.js -``` - -### Reset and Seed Database +### Reset and seed database -From `api/` directory: ```bash -npm run db:reset +cd api && npm run db:reset ``` -What `db:reset` does: -1. Drops all tables (IMAGE_DATA, TASK_RELATIONS, TASKS, PROJECTS, USERS, etc.) -2. Drops custom enum types -3. Runs `drizzle-kit push --force` to recreate schema from `lib/db/schema.js` -4. Seeds all data (users → tags → status definitions → projects → deliverables → tasks → task relations) - -### Seed Data Only (Without Reset) +### Seed only (tables must exist) -If tables already exist and you just want to reseed: ```bash cd api && npm run db:seed ``` -### Check Database State +### Check database state ```bash -# List all tables -docker exec task_blaster_postgres psql -U postgres -d task_blaster_dev -c "\dt" +docker compose exec postgres psql -U postgres -d zazz_board_db -c "\dt" -# Count rows in key tables -docker exec task_blaster_postgres psql -U postgres -d task_blaster_dev -c \ - "SELECT 'USERS' as table_name, count(*) FROM \"USERS\" \ +docker compose exec postgres psql -U postgres -d zazz_board_db -c \ + "SELECT 'USERS' as tbl, count(*) FROM \"USERS\" \ UNION ALL SELECT 'PROJECTS', count(*) FROM \"PROJECTS\" \ - UNION ALL SELECT 'TASKS', count(*) FROM \"TASKS\" \ - UNION ALL SELECT 'TASK_RELATIONS', count(*) FROM \"TASK_RELATIONS\";" + UNION ALL SELECT 'TASKS', count(*) FROM \"TASKS\";" ``` ## Configuration -- **API base URL**: http://localhost:3030 -- **PostgreSQL (Docker)**: `task_blaster_postgres` container - - Host: localhost:5433 (container port 5432) - - Username: postgres - - Password: password (from docker-compose) - - Dev database: `task_blaster_dev` - - Test database: `task_blaster_test` +- **API**: http://localhost:3030 +- **Postgres** (from host): localhost:5433, database `zazz_board_db` +- **api/.env**: `DATABASE_URL=postgres://postgres:password@localhost:5433/zazz_board_db` ## Environment -API reads from `api/.env`: -- `DATABASE_URL` — Connection string for dev database -- `DATABASE_URL_TEST` — Connection string for test database (used when `NODE_ENV=test`) -- `NODE_ENV` — Set to `development` (or `test` when running tests) -- `PORT` — Default 3030 +- `DATABASE_URL` — Dev database (see `api/.env.example`) +- `DATABASE_URL_TEST` — Test database (`zazz_board_test`) when `NODE_ENV=test` +- `PORT` — 3030 ## Troubleshooting **Database doesn't exist:** ```bash -docker exec task_blaster_postgres psql -U postgres -c "CREATE DATABASE task_blaster_dev;" +docker compose exec postgres psql -U postgres -c "CREATE DATABASE zazz_board_db;" 2>/dev/null || true cd api && npm run db:reset ``` -**Drizzle-kit errors with "please install drizzle-orm":** +**drizzle-kit "please install drizzle-orm":** ```bash ln -sf ./api/node_modules/drizzle-orm ./node_modules/drizzle-orm ``` **Connection refused:** -- Check Docker is running: `docker ps | grep task_blaster_postgres` -- Check `.env` DATABASE_URL points to localhost:5433 +- `docker ps | grep zazz_board_postgres` +- `DATABASE_URL` in `api/.env` must use `localhost:5433` -**Port already in use:** +**Port in use:** ```bash lsof -ti:3030 | xargs kill -9 ``` diff --git a/api/__tests__/helpers/testServerWithSwagger.js b/api/__tests__/helpers/testServerWithSwagger.js new file mode 100644 index 00000000..d410a51e --- /dev/null +++ b/api/__tests__/helpers/testServerWithSwagger.js @@ -0,0 +1,46 @@ +/** + * Creates a Fastify app with Swagger registered (for OpenAPI spec tests). + * Does NOT start HTTP server — use app.inject() or app.swagger(). + */ +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import swagger from '@fastify/swagger'; +import routes from '../../src/routes/index.js'; +import { tokenService } from '../../src/services/tokenService.js'; + +const BASE_URL = 'http://localhost:3030'; + +export async function createTestServerWithSwagger() { + const app = Fastify({ logger: false }); + + await app.register(cors, { + origin: ['http://localhost:3001', 'http://127.0.0.1:3001'], + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'] + }); + + await app.register(swagger, { + openapi: { + openapi: '3.1.0', + info: { + title: 'Zazz Board API', + description: 'Test', + version: '1.0.0' + }, + servers: [{ url: BASE_URL, description: 'Local' }], + components: { + securitySchemes: { + TB_TOKEN: { type: 'apiKey', in: 'header', name: 'TB_TOKEN' }, + Bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'UUID' } + } + }, + security: [{ TB_TOKEN: [] }] + } + }); + + await app.register(routes); + + await tokenService.initialize(); + + return app; +} diff --git a/api/__tests__/routes/openapi.test.mjs b/api/__tests__/routes/openapi.test.mjs new file mode 100644 index 00000000..ddc58de4 --- /dev/null +++ b/api/__tests__/routes/openapi.test.mjs @@ -0,0 +1,124 @@ +/** + * OpenAPI / Swagger documentation tests. + * Validates spec correctness via openapi-schema-validator. + * Prevents regressions when splitting or updating schema files. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import OpenAPISchemaValidatorPkg from 'openapi-schema-validator'; +import { createTestServerWithSwagger } from '../helpers/testServerWithSwagger.js'; + +const OpenAPISchemaValidator = OpenAPISchemaValidatorPkg.default; + +let app; + +beforeAll(async () => { + app = await createTestServerWithSwagger(); +}); + +afterAll(async () => { + if (app) await app.close(); +}); + +describe('OpenAPI / Swagger documentation', () => { + it('should produce a valid OpenAPI 3.x spec (schema validation)', async () => { + const spec = await app.swagger(); + const validator = new OpenAPISchemaValidator({ version: 3 }); + const result = validator.validate(spec); + expect(result.errors, `OpenAPI spec validation failed: ${JSON.stringify(result.errors)}`).toHaveLength(0); + }); + + it('should return spec with required structure', async () => { + const spec = await app.swagger(); + expect(spec).toBeDefined(); + expect(spec.openapi).toMatch(/^3\.\d+\.\d+$/); + expect(spec.info).toBeDefined(); + expect(spec.info.title).toBe('Zazz Board API'); + expect(spec.paths).toBeDefined(); + expect(typeof spec.paths).toBe('object'); + }); + + it('should document core agent operations: create deliverable', async () => { + const spec = await app.swagger(); + const path = spec.paths['/projects/{projectCode}/deliverables']; + expect(path).toBeDefined(); + expect(path.post).toBeDefined(); + expect(path.post.summary).toBeDefined(); + expect(path.post.description).toContain('id'); + expect(path.post.description).toContain('deliverableId'); + expect(path.post.requestBody?.content?.['application/json']?.schema?.properties?.name).toBeDefined(); + expect(path.post.requestBody?.content?.['application/json']?.schema?.properties?.dedFilePath).toBeDefined(); + expect(path.post.requestBody?.content?.['application/json']?.schema?.properties?.planFilePath).toBeDefined(); + }); + + it('should document core agent operations: create task', async () => { + const spec = await app.swagger(); + const path = spec.paths['/projects/{code}/deliverables/{delivId}/tasks']; + expect(path).toBeDefined(); + expect(path.post).toBeDefined(); + expect(path.post.description).toContain('delivId'); + expect(path.post.description).toContain('numeric'); + expect(path.post.requestBody?.content?.['application/json']?.schema?.required).toContain('title'); + }); + + it('should document core agent operations: update deliverable', async () => { + const spec = await app.swagger(); + const path = spec.paths['/projects/{projectCode}/deliverables/{id}']; + expect(path).toBeDefined(); + expect(path.put).toBeDefined(); + const bodySchema = path.put.requestBody?.content?.['application/json']?.schema; + expect(bodySchema?.properties?.dedFilePath).toBeDefined(); + expect(bodySchema?.properties?.planFilePath).toBeDefined(); + expect(bodySchema?.properties?.gitWorktree).toBeDefined(); + }); + + it('should document core agent operations: change deliverable status', async () => { + const spec = await app.swagger(); + const path = spec.paths['/projects/{projectCode}/deliverables/{id}/status']; + expect(path).toBeDefined(); + expect(path.patch).toBeDefined(); + expect(path.patch.requestBody?.content?.['application/json']?.schema?.required).toContain('status'); + }); + + it('should document core agent operations: change task status', async () => { + const spec = await app.swagger(); + const path = spec.paths['/projects/{code}/deliverables/{delivId}/tasks/{taskId}/status']; + expect(path).toBeDefined(); + expect(path.patch).toBeDefined(); + expect(path.patch.description).toContain('agentName'); + expect(path.patch.requestBody?.content?.['application/json']?.schema?.properties?.status).toBeDefined(); + }); + + it('should document key paths with tags and summaries', async () => { + const spec = await app.swagger(); + const keyPaths = [ + '/projects/{projectCode}/deliverables', + '/projects/{projectCode}/deliverables/{id}', + '/projects/{projectCode}/deliverables/{id}/approve', + '/projects/{code}/deliverables/{delivId}/tasks', + '/projects/{code}/deliverables/{delivId}/tasks/{taskId}', + '/projects/{code}/deliverables/{delivId}/graph', + '/projects/{code}/tasks/{taskId}/relations', + '/projects/{code}/tasks/{taskId}/readiness', + '/health' + ]; + for (const p of keyPaths) { + expect(spec.paths[p], `Missing path: ${p}`).toBeDefined(); + const methods = Object.keys(spec.paths[p]).filter((m) => !m.startsWith('/')); + for (const m of methods) { + if (['get', 'post', 'put', 'patch', 'delete'].includes(m)) { + expect(spec.paths[p][m].summary, `Missing summary for ${m.toUpperCase()} ${p}`).toBeDefined(); + } + } + } + }); + + it('should include common operations in info description', async () => { + const spec = await app.swagger(); + const desc = spec.info.description || ''; + expect(desc).toContain('Create deliverable'); + expect(desc).toContain('Create task'); + expect(desc).toContain('Update deliverable'); + expect(desc).toContain('Change deliverable status'); + expect(desc).toContain('Change task status'); + }); +}); diff --git a/api/package.json b/api/package.json index 06348ed5..b94460b4 100644 --- a/api/package.json +++ b/api/package.json @@ -49,6 +49,7 @@ "@types/node": "^22.10.7", "@vitest/coverage-v8": "^4.0.15", "drizzle-kit": "^0.31.8", + "openapi-schema-validator": "^12.1.3", "pactum": "^3.8.0", "vitest": "^4.0.15" } diff --git a/api/src/routes/projects.js b/api/src/routes/projects.js index 6a18d30b..25aab4d8 100644 --- a/api/src/routes/projects.js +++ b/api/src/routes/projects.js @@ -110,12 +110,15 @@ export default async function projectRoutes(fastify, options) { // GET /projects/:id/kanban/tasks/column/:status - Get column positions for a specific status fastify.get('/projects/:id/kanban/tasks/column/:status', { schema: { + tags: ['projects'], + summary: 'Get kanban column positions', + description: 'Returns task order within a status column for the Kanban board. id = numeric project id, status = column status (e.g. TO_DO, IN_PROGRESS).', params: { type: 'object', required: ['id', 'status'], properties: { - id: { type: 'string', pattern: '^\\d+$' }, - status: { type: 'string', pattern: '^[A-Z_]+$' } + id: { type: 'string', pattern: '^\\d+$', description: 'Numeric project id.' }, + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Column status (e.g. TO_DO, IN_PROGRESS).' } } } } @@ -135,12 +138,15 @@ export default async function projectRoutes(fastify, options) { // PATCH /projects/:code/kanban/tasks/column/:status/positions - Update multiple task positions in a column fastify.patch('/projects/:code/kanban/tasks/column/:status/positions', { schema: { + tags: ['projects'], + summary: 'Bulk update column positions', + description: 'Updates positions of multiple tasks in a Kanban column. Body: { positionUpdates: [{ taskId, newPosition }] }. Use when drag-and-drop reorders several tasks.', params: { type: 'object', required: ['code', 'status'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, // Project code like PROJ, FEATURE - status: { type: 'string', pattern: '^[A-Z_]+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Column status.' } } }, body: { @@ -153,10 +159,11 @@ export default async function projectRoutes(fastify, options) { type: 'object', required: ['taskId', 'newPosition'], properties: { - taskId: { type: 'number' }, - newPosition: { type: 'number' } + taskId: { type: 'number', description: 'Numeric task id.' }, + newPosition: { type: 'number', description: 'New position in column.' } } - } + }, + description: 'Array of { taskId, newPosition } for each moved task.' } } } @@ -183,20 +190,23 @@ export default async function projectRoutes(fastify, options) { // PATCH /projects/:code/kanban/tasks/:taskId/position - Update single task position fastify.patch('/projects/:code/kanban/tasks/:taskId/position', { schema: { + tags: ['projects'], + summary: 'Update single task position', + description: 'Moves a task to a new position within a column. Body: { newPosition, status }. Use when drag-and-drop moves one task.', params: { type: 'object', required: ['code', 'taskId'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, // Project code like PROJ, FEATURE - taskId: { type: 'string', pattern: '^\\d+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } }, body: { type: 'object', required: ['newPosition', 'status'], properties: { - newPosition: { type: 'number' }, - status: { type: 'string', pattern: '^[A-Z_]+$' } + newPosition: { type: 'number', description: 'New position in column.' }, + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Column status (task stays in same column or moves).' } } } } @@ -346,11 +356,14 @@ export default async function projectRoutes(fastify, options) { // GET /projects/:code/statuses - Get project's status workflow fastify.get('/projects/:code/statuses', { schema: { + tags: ['projects'], + summary: 'Get task status workflow', + description: 'Returns the project\'s task status workflow (e.g. TO_DO, IN_PROGRESS, QA, COMPLETED). Use to know valid statuses for PATCH task status.', params: { type: 'object', required: ['code'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' } } }, response: { @@ -384,11 +397,14 @@ export default async function projectRoutes(fastify, options) { // PUT /projects/:code/statuses - Update project's status workflow (leaders only) fastify.put('/projects/:code/statuses', { schema: { + tags: ['projects'], + summary: 'Update task status workflow', + description: 'Updates the project\'s task status workflow. Leaders only. Status codes must exist in STATUS_DEFINITIONS. Cannot remove statuses that have tasks.', params: { type: 'object', required: ['code'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' } } }, body: { @@ -464,11 +480,14 @@ export default async function projectRoutes(fastify, options) { // GET /projects/:code/deliverable-statuses - Get project's deliverable status workflow fastify.get('/projects/:code/deliverable-statuses', { schema: { + tags: ['projects'], + summary: 'Get deliverable status workflow', + description: 'Returns the project\'s deliverable status workflow (e.g. PLANNING, IN_PROGRESS, IN_REVIEW, STAGED, DONE). Use to know valid statuses for PATCH deliverable status.', params: { type: 'object', required: ['code'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' } } } } @@ -487,11 +506,14 @@ export default async function projectRoutes(fastify, options) { // PUT /projects/:code/deliverable-statuses - Update deliverable status workflow (leaders only) fastify.put('/projects/:code/deliverable-statuses', { schema: { + tags: ['projects'], + summary: 'Update deliverable status workflow', + description: 'Updates the project\'s deliverable status workflow. Leaders only. Cannot remove statuses that have deliverables.', params: { type: 'object', required: ['code'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' } } }, body: { @@ -654,13 +676,16 @@ export default async function projectRoutes(fastify, options) { // DELETE /projects/:code/deliverables/:delivId/tasks/:taskId - Delete task fastify.delete('/projects/:code/deliverables/:delivId/tasks/:taskId', { schema: { + tags: ['projects'], + summary: 'Delete task (deliverable-scoped)', + description: 'Deletes a task. code = project code, delivId = numeric deliverable id, taskId = numeric task id. Verifies task belongs to deliverable.', params: { type: 'object', required: ['code', 'delivId', 'taskId'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } } } @@ -703,17 +728,17 @@ export default async function projectRoutes(fastify, options) { type: 'object', required: ['code', 'delivId', 'taskId'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } }, body: { type: 'object', required: ['note'], properties: { - note: { type: 'string', minLength: 1, maxLength: 5000 }, - agentName: { type: 'string', maxLength: 255 } + note: { type: 'string', minLength: 1, maxLength: 5000, description: 'Progress message. Appended as "[timestamp] [agentName]: note".' }, + agentName: { type: 'string', maxLength: 255, description: 'Agent or user name for the log entry. Defaults to authenticated user.' } }, additionalProperties: false } @@ -760,23 +785,23 @@ export default async function projectRoutes(fastify, options) { fastify.patch('/projects/:code/deliverables/:delivId/tasks/:taskId/status', { schema: { tags: ['projects'], - summary: 'Change task status (agent claim)', - description: 'Change task status. Include agentName to claim the task: { status: "IN_PROGRESS", agentName: "worker-1" }. Returns 409 if task is cancelled.', + summary: 'Change task status (deliverable-scoped)', + description: 'Changes task status within a deliverable. Use when you know the deliverable id: pick up (TO_DO→IN_PROGRESS), complete (IN_PROGRESS→COMPLETED), or move to QA. Include agentName to claim: { status: "IN_PROGRESS", agentName: "worker-1" }. delivId and taskId are numeric ids from create deliverable and create task. Returns 409 if task is cancelled.', params: { type: 'object', required: ['code', 'delivId', 'taskId'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id from create deliverable.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id from create task.' } } }, body: { type: 'object', required: ['status'], properties: { - status: { type: 'string', pattern: '^[A-Z_]+$' }, - agentName: { type: 'string', maxLength: 50 } + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Target status (e.g. IN_PROGRESS, COMPLETED).' }, + agentName: { type: 'string', maxLength: 50, description: 'Optional. Set to claim the task when moving to IN_PROGRESS.' } } } } @@ -846,9 +871,9 @@ export default async function projectRoutes(fastify, options) { type: 'object', required: ['code', 'delivId', 'taskId'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } } } @@ -888,20 +913,23 @@ export default async function projectRoutes(fastify, options) { // PATCH /projects/:code/deliverables/:delivId/tasks/:taskId/reorder - Reorder task position fastify.patch('/projects/:code/deliverables/:delivId/tasks/:taskId/reorder', { schema: { + tags: ['projects'], + summary: 'Reorder task', + description: 'Changes task position within its column. Body: { position: number }. Use when reordering tasks in a deliverable.', params: { type: 'object', required: ['code', 'delivId', 'taskId'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } }, body: { type: 'object', required: ['position'], properties: { - position: { type: 'integer', minimum: 0 } + position: { type: 'integer', minimum: 0, description: 'New position in column.' } } } } @@ -938,13 +966,16 @@ export default async function projectRoutes(fastify, options) { // PUT /projects/:code/deliverables/:delivId/tasks/:taskId/tags - Set task tags fastify.put('/projects/:code/deliverables/:delivId/tasks/:taskId/tags', { schema: { + tags: ['projects'], + summary: 'Set task tags', + description: 'Replaces all tags on a task. Body: { tagIds: string[] }. Use tag ids from GET /tags.', params: { type: 'object', required: ['code', 'delivId', 'taskId'], properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } }, body: { @@ -954,7 +985,8 @@ export default async function projectRoutes(fastify, options) { tagIds: { type: 'array', items: { type: 'string' }, - uniqueItems: true + uniqueItems: true, + description: 'Array of tag ids (from GET /tags). Replaces existing tags.' } } } diff --git a/api/src/routes/statusDefinitions.js b/api/src/routes/statusDefinitions.js index f391d378..4f77455a 100644 --- a/api/src/routes/statusDefinitions.js +++ b/api/src/routes/statusDefinitions.js @@ -9,6 +9,9 @@ export default async function statusDefinitionsRoutes(fastify, options) { // GET /status-definitions - Get all available status codes fastify.get('/status-definitions', { schema: { + tags: ['status-definitions'], + summary: 'List status definitions', + description: 'Returns all valid status codes (e.g. TO_DO, IN_PROGRESS, COMPLETED) with descriptions. Use to validate status values before PATCH task/deliverable status.', response: { 200: { type: 'array', diff --git a/api/src/routes/translations.js b/api/src/routes/translations.js index 88839674..1b1011f6 100644 --- a/api/src/routes/translations.js +++ b/api/src/routes/translations.js @@ -9,6 +9,9 @@ export default async function translationsRoutes(fastify, options) { // GET /translations/:language - Get translations for specified language fastify.get('/translations/:language', { schema: { + tags: ['translations'], + summary: 'Get translations by language', + description: 'Returns i18n translations for a two-letter language code (e.g. en, es). Used by the client for localized UI.', params: { type: 'object', required: ['language'], diff --git a/api/src/schemas/common.js b/api/src/schemas/common.js new file mode 100644 index 00000000..ee47b3e6 --- /dev/null +++ b/api/src/schemas/common.js @@ -0,0 +1,102 @@ +/** + * Shared parameter schemas and response shapes for Zazz Board API. + * Used across domain schema files. + */ + +export const idParam = { + type: 'object', + required: ['id'], + properties: { + id: { type: 'string', pattern: '^\\d+$', description: 'Numeric id.' } + } +}; + +export const codeParam = { + type: 'object', + required: ['code'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ). Uppercase letters and numbers.' } + } +}; + +export const taskIdParam = { + type: 'object', + required: ['taskId'], + properties: { + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } + } +}; + +export const taskResponseSchema = { + type: 'object', + properties: { + id: { type: 'number', description: 'Numeric task id. Use for API paths.' }, + projectId: { type: 'number', description: 'Project id.' }, + deliverableId: { type: 'number', description: 'Deliverable id.' }, + title: { type: 'string', description: 'Task title.' }, + status: { type: 'string', enum: ['TO_DO', 'READY', 'IN_PROGRESS', 'QA', 'COMPLETED'], description: 'Current workflow status.' }, + position: { type: 'number', description: 'Sort order in column.' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, + storyPoints: { type: 'number', nullable: true }, + assigneeId: { type: 'number', nullable: true }, + prompt: { type: 'string', nullable: true, description: 'Goal, instructions, acceptance criteria for agent.' }, + isBlocked: { type: 'boolean', nullable: true }, + blockedReason: { type: 'string', nullable: true }, + gitWorktree: { type: 'string', nullable: true, description: 'Git worktree for implementation.' }, + notes: { type: 'string', nullable: true, description: 'Append-only progress log.' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' } + } +}; + +export const deliverableResponseSchema = { + type: 'object', + properties: { + id: { type: 'number', description: 'Numeric primary key. Use this for API paths (e.g. create task, update deliverable).' }, + projectId: { type: 'number' }, + deliverableId: { type: 'string', description: 'Human-readable ID (e.g. ZAZZ-4). Use for display; use id for API calls.' }, + name: { type: 'string' }, + description: { type: 'string', nullable: true }, + type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'] }, + status: { type: 'string' }, + dedFilePath: { type: 'string', nullable: true, description: 'Relative path or URL to the deliverable specification (SPEC) document.' }, + planFilePath: { type: 'string', nullable: true, description: 'Relative path or URL to the implementation plan (PLAN) document.' }, + prdFilePath: { type: 'string', nullable: true, description: 'Relative path or URL to the PRD document.' }, + gitWorktree: { type: 'string', nullable: true, description: 'Git worktree name used for implementation (e.g. feature-auth).' }, + gitBranch: { type: 'string', nullable: true, description: 'Git branch name for the deliverable work (e.g. feature-auth).' }, + pullRequestUrl: { type: 'string', nullable: true }, + approvedBy: { type: 'number', nullable: true }, + approvedAt: { type: 'string', format: 'date-time', nullable: true }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' } + } +}; + +export const tagNamePattern = '^[a-z0-9]+(-[a-z0-9]+)*$'; + +export const errorResponseSchema = { + type: 'object', + properties: { + statusCode: { type: 'number', example: 400 }, + error: { type: 'string', example: 'Bad Request' }, + message: { type: 'string', example: 'Invalid request parameters' } + } +}; + +export const responseSchemas = { + error: { + type: 'object', + properties: { + error: { type: 'string' }, + message: { type: 'string' }, + statusCode: { type: 'integer' } + } + }, + success: { + type: 'object', + properties: { + success: { type: 'boolean' }, + data: { type: 'object' } + } + } +}; diff --git a/api/src/schemas/core.js b/api/src/schemas/core.js new file mode 100644 index 00000000..9445980f --- /dev/null +++ b/api/src/schemas/core.js @@ -0,0 +1,90 @@ +/** + * Core route schemas (health, root, db-test, token-info). + */ + +export const coreSchemas = { + getHealth: { + tags: ['core'], + summary: 'Health check', + description: 'Returns API health and token cache stats. No authentication required.', + security: [], + response: { + 200: { + description: 'API is healthy', + type: 'object', + properties: { + status: { type: 'string', example: 'ok' }, + timestamp: { type: 'string', format: 'date-time' }, + auth: { + type: 'object', + properties: { + tokenCacheInitialized: { type: 'boolean' }, + userCount: { type: 'number' } + } + } + } + } + } + }, + + getRoot: { + tags: ['core'], + summary: 'API info', + description: 'Returns API message and endpoint list. No authentication required.', + security: [], + response: { + 200: { + description: 'API info', + type: 'object', + properties: { + message: { type: 'string' }, + version: { type: 'string' }, + endpoints: { type: 'array', items: { type: 'string' } } + } + } + } + }, + + getDbTest: { + tags: ['core'], + summary: 'Database connectivity test', + description: 'Tests database connection. No authentication required.', + security: [], + response: { + 200: { + description: 'Database connected', + type: 'object', + properties: { + status: { type: 'string' }, + result: {} + } + }, + 500: { + description: 'Database connection failed', + type: 'object', + properties: { + error: { type: 'string' }, + details: { type: 'string' } + } + } + } + }, + + getTokenInfo: { + tags: ['core'], + summary: 'Token cache debug', + description: 'Returns token cache stats for debugging. No authentication required.', + security: [], + response: { + 200: { + description: 'Token cache info', + type: 'object', + properties: { + cacheInitialized: { type: 'boolean' }, + userCount: { type: 'number' }, + hasTokens: { type: 'boolean' } + } + } + } + } +}; diff --git a/api/src/schemas/deliverables.js b/api/src/schemas/deliverables.js new file mode 100644 index 00000000..ef8a9ac0 --- /dev/null +++ b/api/src/schemas/deliverables.js @@ -0,0 +1,193 @@ +/** + * Deliverable route schemas. + */ + +import { taskResponseSchema, deliverableResponseSchema } from './common.js'; + +export const deliverableSchemas = { + getProjectDeliverables: { + tags: ['deliverables'], + summary: 'List deliverables', + description: 'Returns deliverables for a project. Filter by status (e.g. IN_PROGRESS) or type (e.g. FEATURE) via query params.', + params: { + type: 'object', + required: ['projectCode'], + properties: { projectCode: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' } } + }, + querystring: { + type: 'object', + properties: { + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Filter by deliverable status.' }, + type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'], description: 'Filter by deliverable type.' } + } + }, + response: { + 200: { + description: 'List of deliverables', + type: 'array', + items: deliverableResponseSchema + } + } + }, + + getDeliverableById: { + tags: ['deliverables'], + summary: 'Get deliverable by ID', + description: 'Returns deliverable with dedFilePath (SPEC), planFilePath (PLAN), prdFilePath for document retrieval.', + params: { + type: 'object', + required: ['projectCode', 'id'], + properties: { + projectCode: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + id: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' } + } + }, + response: { + 200: { description: 'Deliverable', ...deliverableResponseSchema } + } + }, + + createDeliverable: { + tags: ['deliverables'], + summary: 'Create deliverable', + description: 'Creates a new deliverable card in the project. Use this when starting work on a new feature, bug fix, or other work item. The response includes id (numeric—use for create task and other API paths) and deliverableId (string, e.g. ZAZZ-4—use for display). You can include dedFilePath and planFilePath on create if known, or add them later via update deliverable.', + params: { + type: 'object', + required: ['projectCode'], + properties: { + projectCode: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ). Uppercase letters and numbers.' } + } + }, + body: { + type: 'object', + required: ['name', 'type'], + properties: { + name: { type: 'string', minLength: 1, maxLength: 30, description: 'Short name for the deliverable (e.g. "User Auth").' }, + description: { type: 'string', description: 'Optional longer description.' }, + type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'], description: 'Type of work.' }, + dedFilePath: { type: 'string', maxLength: 500, description: 'Relative path to the SPEC document (e.g. .zazz/deliverables/user-auth-SPEC.md). Add when SPEC exists.' }, + planFilePath: { type: 'string', maxLength: 500, description: 'Relative path to the PLAN document (e.g. .zazz/deliverables/user-auth-PLAN.md). Add when PLAN exists.' }, + prdFilePath: { type: 'string', maxLength: 500, description: 'Relative path to the PRD document.' }, + gitWorktree: { type: 'string', maxLength: 255, description: 'Git worktree name for implementation (e.g. feature-auth). Add when work begins.' }, + gitBranch: { type: 'string', maxLength: 255, description: 'Git branch name (e.g. feature-auth). Add when work begins.' }, + pullRequestUrl: { type: 'string', maxLength: 500, description: 'URL to the PR when ready for review.' } + }, + additionalProperties: false + }, + response: { + 201: { description: 'Deliverable created. Use id for create task and update paths; deliverableId for display.', ...deliverableResponseSchema } + } + }, + + updateDeliverable: { + tags: ['deliverables'], + summary: 'Update deliverable', + description: 'Updates deliverable metadata. Use this to add or change: dedFilePath (after SPEC is written), planFilePath (after PLAN is approved), gitWorktree and gitBranch (when work begins), pullRequestUrl (when PR is opened). Send only the fields you are updating. id is the numeric id from create deliverable or list deliverables.', + params: { + type: 'object', + required: ['projectCode', 'id'], + properties: { + projectCode: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + id: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id from create or list.' } + } + }, + body: { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 30 }, + description: { type: 'string' }, + type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'] }, + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Deliverable status (e.g. PLANNING, IN_PROGRESS, IN_REVIEW, STAGED, DONE). Use update deliverable status for status-only changes.' }, + dedFilePath: { type: 'string', maxLength: 500, description: 'Relative path to SPEC (e.g. .zazz/deliverables/user-auth-SPEC.md). Set when SPEC is created.' }, + planFilePath: { type: 'string', maxLength: 500, description: 'Relative path to PLAN (e.g. .zazz/deliverables/user-auth-PLAN.md). Set when PLAN is approved.' }, + prdFilePath: { type: 'string', maxLength: 500, description: 'Relative path to PRD.' }, + gitWorktree: { type: 'string', maxLength: 255, description: 'Git worktree name (e.g. feature-auth). Set when implementation begins.' }, + gitBranch: { type: 'string', maxLength: 255, description: 'Git branch name. Set when implementation begins.' }, + pullRequestUrl: { type: 'string', maxLength: 500, description: 'PR URL when ready for review.' }, + position: { type: 'integer', minimum: 0, description: 'Sort order in deliverable list.' } + }, + additionalProperties: false + }, + response: { + 200: { description: 'Deliverable updated', ...deliverableResponseSchema } + } + }, + + deleteDeliverable: { + tags: ['deliverables'], + summary: 'Delete deliverable', + description: 'Deletes a deliverable by numeric id. Cascades to tasks. Use projectCode (e.g. ZAZZ) and numeric id.', + params: { + type: 'object', + required: ['projectCode', 'id'], + properties: { + projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' }, + id: { type: 'string', pattern: '^\\d+$' } + } + }, + response: { + 200: { description: 'Deliverable deleted' } + } + }, + + updateDeliverableStatus: { + tags: ['deliverables'], + summary: 'Update deliverable status', + description: 'Changes the deliverable status in the workflow. Use to move a deliverable through stages: PLANNING → IN_PROGRESS (when work begins) → IN_REVIEW (when PR is ready) → STAGED → DONE. Valid statuses come from the project\'s deliverable workflow. Use GET /projects/{code}/deliverable-statuses to see allowed values.', + params: { + type: 'object', + required: ['projectCode', 'id'], + properties: { + projectCode: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + id: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' } + } + }, + body: { + type: 'object', + required: ['status'], + properties: { status: { type: 'string', pattern: '^[A-Z_]+$' } }, + additionalProperties: false + }, + response: { + 200: { description: 'Deliverable status updated', ...deliverableResponseSchema } + } + }, + + approveDeliverable: { + tags: ['deliverables'], + summary: 'Approve deliverable plan', + description: 'Approves the deliverable plan. Required before tasks can be created. Use after PLAN is finalized.', + params: { + type: 'object', + required: ['projectCode', 'id'], + properties: { + projectCode: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + id: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' } + } + }, + response: { + 200: { description: 'Deliverable approved', ...deliverableResponseSchema } + } + }, + + getDeliverableTasks: { + tags: ['deliverables'], + summary: 'List tasks for deliverable', + description: 'Returns all tasks in a deliverable. Use to check task completion status or list tasks for a deliverable.', + params: { + type: 'object', + required: ['projectCode', 'id'], + properties: { + projectCode: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + id: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' } + } + }, + response: { + 200: { + description: 'List of tasks', + type: 'array', + items: taskResponseSchema + } + } + } +}; diff --git a/api/src/schemas/images.js b/api/src/schemas/images.js new file mode 100644 index 00000000..f7afe725 --- /dev/null +++ b/api/src/schemas/images.js @@ -0,0 +1,139 @@ +/** + * Image route schemas. + */ + +export const imageSchemas = { + getTaskImages: { + tags: ['images'], + summary: 'List task images', + description: 'Returns metadata (id, originalName, contentType, fileSize) for images attached to a task. Use GET /images/:id for binary.', + params: { + type: 'object', + required: ['taskId'], + properties: { taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } + }, + response: { + 200: { + description: 'List of image metadata', + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'number' }, + taskId: { type: 'number' }, + originalName: { type: 'string' }, + contentType: { type: 'string' }, + fileSize: { type: 'number' } + } + } + } + } + }, + + uploadTaskImages: { + tags: ['images'], + summary: 'Upload task images', + description: 'Upload one or more images as base64. Body: { images: [{ originalName, contentType, fileSize, base64Data }] }. contentType must be image/*.', + params: { + type: 'object', + required: ['taskId'], + properties: { taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } } + }, + body: { + type: 'object', + required: ['images'], + properties: { + images: { + type: 'array', + items: { + type: 'object', + required: ['originalName', 'contentType', 'fileSize', 'base64Data'], + properties: { + originalName: { type: 'string' }, + contentType: { type: 'string', pattern: '^image/' }, + fileSize: { type: 'integer', minimum: 1 }, + base64Data: { type: 'string' } + } + } + } + } + }, + response: { + 201: { + description: 'Images uploaded', + type: 'object', + properties: { + success: { type: 'boolean' }, + images: { type: 'array', items: { type: 'object' } }, + count: { type: 'number' } + } + } + } + }, + + getImageById: { + tags: ['images'], + summary: 'Get image binary', + description: 'Returns image binary. Use id from GET /tasks/:taskId/images. Content-Type indicates format.', + params: { + type: 'object', + required: ['id'], + properties: { id: { type: 'string', pattern: '^\\d+$', description: 'Numeric image id.' } } + }, + response: { + 200: { description: 'Image binary' }, + 404: { description: 'Image not found' } + } + }, + + getImageMetadata: { + tags: ['images'], + summary: 'Get image metadata', + description: 'Returns image metadata (id, taskId, originalName, contentType, fileSize) without binary. Use when you need metadata only.', + params: { + type: 'object', + required: ['id'], + properties: { id: { type: 'string', pattern: '^\\d+$', description: 'Numeric image id.' } } + }, + response: { + 200: { + description: 'Image metadata', + type: 'object', + properties: { + id: { type: 'number' }, + taskId: { type: 'number' }, + originalName: { type: 'string' }, + contentType: { type: 'string' }, + fileSize: { type: 'number' } + } + }, + 404: { description: 'Image not found' } + } + }, + + deleteTaskImage: { + tags: ['images'], + summary: 'Delete task image', + description: 'Deletes an image. Verifies image belongs to the specified task. Returns 403 if image belongs to different task.', + params: { + type: 'object', + required: ['taskId', 'imageId'], + properties: { + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' }, + imageId: { type: 'string', pattern: '^\\d+$', description: 'Numeric image id.' } + } + }, + response: { + 200: { + description: 'Image deleted', + type: 'object', + properties: { + message: { type: 'string' }, + image: { type: 'object' } + } + }, + 403: { description: 'Image does not belong to the specified task' }, + 404: { description: 'Image not found' } + } + } +}; diff --git a/api/src/schemas/index.js b/api/src/schemas/index.js new file mode 100644 index 00000000..a59df8f0 --- /dev/null +++ b/api/src/schemas/index.js @@ -0,0 +1,24 @@ +/** + * Schema index — re-exports all domain schemas and common utilities. + * Use: import { projectSchemas } from '../schemas/validation.js' (or '../schemas/index.js') + */ + +export { + idParam, + codeParam, + taskIdParam, + taskResponseSchema, + deliverableResponseSchema, + tagNamePattern, + errorResponseSchema, + responseSchemas +} from './common.js'; + +export { tagSchemas } from './tags.js'; +export { taskSchemas } from './tasks.js'; +export { taskGraphSchemas } from './taskGraph.js'; +export { projectSchemas } from './projects.js'; +export { deliverableSchemas } from './deliverables.js'; +export { userSchemas } from './users.js'; +export { coreSchemas } from './core.js'; +export { imageSchemas } from './images.js'; diff --git a/api/src/schemas/projects.js b/api/src/schemas/projects.js new file mode 100644 index 00000000..b87c06ae --- /dev/null +++ b/api/src/schemas/projects.js @@ -0,0 +1,257 @@ +/** + * Project and project-scoped task route schemas. + */ + +import { idParam, codeParam, taskResponseSchema } from './common.js'; + +export const projectSchemas = { + getProjects: { + tags: ['projects'], + summary: 'List projects', + description: 'Returns all projects. Filter by leaderId or search in title/description.', + querystring: { + type: 'object', + properties: { + leaderId: { type: 'string', pattern: '^[0-9]+$', description: 'Filter by project leader user id.' }, + search: { type: 'string', minLength: 1, maxLength: 255, description: 'Search in project title/description.' } + } + } + }, + + getProjectById: { + tags: ['projects'], + summary: 'Get project by ID', + description: 'Returns a single project by numeric id. Use GET /projects for list.', + params: idParam + }, + + createProject: { + tags: ['projects'], + summary: 'Create project', + description: 'Creates a new project. code must be uppercase letters only (e.g. ZAZZ). leaderId is the user id of the project owner.', + body: { + type: 'object', + properties: { + title: { type: 'string', minLength: 1, maxLength: 255, description: 'Project name.' }, + code: { + type: 'string', + minLength: 2, + maxLength: 10, + pattern: '^[A-Z]+$', + description: 'Project code (e.g. ZAZZ). Uppercase letters only. Immutable after create.' + }, + description: { type: 'string', maxLength: 5000, description: 'Optional project description.' }, + leaderId: { type: 'integer', minimum: 1, description: 'User id of project owner.' } + }, + required: ['title', 'code', 'leaderId'], + additionalProperties: false + } + }, + + updateProject: { + tags: ['projects'], + summary: 'Update project', + description: 'Updates project title, description, leaderId, completionCriteriaStatus, or taskGraphLayoutDirection. Project code is immutable.', + params: idParam, + body: { + type: 'object', + properties: { + title: { type: 'string', minLength: 1, maxLength: 255 }, + description: { type: 'string', maxLength: 5000 }, + leaderId: { type: 'integer', minimum: 1, description: 'User id of new project owner.' }, + completionCriteriaStatus: { type: 'string', maxLength: 25, nullable: true, description: 'Status that counts as "done" for dependencies (default COMPLETED).' }, + taskGraphLayoutDirection: { type: 'string', enum: ['LR', 'TB'], description: 'Graph layout: LR=left-right, TB=top-bottom.' } + }, + additionalProperties: false + } + }, + + deleteProject: { + tags: ['projects'], + summary: 'Delete project', + description: 'Deletes a project by numeric id. Cascades to deliverables and tasks.', + params: idParam + }, + + getProjectTasks: { + tags: ['projects'], + summary: 'List project tasks', + description: 'Returns tasks for a project by numeric project id. Filter by status or priority via query params.', + params: idParam, + querystring: { + type: 'object', + properties: { + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Filter by task status.' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'], description: 'Filter by priority.' } + } + } + }, + + updateTaskStatus: { + tags: ['projects'], + summary: 'Update task status', + description: 'Changes task status in the workflow. Use to: pick up a task (TO_DO→IN_PROGRESS), complete it (IN_PROGRESS→COMPLETED), or move to QA. Valid transitions depend on the project\'s task workflow. Use project code (e.g. ZAZZ) and numeric taskId.', + params: { + type: 'object', + required: ['code', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } + } + }, + body: { + type: 'object', + required: ['status'], + properties: { + status: { type: 'string', pattern: '^[A-Z_]+$' } + } + }, + response: { + 200: { description: 'Task status updated', ...taskResponseSchema } + } + }, + + updateTask: { + tags: ['projects'], + summary: 'Update task', + description: 'Full update of task fields (title, status, prompt, agentName, etc.). Send only fields to change. Use project code (e.g. ZAZZ) and numeric taskId.', + params: { + type: 'object', + required: ['code', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } + } + }, + body: { + type: 'object', + properties: { + title: { type: 'string' }, + status: { type: 'string', pattern: '^[A-Z_]+$' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, + storyPoints: { type: 'number', nullable: true }, + agentName: { type: 'string', maxLength: 50, nullable: true }, + prompt: { type: 'string', nullable: true }, + isBlocked: { type: 'boolean' }, + blockedReason: { type: 'string', nullable: true }, + gitWorktree: { type: 'string', nullable: true }, + deliverableId: { type: 'number', nullable: false }, + tagNames: { type: 'array', items: { type: 'string' } } + } + }, + response: { + 200: { description: 'Task updated', ...taskResponseSchema } + } + }, + + deleteTask: { + tags: ['projects'], + summary: 'Delete task (project-scoped)', + description: 'Deletes a task. Use project code (e.g. ZAZZ) and numeric taskId. Verifies task belongs to project.', + params: { + type: 'object', + required: ['code', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$' }, + taskId: { type: 'string', pattern: '^\\d+$' } + } + } + }, + + createDeliverableTask: { + tags: ['projects'], + summary: 'Create task in deliverable', + description: 'Creates a task within a deliverable. delivId is the numeric id from the create deliverable response (not deliverableId). The deliverable must be approved before creating tasks. Include prompt with goal, instructions, and acceptance criteria. Use phase and phaseTaskId to align with PLAN structure (e.g. phase 1, phaseTaskId "1.2").', + params: { + type: 'object', + required: ['code', 'delivId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id from create deliverable response. Use the id field, not deliverableId.' } + } + }, + body: { + type: 'object', + required: ['title'], + properties: { + title: { type: 'string', minLength: 1, maxLength: 255, description: 'Task title.' }, + description: { type: 'string', maxLength: 5000 }, + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Initial status (default TO_DO).' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, + agentName: { type: 'string', maxLength: 50, description: 'Agent name if pre-assigning.' }, + storyPoints: { type: 'integer', minimum: 1, maximum: 21 }, + position: { type: 'integer', minimum: 0 }, + phaseTaskId: { + type: 'string', + maxLength: 20, + description: 'Phase task ID from PLAN (e.g. "1.2"). Auto-generated from phase if omitted. Use "1.2.1" for rework tasks.' + }, + prompt: { type: 'string', maxLength: 10000, description: 'Goal, instructions, and acceptance criteria for the agent.' }, + gitWorktree: { type: 'string', maxLength: 255 }, + phase: { type: 'integer', minimum: 1, description: 'Phase number from PLAN (e.g. 1, 2, 3).' }, + dependencies: { + type: 'array', + items: { type: 'integer', minimum: 1 }, + uniqueItems: true, + description: 'Task ids this task depends on.' + } + }, + additionalProperties: false + }, + response: { + 201: { description: 'Task created', ...taskResponseSchema } + } + }, + + getDeliverableTask: { + tags: ['projects'], + summary: 'Get task by ID', + description: 'Returns a single task. code = project code (e.g. ZAZZ), delivId = numeric deliverable id, taskId = numeric task id.', + params: { + type: 'object', + required: ['code', 'delivId', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } + } + }, + response: { + 200: { description: 'Task', ...taskResponseSchema } + } + }, + + updateDeliverableTask: { + tags: ['projects'], + summary: 'Update task in deliverable', + description: 'Full update of task fields (title, status, prompt, agentName, etc.). Send only fields to change. code, delivId, taskId are all numeric/string ids.', + params: { + type: 'object', + required: ['code', 'delivId', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } + } + }, + body: { + type: 'object', + properties: { + title: { type: 'string', minLength: 1, maxLength: 255 }, + description: { type: 'string', maxLength: 5000 }, + status: { type: 'string', pattern: '^[A-Z_]+$' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, + agentName: { type: 'string', maxLength: 50 }, + storyPoints: { type: 'integer', minimum: 1, maximum: 21 }, + prompt: { type: 'string', maxLength: 10000 }, + isBlocked: { type: 'boolean' }, + blockedReason: { type: 'string', maxLength: 1000 }, + gitWorktree: { type: 'string', maxLength: 255 } + }, + additionalProperties: false + }, + response: { + 200: { description: 'Task updated', ...taskResponseSchema } + } + } +}; diff --git a/api/src/schemas/tags.js b/api/src/schemas/tags.js new file mode 100644 index 00000000..8d73a732 --- /dev/null +++ b/api/src/schemas/tags.js @@ -0,0 +1,84 @@ +/** + * Tag route schemas. + */ + +import { idParam } from './common.js'; +import { tagNamePattern } from './common.js'; + +export const tagSchemas = { + getTags: { + tags: ['tags'], + summary: 'List tags', + description: 'Returns all tags. Optional search query to filter by name.', + querystring: { + type: 'object', + properties: { + search: { type: 'string', minLength: 1, maxLength: 100, description: 'Filter tags by name (partial match).' } + } + } + }, + + getTagById: { + tags: ['tags'], + summary: 'Get tag by ID', + description: 'Returns a single tag by numeric id.', + params: idParam + }, + + createTag: { + tags: ['tags'], + summary: 'Create tag', + description: 'Creates a new tag. Name must be lowercase with hyphens (e.g. frontend, bug-fix).', + body: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: tagNamePattern, + description: 'Tag name must be lowercase with hyphens as separators. Cannot start or end with hyphen.' + }, + color: { + type: 'string', + pattern: '^#[0-9A-Fa-f]{6}$', + description: 'Color must be a valid hex color (e.g., #FF5733)' + } + }, + required: ['name'], + additionalProperties: false + } + }, + + updateTag: { + tags: ['tags'], + summary: 'Update tag', + description: 'Updates tag name and/or color. id is numeric.', + params: idParam, + body: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: tagNamePattern, + description: 'Tag name must be lowercase with hyphens as separators. Cannot start or end with hyphen.' + }, + color: { + type: 'string', + pattern: '^#[0-9A-Fa-f]{6}$', + description: 'Color must be a valid hex color (e.g., #FF5733)' + } + }, + additionalProperties: false + } + }, + + deleteTag: { + tags: ['tags'], + summary: 'Delete tag', + description: 'Deletes a tag by numeric id. Does not remove tag from tasks.', + params: idParam + } +}; diff --git a/api/src/schemas/taskGraph.js b/api/src/schemas/taskGraph.js new file mode 100644 index 00000000..422eff2b --- /dev/null +++ b/api/src/schemas/taskGraph.js @@ -0,0 +1,177 @@ +/** + * Task graph and relation route schemas. + */ + +import { codeParam } from './common.js'; + +const graphTaskItem = { + type: 'object', + properties: { + id: { type: 'integer', description: 'Integer primary key' }, + taskId: { type: 'integer', description: 'Same as id' }, + phase: { type: 'integer', description: 'Phase number (e.g. 1, 2, 3)' }, + phaseTaskId: { type: 'string', description: 'Human-readable ID within a deliverable, e.g. "1.2". Rework tasks use "1.2.1" format.' }, + title: { type: 'string' }, + status: { type: 'string' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, + deliverableId: { type: 'integer' }, + agentName: { type: 'string', description: 'Agent that claimed this task' }, + prompt: { type: 'string', description: 'Task instructions written by the project leader' }, + notes: { type: 'string', description: 'Append-only agent progress log: "[ISO timestamp] [agent]: message"' }, + isBlocked: { type: 'boolean' }, + isCancelled: { type: 'boolean' }, + coordinationCode: { type: 'string' } + } +}; + +const graphRelationItem = { + type: 'object', + properties: { + taskId: { type: 'integer' }, + relatedTaskId: { type: 'integer' }, + relationType: { type: 'string', enum: ['DEPENDS_ON', 'COORDINATES_WITH'] } + } +}; + +export const taskGraphSchemas = { + getProjectGraph: { + tags: ['task-graph'], + summary: 'Get full task graph for a project', + description: 'Returns all tasks and intra-project relations. Polled every 3 s by the UI for live updates.', + params: codeParam, + response: { + 200: { + type: 'object', + properties: { + projectId: { type: 'integer' }, + projectCode: { type: 'string' }, + taskGraphLayoutDirection: { type: 'string', enum: ['LR', 'TB'] }, + completionCriteriaStatus: { type: 'string' }, + tasks: { type: 'array', items: graphTaskItem }, + relations: { type: 'array', items: graphRelationItem } + } + } + } + }, + + getTaskRelations: { + tags: ['task-graph'], + summary: 'Get task relations', + description: 'Returns DEPENDS_ON and COORDINATES_WITH relations for a task. Use to understand task dependencies before claiming.', + params: { + type: 'object', + required: ['code', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } + } + }, + response: { + 200: { type: 'array', items: graphRelationItem } + } + }, + + createTaskRelation: { + tags: ['task-graph'], + summary: 'Create task relation', + description: 'Creates DEPENDS_ON (task must wait for relatedTask) or COORDINATES_WITH (tasks should be done together). Cycle detection for DEPENDS_ON. Returns 400 for cycles/self-refs, 409 for duplicates.', + params: { + type: 'object', + required: ['code', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id (source task).' } + } + }, + body: { + type: 'object', + required: ['relatedTaskId', 'relationType'], + properties: { + relatedTaskId: { type: 'integer', minimum: 1, description: 'Numeric id of the related task.' }, + relationType: { type: 'string', enum: ['DEPENDS_ON', 'COORDINATES_WITH'], description: 'DEPENDS_ON = this task waits for related; COORDINATES_WITH = do together.' } + }, + additionalProperties: false + } + }, + + deleteTaskRelation: { + tags: ['task-graph'], + summary: 'Delete task relation', + description: 'Removes a DEPENDS_ON or COORDINATES_WITH relation between two tasks.', + params: { + type: 'object', + required: ['code', 'taskId', 'relatedTaskId', 'relationType'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric source task id.' }, + relatedTaskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric related task id.' }, + relationType: { type: 'string', enum: ['DEPENDS_ON', 'COORDINATES_WITH'], description: 'Relation type to remove.' } + } + } + }, + + checkTaskReadiness: { + tags: ['task-graph'], + summary: 'Check task readiness', + description: 'Returns ready=true when all DEPENDS_ON prerequisites have reached the project\'s completionCriteriaStatus (default COMPLETED). Agents poll this before claiming a task. blockedBy lists tasks still blocking.', + params: { + type: 'object', + required: ['code', 'taskId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + taskId: { type: 'string', pattern: '^\\d+$', description: 'Numeric task id.' } + } + }, + response: { + 200: { + type: 'object', + properties: { + ready: { type: 'boolean' }, + blockedBy: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'integer' }, + taskId: { type: 'integer' }, + status: { type: 'string' } + } + } + } + } + } + } + }, + + getDeliverableGraph: { + tags: ['task-graph'], + summary: 'Get deliverable task graph', + description: 'Returns tasks and relations for one deliverable. Cross-deliverable relations excluded. Primary endpoint for agents monitoring their work. Poll every 3s for live updates.', + params: { + type: 'object', + required: ['code', 'delivId'], + properties: { + code: { type: 'string', pattern: '^[A-Z0-9]+$', description: 'Project code (e.g. ZAZZ).' }, + delivId: { type: 'string', pattern: '^\\d+$', description: 'Numeric deliverable id.' } + } + }, + response: { + 200: { + type: 'object', + properties: { + deliverableId: { type: 'integer' }, + projectCode: { type: 'string' }, + taskGraphLayoutDirection: { type: 'string', enum: ['LR', 'TB'] }, + tasks: { type: 'array', items: graphTaskItem }, + relations: { type: 'array', items: graphRelationItem } + } + } + } + }, + + getCoordinationTypes: { + tags: ['task-graph'], + summary: 'List coordination types', + description: 'Returns relation types for COORDINATES_WITH (e.g. for UI display).' + } +}; diff --git a/api/src/schemas/tasks.js b/api/src/schemas/tasks.js new file mode 100644 index 00000000..c03af530 --- /dev/null +++ b/api/src/schemas/tasks.js @@ -0,0 +1,131 @@ +/** + * Task route schemas (legacy /tasks; prefer deliverable-scoped routes). + */ + +import { idParam } from './common.js'; +import { tagNamePattern } from './common.js'; + +export const taskSchemas = { + getTasks: { + tags: ['projects'], + summary: 'List tasks (filtered)', + description: 'Returns tasks filtered by projectId, status, priority, agentName, or search. Use project-scoped GET /projects/:id/tasks or deliverable-scoped graph for agent workflows.', + querystring: { + type: 'object', + properties: { + projectId: { type: 'string', pattern: '^[0-9]+$', description: 'Filter by project.' }, + status: { type: 'string', pattern: '^[A-Z_]+$', description: 'Filter by status (e.g. TO_DO, IN_PROGRESS).' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, + agentName: { type: 'string', maxLength: 50, description: 'Filter by assigned agent.' }, + search: { type: 'string', minLength: 1, maxLength: 255, description: 'Search in title/description.' } + } + } + }, + + getTaskById: { + tags: ['projects'], + summary: 'Get task by ID', + description: 'Returns a single task by numeric id.', + params: idParam + }, + + createTask: { + tags: ['projects'], + summary: 'Create task (legacy)', + description: 'Creates a task with projectId and deliverableId. Prefer POST /projects/:code/deliverables/:delivId/tasks for deliverable-scoped tasks.', + body: { + type: 'object', + properties: { + title: { type: 'string', minLength: 1, maxLength: 255 }, + description: { type: 'string', maxLength: 5000 }, + projectId: { type: 'integer', minimum: 1 }, + deliverableId: { type: 'integer', minimum: 1 }, + status: { type: 'string', pattern: '^[A-Z_]+$', default: 'READY' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'], default: 'MEDIUM' }, + dueDate: { type: 'string', format: 'date-time', nullable: true }, + agentName: { type: 'string', maxLength: 50, nullable: true }, + position: { type: 'integer', minimum: 0 }, + git_worktree: { type: 'string', maxLength: 255 }, + }, + required: ['title', 'deliverableId'], + additionalProperties: false + } + }, + + updateTask: { + tags: ['projects'], + summary: 'Update task (legacy)', + description: 'Full update of task by numeric id. Use PUT /projects/:code/deliverables/:delivId/tasks/:taskId for deliverable-scoped updates.', + params: idParam, + body: { + type: 'object', + properties: { + title: { type: 'string', minLength: 1, maxLength: 255 }, + description: { type: 'string', maxLength: 5000 }, + status: { type: 'string', pattern: '^[A-Z_]+$' }, + priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, + storyPoints: { type: 'integer', minimum: 1, maximum: 21, nullable: true }, + agentName: { type: 'string', maxLength: 50, nullable: true }, + deliverableId: { type: 'integer', minimum: 1, nullable: true }, + prompt: { type: 'string', maxLength: 10000 }, + isBlocked: { type: 'boolean' }, + blockedReason: { type: 'string', maxLength: 1000 }, + gitWorktree: { type: 'string', maxLength: 255 }, + position: { type: 'integer', minimum: 0 }, + tagNames: { + type: 'array', + items: { type: 'string', pattern: tagNamePattern }, + uniqueItems: true + } + }, + additionalProperties: false + } + }, + + deleteTask: { + tags: ['projects'], + summary: 'Delete task (legacy)', + description: 'Deletes a task by numeric id.', + params: idParam + }, + + reorderTask: { + tags: ['projects'], + summary: 'Reorder task', + description: 'Changes task position within its column. Body: { position: number }.', + params: idParam, + body: { + type: 'object', + properties: { + position: { type: 'integer', minimum: 0 } + }, + required: ['position'], + additionalProperties: false + } + }, + + setTaskTags: { + tags: ['projects'], + summary: 'Set task tags', + description: 'Replaces all tags on a task. Body: { tagIds: number[] }. Use tag ids from GET /tags.', + params: { + type: 'object', + properties: { + taskId: { type: 'string', pattern: '^[0-9]+$' } + }, + required: ['taskId'] + }, + body: { + type: 'object', + properties: { + tagIds: { + type: 'array', + items: { type: 'integer', minimum: 1 }, + uniqueItems: true + } + }, + required: ['tagIds'], + additionalProperties: false + } + } +}; diff --git a/api/src/schemas/users.js b/api/src/schemas/users.js new file mode 100644 index 00000000..15a28c07 --- /dev/null +++ b/api/src/schemas/users.js @@ -0,0 +1,86 @@ +/** + * User route schemas. + */ + +import { idParam } from './common.js'; + +export const userSchemas = { + getCurrentUser: { + tags: ['users'], + summary: 'Get authenticated user', + description: 'Returns the current user based on TB_TOKEN or Bearer token.', + response: { + 200: { + description: 'Current user', + type: 'object', + properties: { + id: { type: 'number' }, + fullName: { type: 'string' }, + email: { type: 'string', format: 'email' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' } + } + } + } + }, + + getUsers: { + tags: ['users'], + summary: 'List users', + description: 'Returns all users. Optional search to filter by name/email.', + querystring: { + type: 'object', + properties: { + search: { type: 'string', minLength: 1, maxLength: 255, description: 'Filter users by name or email.' } + } + } + }, + + getUserById: { + tags: ['users'], + summary: 'Get user by ID', + description: 'Returns a single user by numeric id.', + params: idParam + }, + + createUser: { + tags: ['users'], + summary: 'Create user', + description: 'Creates a new user. Required: username, email, firstName, lastName.', + body: { + type: 'object', + properties: { + username: { type: 'string', minLength: 3, maxLength: 50 }, + email: { type: 'string', format: 'email', maxLength: 255 }, + firstName: { type: 'string', minLength: 1, maxLength: 100 }, + lastName: { type: 'string', minLength: 1, maxLength: 100 } + }, + required: ['username', 'email', 'firstName', 'lastName'], + additionalProperties: false + } + }, + + updateUser: { + tags: ['users'], + summary: 'Update user', + description: 'Updates user fields. Send only fields to change.', + params: idParam, + body: { + type: 'object', + properties: { + username: { type: 'string', minLength: 3, maxLength: 50 }, + email: { type: 'string', format: 'email', maxLength: 255 }, + firstName: { type: 'string', minLength: 1, maxLength: 100 }, + lastName: { type: 'string', minLength: 1, maxLength: 100 } + }, + additionalProperties: false + } + }, + + deleteUser: { + tags: ['users'], + summary: 'Delete user', + description: 'Deletes a user by numeric id.', + params: idParam + } +}; diff --git a/api/src/schemas/validation.js b/api/src/schemas/validation.js index 858ffc7f..adc8e745 100644 --- a/api/src/schemas/validation.js +++ b/api/src/schemas/validation.js @@ -1,1180 +1,23 @@ /** - * JSON Schema validation definitions for Zazz Board API - * Uses Fastify's built-in AJV for maximum performance + * Validation schemas — barrel re-export for backward compatibility. + * All schemas are split into domain files under ./schemas/. + * @see ./index.js */ - -// Common parameter schemas -const idParam = { - type: 'object', - required: ['id'], - properties: { - id: { type: 'string', pattern: '^\\d+$' } - } -}; - -const codeParam = { - type: 'object', - required: ['code'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' } // Project code like PROJ, FEATURE - } -}; - -const taskIdParam = { - type: 'object', - required: ['taskId'], - properties: { - taskId: { type: 'string', pattern: '^\\d+$' } - } -}; - -// Reusable response shapes for OpenAPI (defined early, used in projectSchemas and deliverableSchemas) -const taskResponseSchema = { - type: 'object', - properties: { - id: { type: 'number', description: 'Task ID' }, - projectId: { type: 'number' }, - deliverableId: { type: 'number' }, - title: { type: 'string' }, - status: { type: 'string', enum: ['TO_DO', 'READY', 'IN_PROGRESS', 'QA', 'COMPLETED'] }, - position: { type: 'number' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, - storyPoints: { type: 'number', nullable: true }, - assigneeId: { type: 'number', nullable: true }, - prompt: { type: 'string', nullable: true }, - isBlocked: { type: 'boolean', nullable: true }, - blockedReason: { type: 'string', nullable: true }, - gitWorktree: { type: 'string', nullable: true }, - createdAt: { type: 'string', format: 'date-time' }, - updatedAt: { type: 'string', format: 'date-time' } - } -}; - -const deliverableResponseSchema = { - type: 'object', - properties: { - id: { type: 'number' }, - projectId: { type: 'number' }, - deliverableId: { type: 'string', description: 'Unique deliverable ID e.g. PROJ-1' }, - name: { type: 'string' }, - description: { type: 'string', nullable: true }, - type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'] }, - status: { type: 'string' }, - dedFilePath: { type: 'string', nullable: true, description: 'Path or URL to deliverable specification (SPEC)' }, - planFilePath: { type: 'string', nullable: true, description: 'Path or URL to implementation plan' }, - prdFilePath: { type: 'string', nullable: true, description: 'Path or URL to PRD document' }, - gitWorktree: { type: 'string', nullable: true }, - gitBranch: { type: 'string', nullable: true }, - pullRequestUrl: { type: 'string', nullable: true }, - approvedBy: { type: 'number', nullable: true }, - approvedAt: { type: 'string', format: 'date-time', nullable: true }, - createdAt: { type: 'string', format: 'date-time' }, - updatedAt: { type: 'string', format: 'date-time' } - } -}; - -// Tag name validation - enforces lowercase, hyphen rules at API level -const tagNamePattern = '^[a-z0-9]+(-[a-z0-9]+)*$'; - -// Tag schemas -export const tagSchemas = { - // GET /tags - getTags: { - querystring: { - type: 'object', - properties: { - search: { type: 'string', minLength: 1, maxLength: 100 } - } - } - }, - - // GET /tags/:id - getTagById: { - params: idParam - }, - - // POST /tags - createTag: { - body: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 100, - pattern: tagNamePattern, - description: 'Tag name must be lowercase with hyphens as separators. Cannot start or end with hyphen.' - }, - color: { - type: 'string', - pattern: '^#[0-9A-Fa-f]{6}$', - description: 'Color must be a valid hex color (e.g., #FF5733)' - } - }, - required: ['name'], - additionalProperties: false - } - }, - - // PUT /tags/:id - updateTag: { - params: idParam, - body: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 100, - pattern: tagNamePattern, - description: 'Tag name must be lowercase with hyphens as separators. Cannot start or end with hyphen.' - }, - color: { - type: 'string', - pattern: '^#[0-9A-Fa-f]{6}$', - description: 'Color must be a valid hex color (e.g., #FF5733)' - } - }, - additionalProperties: false - } - }, - - // DELETE /tags/:id - deleteTag: { - params: idParam - } -}; - -// Task schemas -export const taskSchemas = { - // GET /tasks - getTasks: { - querystring: { - type: 'object', - properties: { - projectId: { type: 'string', pattern: '^[0-9]+$' }, - status: { type: 'string', pattern: '^[A-Z_]+$' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, - agentName: { type: 'string', maxLength: 50 }, - search: { type: 'string', minLength: 1, maxLength: 255 } - } - } - }, - - // GET /tasks/:id - getTaskById: { - params: idParam - }, - - // POST /tasks - createTask: { - body: { - type: 'object', - properties: { - title: { type: 'string', minLength: 1, maxLength: 255 }, - description: { type: 'string', maxLength: 5000 }, - projectId: { type: 'integer', minimum: 1 }, - deliverableId: { type: 'integer', minimum: 1 }, - status: { type: 'string', pattern: '^[A-Z_]+$', default: 'READY' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'], default: 'MEDIUM' }, - dueDate: { type: 'string', format: 'date-time', nullable: true }, - agentName: { type: 'string', maxLength: 50, nullable: true }, - position: { type: 'integer', minimum: 0 }, - git_worktree: { type: 'string', maxLength: 255 }, - }, - required: ['title', 'deliverableId'], - additionalProperties: false - } - }, - - // PUT /tasks/:id - updateTask: { - params: idParam, - body: { - type: 'object', - properties: { - title: { type: 'string', minLength: 1, maxLength: 255 }, - description: { type: 'string', maxLength: 5000 }, - status: { type: 'string', pattern: '^[A-Z_]+$' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, - storyPoints: { type: 'integer', minimum: 1, maximum: 21, nullable: true }, - agentName: { type: 'string', maxLength: 50, nullable: true }, - deliverableId: { type: 'integer', minimum: 1, nullable: true }, - prompt: { type: 'string', maxLength: 10000 }, - isBlocked: { type: 'boolean' }, - blockedReason: { type: 'string', maxLength: 1000 }, - gitWorktree: { type: 'string', maxLength: 255 }, - position: { type: 'integer', minimum: 0 }, - tagNames: { - type: 'array', - items: { type: 'string', pattern: tagNamePattern }, - uniqueItems: true - } - }, - additionalProperties: false - } - }, - - // DELETE /tasks/:id - deleteTask: { - params: idParam - }, - - // PATCH /tasks/:id/reorder - reorderTask: { - params: idParam, - body: { - type: 'object', - properties: { - position: { type: 'integer', minimum: 0 } - }, - required: ['position'], - additionalProperties: false - } - }, - - // PUT /tasks/:taskId/tags - setTaskTags: { - params: { - type: 'object', - properties: { - taskId: { type: 'string', pattern: '^[0-9]+$' } - }, - required: ['taskId'] - }, - body: { - type: 'object', - properties: { - tagIds: { - type: 'array', - items: { type: 'integer', minimum: 1 }, - uniqueItems: true - } - }, - required: ['tagIds'], - additionalProperties: false - } - } -}; - -// Reusable task item shape for graph response schemas -const graphTaskItem = { - type: 'object', - properties: { - id: { type: 'integer', description: 'Integer primary key' }, - taskId: { type: 'integer', description: 'Same as id' }, - phase: { type: 'integer', description: 'Phase number (e.g. 1, 2, 3)' }, - phaseTaskId: { type: 'string', description: 'Human-readable ID within a deliverable, e.g. "1.2". Rework tasks use "1.2.1" format.' }, - title: { type: 'string' }, - status: { type: 'string' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, - deliverableId: { type: 'integer' }, - agentName: { type: 'string', description: 'Agent that claimed this task' }, - prompt: { type: 'string', description: 'Task instructions written by the project leader' }, - notes: { type: 'string', description: 'Append-only agent progress log: "[ISO timestamp] [agent]: message"' }, - isBlocked: { type: 'boolean' }, - isCancelled: { type: 'boolean' }, - coordinationCode: { type: 'string' } - } -}; - -const graphRelationItem = { - type: 'object', - properties: { - taskId: { type: 'integer' }, - relatedTaskId: { type: 'integer' }, - relationType: { type: 'string', enum: ['DEPENDS_ON', 'COORDINATES_WITH'] } - } -}; - -// Task Graph / Relation schemas -export const taskGraphSchemas = { - // GET /projects/:code/graph - getProjectGraph: { - tags: ['task-graph'], - summary: 'Get full task graph for a project', - description: 'Returns all tasks and intra-project relations. Polled every 3 s by the UI for live updates.', - params: codeParam, - response: { - 200: { - type: 'object', - properties: { - projectId: { type: 'integer' }, - projectCode: { type: 'string' }, - taskGraphLayoutDirection: { type: 'string', enum: ['LR', 'TB'] }, - completionCriteriaStatus: { type: 'string' }, - tasks: { type: 'array', items: graphTaskItem }, - relations: { type: 'array', items: graphRelationItem } - } - } - } - }, - - // GET /projects/:code/tasks/:taskId/relations - getTaskRelations: { - tags: ['task-graph'], - summary: 'Get all relations for a task', - params: { - type: 'object', - required: ['code', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { type: 'array', items: graphRelationItem } - } - }, - - // POST /projects/:code/tasks/:taskId/relations - createTaskRelation: { - tags: ['task-graph'], - summary: 'Create a task relation', - description: 'Create a DEPENDS_ON or COORDINATES_WITH relation. Cycle detection is enforced for DEPENDS_ON. Returns 400 for cycles/self-refs, 409 for duplicates.', - params: { - type: 'object', - required: ['code', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - }, - body: { - type: 'object', - required: ['relatedTaskId', 'relationType'], - properties: { - relatedTaskId: { type: 'integer', minimum: 1 }, - relationType: { type: 'string', enum: ['DEPENDS_ON', 'COORDINATES_WITH'] } - }, - additionalProperties: false - } - }, - - // DELETE /projects/:code/tasks/:taskId/relations/:relatedTaskId/:relationType - deleteTaskRelation: { - tags: ['task-graph'], - summary: 'Delete a task relation', - params: { - type: 'object', - required: ['code', 'taskId', 'relatedTaskId', 'relationType'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - taskId: { type: 'string', pattern: '^\\d+$' }, - relatedTaskId: { type: 'string', pattern: '^\\d+$' }, - relationType: { type: 'string', enum: ['DEPENDS_ON', 'COORDINATES_WITH'] } - } - } - }, - - // GET /projects/:code/tasks/:taskId/readiness - checkTaskReadiness: { - tags: ['task-graph'], - summary: 'Check if a task\'s dependencies are met', - description: 'Returns ready=true when all DEPENDS_ON prerequisites have reached the project\'s completionCriteriaStatus (default DONE). Agents poll this before claiming a task.', - params: { - type: 'object', - required: ['code', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { - type: 'object', - properties: { - ready: { type: 'boolean' }, - blockedBy: { - type: 'array', - items: { - type: 'object', - properties: { - id: { type: 'integer' }, - taskId: { type: 'integer' }, - status: { type: 'string' } - } - } - } - } - } - } - }, - - // GET /projects/:code/deliverables/:delivId/graph - getDeliverableGraph: { - tags: ['task-graph'], - summary: 'Get task graph scoped to a deliverable', - description: 'Returns tasks and relations for one deliverable. Cross-deliverable relations are excluded. Primary endpoint for agents monitoring their own work. Polled every 3 s.', - params: { - type: 'object', - required: ['code', 'delivId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { - type: 'object', - properties: { - deliverableId: { type: 'integer' }, - projectCode: { type: 'string' }, - taskGraphLayoutDirection: { type: 'string', enum: ['LR', 'TB'] }, - tasks: { type: 'array', items: graphTaskItem }, - relations: { type: 'array', items: graphRelationItem } - } - } - } - }, - - // GET /coordination-types - getCoordinationTypes: { - tags: ['task-graph'], - summary: 'List all coordination types' - }, -}; - -// Project schemas -export const projectSchemas = { - // GET /projects - getProjects: { - tags: ['projects'], - summary: 'List projects', - description: 'Returns all projects. Use for project discovery.', - querystring: { - type: 'object', - properties: { - leaderId: { type: 'string', pattern: '^[0-9]+$' }, - search: { type: 'string', minLength: 1, maxLength: 255 } - } - } - }, - - // GET /projects/:id - getProjectById: { - params: idParam - }, - - // POST /projects - createProject: { - body: { - type: 'object', - properties: { - title: { type: 'string', minLength: 1, maxLength: 255 }, - code: { - type: 'string', - minLength: 2, - maxLength: 10, - pattern: '^[A-Z]+$', - description: 'Project code must contain only uppercase letters (A-Z), no spaces, numbers, or special characters' - }, - description: { type: 'string', maxLength: 5000 }, - leaderId: { type: 'integer', minimum: 1 } - }, - required: ['title', 'code', 'leaderId'], - additionalProperties: false - } - }, - - // PUT /projects/:id - Project codes are immutable, only title, description, leaderId, and graph settings can be updated - updateProject: { - params: idParam, - body: { - type: 'object', - properties: { - title: { type: 'string', minLength: 1, maxLength: 255 }, - description: { type: 'string', maxLength: 5000 }, - leaderId: { type: 'integer', minimum: 1 }, - completionCriteriaStatus: { type: 'string', maxLength: 25, nullable: true }, - taskGraphLayoutDirection: { type: 'string', enum: ['LR', 'TB'] } - }, - additionalProperties: false - } - }, - - // DELETE /projects/:id - deleteProject: { - params: idParam - }, - - // GET /projects/:id/tasks - getProjectTasks: { - params: idParam, - querystring: { - type: 'object', - properties: { - status: { type: 'string', pattern: '^[A-Z_]+$' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] } - } - } - }, - - // Project code-based task schemas - // PATCH /projects/:code/tasks/:taskId/status - updateTaskStatus: { - tags: ['projects'], - summary: 'Update task status', - description: 'Pick up task (TO_DO→IN_PROGRESS), complete (IN_PROGRESS→COMPLETED), or move to QA. Use project code (e.g. ZAZZ).', - params: { - type: 'object', - required: ['code', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - }, - body: { - type: 'object', - required: ['status'], - properties: { - status: { - type: 'string', - pattern: '^[A-Z_]+$' - } - } - }, - response: { - 200: { description: 'Task status updated', ...taskResponseSchema } - } - }, - - // PUT /projects/:code/tasks/:taskId - updateTask: { - tags: ['projects'], - summary: 'Update task', - description: 'Update task title, status, assignee, prompt, etc. Use project code (e.g. ZAZZ).', - params: { - type: 'object', - required: ['code', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - }, - body: { - type: 'object', - properties: { - title: { type: 'string' }, - status: { type: 'string', pattern: '^[A-Z_]+$' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, - storyPoints: { type: 'number', nullable: true }, - agentName: { type: 'string', maxLength: 50, nullable: true }, - prompt: { type: 'string', nullable: true }, - isBlocked: { type: 'boolean' }, - blockedReason: { type: 'string', nullable: true }, - gitWorktree: { type: 'string', nullable: true }, - deliverableId: { type: 'number', nullable: false }, - tagNames: { type: 'array', items: { type: 'string' } } - } - }, - response: { - 200: { description: 'Task updated', ...taskResponseSchema } - } - }, - - // DELETE /projects/:code/tasks/:taskId - deleteTask: { - params: { - type: 'object', - required: ['code', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - } - }, - - // Deliverable-scoped task CRUD (POST/GET/PUT/PATCH/DELETE under /projects/:code/deliverables/:delivId/tasks) - createDeliverableTask: { - tags: ['projects'], - summary: 'Create task in deliverable', - description: 'Creates a task within a deliverable. Include prompt with goal, instructions, AC. Use project code (e.g. ZAZZ).', - params: { - type: 'object', - required: ['code', 'delivId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' } - } - }, - body: { - type: 'object', - required: ['title'], - properties: { - title: { type: 'string', minLength: 1, maxLength: 255 }, - description: { type: 'string', maxLength: 5000 }, - status: { type: 'string', pattern: '^[A-Z_]+$' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, - agentName: { type: 'string', maxLength: 50 }, - storyPoints: { type: 'integer', minimum: 1, maximum: 21 }, - position: { type: 'integer', minimum: 0 }, - phaseTaskId: { - type: 'string', - maxLength: 20, - description: 'Explicit phase task ID (e.g. "1.2"). Auto-generated from phase if omitted. Use "1.2.1" format for rework tasks.' - }, - prompt: { type: 'string', maxLength: 10000 }, - gitWorktree: { type: 'string', maxLength: 255 }, - phase: { type: 'integer', minimum: 1 }, - dependencies: { - type: 'array', - items: { type: 'integer', minimum: 1 }, - uniqueItems: true - } - }, - additionalProperties: false - }, - response: { - 201: { description: 'Task created', ...taskResponseSchema } - } - }, - getDeliverableTask: { - tags: ['projects'], - summary: 'Get task by ID', - description: 'Returns a single task. Use project code and deliverable id.', - params: { - type: 'object', - required: ['code', 'delivId', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { description: 'Task', ...taskResponseSchema } - } - }, - updateDeliverableTask: { - tags: ['projects'], - summary: 'Update task in deliverable', - description: 'Full update of task. Use project code and deliverable id.', - params: { - type: 'object', - required: ['code', 'delivId', 'taskId'], - properties: { - code: { type: 'string', pattern: '^[A-Z0-9]+$' }, - delivId: { type: 'string', pattern: '^\\d+$' }, - taskId: { type: 'string', pattern: '^\\d+$' } - } - }, - body: { - type: 'object', - properties: { - title: { type: 'string', minLength: 1, maxLength: 255 }, - description: { type: 'string', maxLength: 5000 }, - status: { type: 'string', pattern: '^[A-Z_]+$' }, - priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] }, - agentName: { type: 'string', maxLength: 50 }, - storyPoints: { type: 'integer', minimum: 1, maximum: 21 }, - prompt: { type: 'string', maxLength: 10000 }, - isBlocked: { type: 'boolean' }, - blockedReason: { type: 'string', maxLength: 1000 }, - gitWorktree: { type: 'string', maxLength: 255 } - }, - additionalProperties: false - }, - response: { - 200: { description: 'Task updated', ...taskResponseSchema } - } - } -}; - -export const deliverableSchemas = { - getProjectDeliverables: { - tags: ['deliverables'], - summary: 'List deliverables for a project', - description: 'Returns deliverables for project. Use projectCode (e.g. ZAZZ). Filter by status or type.', - params: { - type: 'object', - required: ['projectCode'], - properties: { projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' } } - }, - querystring: { - type: 'object', - properties: { - status: { type: 'string', pattern: '^[A-Z_]+$' }, - type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'] } - } - }, - response: { - 200: { - description: 'List of deliverables', - type: 'array', - items: deliverableResponseSchema - } - } - }, - getDeliverableById: { - tags: ['deliverables'], - summary: 'Get deliverable by ID', - description: 'Returns deliverable with path to deliverable specification (dedFilePath), planFilePath, prdFilePath for document retrieval. Use projectCode (e.g. ZAZZ) and numeric deliverable id.', - params: { - type: 'object', - required: ['projectCode', 'id'], - properties: { - projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' }, - id: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { description: 'Deliverable', ...deliverableResponseSchema } - } - }, - createDeliverable: { - tags: ['deliverables'], - summary: 'Create deliverable', - description: 'Creates a deliverable in the project. Include dedFilePath (path to SPEC), planFilePath for document links.', - params: { - type: 'object', - required: ['projectCode'], - properties: { projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' } } - }, - body: { - type: 'object', - required: ['name', 'type'], - properties: { - name: { type: 'string', minLength: 1, maxLength: 30 }, - description: { type: 'string' }, - type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'] }, - dedFilePath: { type: 'string', maxLength: 500 }, - planFilePath: { type: 'string', maxLength: 500 }, - prdFilePath: { type: 'string', maxLength: 500 }, - gitWorktree: { type: 'string', maxLength: 255 }, - gitBranch: { type: 'string', maxLength: 255 }, - pullRequestUrl: { type: 'string', maxLength: 500 } - }, - additionalProperties: false - }, - response: { - 201: { description: 'Deliverable created', ...deliverableResponseSchema } - } - }, - updateDeliverable: { - tags: ['deliverables'], - summary: 'Update deliverable', - description: 'Updates deliverable including dedFilePath (path to SPEC), planFilePath, prdFilePath for document paths.', - params: { - type: 'object', - required: ['projectCode', 'id'], - properties: { - projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' }, - id: { type: 'string', pattern: '^\\d+$' } - } - }, - body: { - type: 'object', - properties: { - name: { type: 'string', minLength: 1, maxLength: 30 }, - description: { type: 'string' }, - type: { type: 'string', enum: ['FEATURE', 'BUG_FIX', 'REFACTOR', 'ENHANCEMENT', 'CHORE', 'DOCUMENTATION'] }, - status: { type: 'string', pattern: '^[A-Z_]+$' }, - dedFilePath: { type: 'string', maxLength: 500 }, - planFilePath: { type: 'string', maxLength: 500 }, - prdFilePath: { type: 'string', maxLength: 500 }, - gitWorktree: { type: 'string', maxLength: 255 }, - gitBranch: { type: 'string', maxLength: 255 }, - pullRequestUrl: { type: 'string', maxLength: 500 }, - position: { type: 'integer', minimum: 0 } - }, - additionalProperties: false - }, - response: { - 200: { description: 'Deliverable updated', ...deliverableResponseSchema } - } - }, - deleteDeliverable: { - tags: ['deliverables'], - summary: 'Delete deliverable', - params: { - type: 'object', - required: ['projectCode', 'id'], - properties: { - projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' }, - id: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { description: 'Deliverable deleted' } - } - }, - updateDeliverableStatus: { - tags: ['deliverables'], - summary: 'Update deliverable status', - description: 'Set deliverable status e.g. IN_REVIEW when PR is ready.', - params: { - type: 'object', - required: ['projectCode', 'id'], - properties: { - projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' }, - id: { type: 'string', pattern: '^\\d+$' } - } - }, - body: { - type: 'object', - required: ['status'], - properties: { status: { type: 'string', pattern: '^[A-Z_]+$' } }, - additionalProperties: false - }, - response: { - 200: { description: 'Deliverable status updated', ...deliverableResponseSchema } - } - }, - approveDeliverable: { - tags: ['deliverables'], - summary: 'Approve deliverable plan', - description: 'Approves the deliverable plan. Required before tasks can be created.', - params: { - type: 'object', - required: ['projectCode', 'id'], - properties: { - projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' }, - id: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { description: 'Deliverable approved', ...deliverableResponseSchema } - } - }, - getDeliverableTasks: { - tags: ['deliverables'], - summary: 'List tasks for deliverable', - description: 'Returns tasks in a deliverable. Use to check task completion status.', - params: { - type: 'object', - required: ['projectCode', 'id'], - properties: { - projectCode: { type: 'string', pattern: '^[A-Z0-9]+$' }, - id: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { - description: 'List of tasks', - type: 'array', - items: taskResponseSchema - } - } - } -}; - -// User schemas -export const userSchemas = { - // GET /users/me - getCurrentUser: { - tags: ['users'], - summary: 'Get authenticated user', - description: 'Returns the current user based on TB_TOKEN or Bearer token.', - response: { - 200: { - description: 'Current user', - type: 'object', - properties: { - id: { type: 'number' }, - fullName: { type: 'string' }, - email: { type: 'string', format: 'email' }, - createdAt: { type: 'string', format: 'date-time' }, - updatedAt: { type: 'string', format: 'date-time' } - } - } - } - }, - // GET /users - getUsers: { - querystring: { - type: 'object', - properties: { - search: { type: 'string', minLength: 1, maxLength: 255 } - } - } - }, - - // GET /users/:id - getUserById: { - params: idParam - }, - - // POST /users - createUser: { - body: { - type: 'object', - properties: { - username: { type: 'string', minLength: 3, maxLength: 50 }, - email: { type: 'string', format: 'email', maxLength: 255 }, - firstName: { type: 'string', minLength: 1, maxLength: 100 }, - lastName: { type: 'string', minLength: 1, maxLength: 100 } - }, - required: ['username', 'email', 'firstName', 'lastName'], - additionalProperties: false - } - }, - - // PUT /users/:id - updateUser: { - params: idParam, - body: { - type: 'object', - properties: { - username: { type: 'string', minLength: 3, maxLength: 50 }, - email: { type: 'string', format: 'email', maxLength: 255 }, - firstName: { type: 'string', minLength: 1, maxLength: 100 }, - lastName: { type: 'string', minLength: 1, maxLength: 100 } - }, - additionalProperties: false - } - }, - - // DELETE /users/:id - deleteUser: { - params: idParam - } -}; - -// Standard error response for OpenAPI (reference in route response schemas) -export const errorResponseSchema = { - type: 'object', - properties: { - statusCode: { type: 'number', example: 400 }, - error: { type: 'string', example: 'Bad Request' }, - message: { type: 'string', example: 'Invalid request parameters' } - } -}; - -// Common response schemas -export const responseSchemas = { - error: { - type: 'object', - properties: { - error: { type: 'string' }, - message: { type: 'string' }, - statusCode: { type: 'integer' } - } - }, - success: { - type: 'object', - properties: { - success: { type: 'boolean' }, - data: { type: 'object' } - } - } -}; - -// Core routes (public, no auth) -export const coreSchemas = { - getHealth: { - tags: ['core'], - summary: 'Health check', - description: 'Returns API health and token cache stats. No authentication required.', - security: [], - response: { - 200: { - description: 'API is healthy', - type: 'object', - properties: { - status: { type: 'string', example: 'ok' }, - timestamp: { type: 'string', format: 'date-time' }, - auth: { - type: 'object', - properties: { - tokenCacheInitialized: { type: 'boolean' }, - userCount: { type: 'number' } - } - } - } - } - } - }, - getRoot: { - tags: ['core'], - summary: 'API info', - description: 'Returns API message and endpoint list. No authentication required.', - security: [], - response: { - 200: { - description: 'API info', - type: 'object', - properties: { - message: { type: 'string' }, - version: { type: 'string' }, - endpoints: { type: 'array', items: { type: 'string' } } - } - } - } - }, - getDbTest: { - tags: ['core'], - summary: 'Database connectivity test', - description: 'Tests database connection. No authentication required.', - security: [], - response: { - 200: { - description: 'Database connected', - type: 'object', - properties: { - status: { type: 'string' }, - result: {} - } - }, - 500: { - description: 'Database connection failed', - type: 'object', - properties: { - error: { type: 'string' }, - details: { type: 'string' } - } - } - } - }, - getTokenInfo: { - tags: ['core'], - summary: 'Token cache debug', - description: 'Returns token cache stats for debugging. No authentication required.', - security: [], - response: { - 200: { - description: 'Token cache info', - type: 'object', - properties: { - cacheInitialized: { type: 'boolean' }, - userCount: { type: 'number' }, - hasTokens: { type: 'boolean' } - } - } - } - } -}; - -// Image routes -export const imageSchemas = { - getTaskImages: { - tags: ['images'], - summary: 'Get all images for a task', - description: 'Returns metadata for images attached to a task.', - params: { - type: 'object', - required: ['taskId'], - properties: { taskId: { type: 'string', pattern: '^\\d+$' } } - }, - response: { - 200: { - description: 'List of image metadata', - type: 'array', - items: { - type: 'object', - properties: { - id: { type: 'number' }, - taskId: { type: 'number' }, - originalName: { type: 'string' }, - contentType: { type: 'string' }, - fileSize: { type: 'number' } - } - } - } - } - }, - uploadTaskImages: { - tags: ['images'], - summary: 'Upload images to a task', - description: 'Upload one or more images as base64. Each image needs originalName, contentType, fileSize, base64Data.', - params: { - type: 'object', - required: ['taskId'], - properties: { taskId: { type: 'string', pattern: '^\\d+$' } } - }, - body: { - type: 'object', - required: ['images'], - properties: { - images: { - type: 'array', - items: { - type: 'object', - required: ['originalName', 'contentType', 'fileSize', 'base64Data'], - properties: { - originalName: { type: 'string' }, - contentType: { type: 'string', pattern: '^image/' }, - fileSize: { type: 'integer', minimum: 1 }, - base64Data: { type: 'string' } - } - } - } - } - }, - response: { - 201: { - description: 'Images uploaded', - type: 'object', - properties: { - success: { type: 'boolean' }, - images: { type: 'array', items: { type: 'object' } }, - count: { type: 'number' } - } - } - } - }, - getImageById: { - tags: ['images'], - summary: 'Serve individual image (binary)', - description: 'Returns image binary data. Use Content-Type header for format.', - params: { - type: 'object', - required: ['id'], - properties: { id: { type: 'string', pattern: '^\\d+$' } } - }, - response: { - 200: { description: 'Image binary' }, - 404: { description: 'Image not found' } - } - }, - getImageMetadata: { - tags: ['images'], - summary: 'Get image metadata only', - description: 'Returns image metadata without binary data.', - params: { - type: 'object', - required: ['id'], - properties: { id: { type: 'string', pattern: '^\\d+$' } } - }, - response: { - 200: { - description: 'Image metadata', - type: 'object', - properties: { - id: { type: 'number' }, - taskId: { type: 'number' }, - originalName: { type: 'string' }, - contentType: { type: 'string' }, - fileSize: { type: 'number' } - } - }, - 404: { description: 'Image not found' } - } - }, - deleteTaskImage: { - tags: ['images'], - summary: 'Delete image from task', - description: 'Deletes an image. Verifies image belongs to the specified task.', - params: { - type: 'object', - required: ['taskId', 'imageId'], - properties: { - taskId: { type: 'string', pattern: '^\\d+$' }, - imageId: { type: 'string', pattern: '^\\d+$' } - } - }, - response: { - 200: { - description: 'Image deleted', - type: 'object', - properties: { - message: { type: 'string' }, - image: { type: 'object' } - } - }, - 403: { description: 'Image does not belong to the specified task' }, - 404: { description: 'Image not found' } - } - } -}; - +export { + idParam, + codeParam, + taskIdParam, + taskResponseSchema, + deliverableResponseSchema, + tagNamePattern, + errorResponseSchema, + responseSchemas, + tagSchemas, + taskSchemas, + taskGraphSchemas, + projectSchemas, + deliverableSchemas, + userSchemas, + coreSchemas, + imageSchemas +} from './index.js'; diff --git a/api/src/server.js b/api/src/server.js index 986c5b19..0b239154 100644 --- a/api/src/server.js +++ b/api/src/server.js @@ -44,7 +44,14 @@ const start = async () => { openapi: '3.1.0', info: { title: 'Zazz Board API', - description: 'Kanban-style orchestration API for coordinating AI agents and humans. All routes require `TB_TOKEN` header (or Authorization: Bearer) except /health, /, /db-test, /token-info, /openapi.json.', + description: `Kanban-style orchestration API for coordinating AI agents and humans. Auth: TB_TOKEN header or Authorization: Bearer (except /health, /, /db-test, /token-info, /openapi.json). + +**Common operations (agent quick reference)**: +- Create deliverable: POST /projects/{projectCode}/deliverables — body: name, type; optional: dedFilePath, planFilePath. Response id = use for create task. +- Create task: POST /projects/{code}/deliverables/{delivId}/tasks — delivId = numeric id from create deliverable. Body: title, prompt, phase, phaseTaskId. +- Update deliverable: PUT /projects/{projectCode}/deliverables/{id} — add dedFilePath (spec path), planFilePath (plan path), gitWorktree, gitBranch when known. +- Change deliverable status: PATCH /projects/{projectCode}/deliverables/{id}/status — body: { status }. +- Change task status: PATCH /projects/{code}/deliverables/{delivId}/tasks/{taskId}/status — body: { status }; optional agentName to claim.`, version: '1.0.0' }, servers: [{ url: API_BASE_URL, description: API_BASE_URL === BASE_URL ? 'Local' : 'API server' }], diff --git a/package-lock.json b/package-lock.json index c04bb066..b34ebc9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@types/node": "^22.10.7", "@vitest/coverage-v8": "^4.0.15", "drizzle-kit": "^0.31.8", + "openapi-schema-validator": "^12.1.3", "pactum": "^3.8.0", "vitest": "^4.0.15" } @@ -3176,6 +3177,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", @@ -3334,6 +3342,37 @@ "klona": "^2.0.4" } }, + "node_modules/openapi-schema-validator": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-12.1.3.tgz", + "integrity": "sha512-xTHOmxU/VQGUgo7Cm0jhwbklOKobXby+/237EG967+3TQEYJztMgX9Q5UE2taZKwyKPUq0j11dngpGjUuxz1hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", + "lodash.merge": "^4.6.1", + "openapi-types": "^12.1.3" + } + }, + "node_modules/openapi-schema-validator/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", diff --git a/package.json b/package.json index 1a18479a..20a19c51 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "docker:prod:up": "docker-compose -f docker-compose.prod.yml up -d", "docker:prod:down": "docker-compose -f docker-compose.prod.yml down", "docker:prod:build": "docker-compose -f docker-compose.prod.yml build", + "docker:deploy": "bash scripts/deploy-docker.sh", "db:migrate": "npm run db:migrate --workspace=api", "db:seed": "npm run db:seed --workspace=api", "db:reset": "npm run db:reset --workspace=api", diff --git a/scripts/deploy-docker.sh b/scripts/deploy-docker.sh new file mode 100755 index 00000000..71180778 --- /dev/null +++ b/scripts/deploy-docker.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Deploy this worktree to Docker Desktop. +# Stops existing zazz-board containers, rebuilds from this repo, starts the stack. +# DB data persists in zazz_board_postgres_data volume. + +set -e +cd "$(dirname "$0")/.." + +echo "Stopping and removing existing zazz-board containers..." +docker compose down 2>/dev/null || docker-compose down 2>/dev/null || true +# Remove by name in case they were started from a different compose project +for c in zazz_board_postgres zazz_board_api zazz_board_client; do + docker rm -f "$c" 2>/dev/null || true +done + +echo "Building images (API + client)..." +docker compose build --no-cache api client + +echo "Starting stack..." +docker compose up -d + +echo "" +echo "Deployed. API: http://localhost:3030 Client: http://localhost:3001" +echo "Logs: npm run docker:logs" From 35093b5b04544ee426fa10bc9576f18464c49873 Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Wed, 4 Mar 2026 19:31:37 -0500 Subject: [PATCH 07/10] Spec naming, ZAZZ-5/6 deliverables, spec-builder skill updates - spec-builder-agent: SPEC naming {deliverableCode}-{slug}-SPEC.md, first 5 words, hyphen-delimited, index.yaml update, do not invent data - ZAZZ-5: fix-routes-no-project SPEC (image routes project filtering) - ZAZZ-6: multiple-agent-tokens-feature deliverable, rename spec to ZAZZ-6 prefix - index.yaml: add ZAZZ-5, ZAZZ-6 - future-fixes: minor updates Made-with: Cursor --- .agents/skills/spec-builder-agent/SKILL.md | 35 +++-- .../ZAZZ-5-fix-routes-no-project-SPEC.md | 146 ++++++++++++++++++ ...Z-6-multiple-agent-tokens-feature-SPEC.md} | 0 .zazz/deliverables/index.yaml | 6 + .zazz/future-fixes.md | 6 +- 5 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 .zazz/deliverables/ZAZZ-5-fix-routes-no-project-SPEC.md rename .zazz/deliverables/{multiple-agent-tokens-feature-SPEC.md => ZAZZ-6-multiple-agent-tokens-feature-SPEC.md} (100%) diff --git a/.agents/skills/spec-builder-agent/SKILL.md b/.agents/skills/spec-builder-agent/SKILL.md index b24de94a..310a811c 100644 --- a/.agents/skills/spec-builder-agent/SKILL.md +++ b/.agents/skills/spec-builder-agent/SKILL.md @@ -46,7 +46,7 @@ You do **not** implement. You ask, clarify, document, and iterate until the Owne - **You are having a conversation.** Ask one or a few questions at a time; don't overwhelm. Follow up on answers. - **Be friendly and human.** Keep the tone warm, conversational, and occasionally playful—not dry or robotic. You're a helpful colleague, not a form-filling bot. See "Tone & Personality" below. - **Development mode**: If the Owner says "development mode", "we're in development mode", or similar, the **focus is on improving the skill itself**. Write the SPEC file only (no API calls). **Only in development mode** may the agent edit `.agents/skills/spec-builder-agent/SKILL.md` and `.agents/skills/spec-builder-agent/README.md` to iterate on how the skill works. **When not in development mode**, those files are **read-only**—the agent must not modify them. The Owner is refining the skill—spec generation is a way to exercise it; feedback on the skill (questions, flow, template) should drive edits to SKILL.md. -- **Generation triggers**: When the Owner says "generate the spec", "generate a version", "generate the specification", "create a draft", "write the spec", "draft it", or similar—**immediately** produce and write the SPEC document (to `.zazz/deliverables/{name}-SPEC.md`) so they can review it. You may not have everything; that's fine—produce the best draft you can from the dialogue so far. **Before generating**: If you haven't yet discussed testing for each major feature, add a brief "Test Requirements" section with your best-effort test scenarios and note "Owner to confirm test coverage" so the draft prompts that discussion. The Owner can then give feedback and you iterate. +- **Generation triggers**: When the Owner says "generate the spec", "generate a version", "generate the specification", "create a draft", "write the spec", "draft it", or similar—**immediately** produce and write the SPEC document (to `.zazz/deliverables/{deliverableCode}-{slug}-SPEC.md` per the naming rules below) so they can review it. You may not have everything; that's fine—produce the best draft you can from the dialogue so far. **Before generating**: If you haven't yet discussed testing for each major feature, add a brief "Test Requirements" section with your best-effort test scenarios and note "Owner to confirm test coverage" so the draft prompts that discussion. The Owner can then give feedback and you iterate. - **Draw out, don't assume.** If the Owner says "it should be fast," ask: "What does fast mean? Response time? Throughput? Under what load?" - **Never skip the testing discussion.** For every feature or requirement, ask how it will be tested. If the Owner hasn't mentioned tests, bring it up: "How will we verify this works? What test would pass when it's done?" Reference `.zazz/standards/testing.md` for project-specific patterns (e.g., PactumJS for API routes). - **Reference standards proactively.** Read `.zazz/standards/index.yaml` and the listed files. Discuss with the Owner which apply and how. @@ -354,21 +354,28 @@ This phase is **mandatory**. Do not generate a spec without explicit AC and test **Directory**: `.zazz/deliverables/` — All deliverable specs live here. -**Naming**: `{deliverable-name}-SPEC.md` — Use kebab-case for the deliverable name (e.g. `user-auth`, `multiple-agent-tokens-feature`). Suffix `-SPEC.md` is required. +**Naming**: `{deliverableCode}-{slug}-SPEC.md` -**Examples**: -- `user-auth-SPEC.md` -- `multiple-agent-tokens-feature-SPEC.md` -- `deliverables-feature-SPEC.md` +- **Prefix**: Deliverable code (e.g. `ZAZZ-5`) — makes the file unique per deliverable. +- **Slug**: First 5 words from the deliverable name, hyphen-delimited. Example: "Audit routes for project filter" → `audit-routes-for-project-filter`. +- **Suffix**: `-SPEC.md` required. +- **Hyphen-delimited only** — No spaces. Git worktrees cannot have spaces in paths; enforce hyphen-delimited naming for all deliverable and plan documents (SPEC, PLAN, etc.). -**Path from repo root**: `.zazz/deliverables/{deliverable-name}-SPEC.md` -- Relative path for API sync: `.zazz/deliverables/user-auth-SPEC.md` +**Example**: For deliverable ZAZZ-5 named "Audit routes for project filter" → `ZAZZ-5-audit-routes-for-project-filter-SPEC.md`. The deliverable code and `-SPEC` suffix make the doc unique. + +**Deliverable code**: Get from the deliverable card (deliverableId, e.g. ZAZZ-5) or from the Owner. Required to construct the filename. + +**After writing the SPEC**: +1. Write to `.zazz/deliverables/{filename}.md` +2. Update `.zazz/deliverables/index.yaml` — add an entry under `deliverables:` with `id`, `name`, `spec` (filename only), and optionally `plan` when it exists. + +**Path for API sync** (dedFilePath): `.zazz/deliverables/ZAZZ-5-audit-routes-for-project-filter-SPEC.md` --- ## SPEC Document Template -Create `.zazz/deliverables/{deliverable-name}-SPEC.md` with this structure: +Create `.zazz/deliverables/{deliverableCode}-{slug}-SPEC.md` with this structure: ```markdown # {Deliverable Name} Specification @@ -475,8 +482,8 @@ When not in development mode: When the SPEC is created or updated, sync the deli **API calls** (requires zazz-board-api skill, `ZAZZ_API_BASE_URL`, `ZAZZ_API_TOKEN`): 1. **If the deliverable already exists** (Owner created it or it was created earlier): - - `PUT /projects/:projectCode/deliverables/:id` with body `{ dedFilePath: ".zazz/deliverables/{deliverable-name}-SPEC.md" }` - - Use the relative path from the repo root (worktree root). Example: `.zazz/deliverables/user-auth-SPEC.md` + - `PUT /projects/:projectCode/deliverables/:id` with body `{ dedFilePath: ".zazz/deliverables/{deliverableCode}-{slug}-SPEC.md" }` + - Use the relative path from the repo root (worktree root). Example: `.zazz/deliverables/ZAZZ-5-audit-routes-for-project-filter-SPEC.md` 2. **If creating a new deliverable** (Owner wants it on the board): - `POST /projects/:projectCode/deliverables` with `name`, `type`, `description`, and `dedFilePath` in the body @@ -497,7 +504,7 @@ When not in development mode: When the SPEC is created or updated, sync the deli - [ ] Document agent constraints, preferences, escalation rules - [ ] Guide decomposition for complex deliverables; document break patterns - [ ] Define evaluation criteria -- [ ] Create `.zazz/deliverables/{deliverable-name}-SPEC.md` +- [ ] Create `.zazz/deliverables/{deliverableCode}-{slug}-SPEC.md` and update `index.yaml` - [ ] Sync `dedFilePath` to Zazz Board via API (unless in development mode) - [ ] Iterate based on feedback until Owner approves @@ -526,11 +533,11 @@ When not in development mode: When the SPEC is created or updated, sync the deli **Behavior when development mode is on**: - Do **not** call the Zazz Board API (no POST, PUT, PATCH for deliverables) - Do **not** create or update deliverable cards -- **Only** write the SPEC file to `.zazz/deliverables/{deliverable-name}-SPEC.md` +- **Only** write the SPEC file to `.zazz/deliverables/{deliverableCode}-{slug}-SPEC.md` - The agent **may edit** `.agents/skills/spec-builder-agent/SKILL.md` and `.agents/skills/spec-builder-agent/README.md` to improve the skill. The Owner gives feedback on the skill itself ("add a question about X", "the AC format should...", "Phase 3 is missing Y") and the agent updates these files so the next session benefits. **Behavior when development mode is off**: -- `.agents/skills/spec-builder-agent/SKILL.md` and `.agents/skills/spec-builder-agent/README.md` are **read-only**. The agent must **not** modify them. Only the SPEC file (`.zazz/deliverables/{name}-SPEC.md`) and deliverable cards (via API) may be written. +- `.agents/skills/spec-builder-agent/SKILL.md` and `.agents/skills/spec-builder-agent/README.md` are **read-only**. The agent must **not** modify them. Only the SPEC file (`.zazz/deliverables/{deliverableCode}-{slug}-SPEC.md`) and deliverable cards (via API) may be written. **Focus**: In development mode, skill improvement. Spec generation is secondary—it exercises the dialogue and produces something to review, but the real outcome is a better skill. diff --git a/.zazz/deliverables/ZAZZ-5-fix-routes-no-project-SPEC.md b/.zazz/deliverables/ZAZZ-5-fix-routes-no-project-SPEC.md new file mode 100644 index 00000000..6062e3e6 --- /dev/null +++ b/.zazz/deliverables/ZAZZ-5-fix-routes-no-project-SPEC.md @@ -0,0 +1,146 @@ +# Fix Routes No Project Specification + +## 1. Problem Statement + +**What**: Several API routes operate on project-scoped data (tasks, images) but do not include project in the path or verify that the requested resource belongs to a project the caller is authorized to access. + +**Why**: An agent token scoped to project A could access or modify data belonging to project B (e.g. task images, task metadata). This is a data isolation and authorization gap. + +**Who**: Agents using the zazz-board-api skill; any client with a valid token. + +**Current state**: Routes such as `GET /tasks/:taskId/images`, `POST /tasks/:taskId/images/upload`, `GET /images/:id`, `GET /images/:id/metadata` accept a taskId or imageId without project context. The auth middleware validates the token but does not verify the resource belongs to the token's project. + +**Desired state**: All routes that operate on project-scoped resources must either (a) include project in the path and verify access, or (b) resolve the resource's project and verify the token has access before returning data. + +--- + +## 2. Standards Applied + +- **testing.md** — PactumJS for API tests; every route needs happy path, edge cases, 401/403/404 +- **system-architecture.md** — API layer, auth middleware +- **data-architecture.md** — Tasks belong to deliverables; deliverables belong to projects; images belong to tasks + +--- + +## 3. Scope + +### In Scope + +- Audit all API routes to identify which operate on project-scoped data without project filtering +- Fix image routes: `/tasks/:taskId/images`, `/tasks/:taskId/images/upload`, `/images/:id`, `/images/:id/metadata`, `DELETE /tasks/:taskId/images/:imageId` — add project verification +- Add PactumJS tests for cross-project access (403 when token for project A accesses project B's data) +- Update zazz-board-api skill if route paths change (e.g. images under `/projects/:code/...`) + +### Out of Scope + +- Tags (`/tags`) — design decision: tags may be global or project-scoped; defer to separate deliverable +- Project-level access control (USER_PROJECTS, membership) — see future-fixes.md #5 +- Standardizing `:id` vs `:code` for project params — see future-fixes.md #1 + +--- + +## 4. Features & Requirements + +- **F1**: Identify all routes that operate on project-scoped data (tasks, deliverables, images) but lack project in path or project verification +- **F2**: For image routes: verify the task (and thus its project) belongs to a project the token can access before returning data or performing mutations +- **F3**: Return 403 Forbidden when a valid token attempts to access another project's data +- **F4**: Add PactumJS tests: happy path (same project), 403 (cross-project), 401 (no/invalid token), 404 (resource not found) + +--- + +## 5. Acceptance Criteria + +- **AC1**: `GET /tasks/:taskId/images` returns 403 when the task belongs to a project different from the token's project — Verified by: API test +- **AC2**: `POST /tasks/:taskId/images/upload` returns 403 when the task belongs to a different project — Verified by: API test +- **AC3**: `GET /images/:id` returns 403 when the image's task belongs to a different project — Verified by: API test +- **AC4**: `GET /images/:id/metadata` returns 403 when the image's task belongs to a different project — Verified by: API test +- **AC5**: `DELETE /tasks/:taskId/images/:imageId` returns 403 when the task belongs to a different project — Verified by: API test +- **AC6**: All image routes return 200/201 as before when the task belongs to the token's project — Verified by: API test +- **AC7**: Document the list of routes that were audited and which were fixed vs deferred — Verified by: Owner sign-off + +--- + +## 6. Definition of Done + +- [ ] All AC satisfied +- [ ] All PactumJS tests passing +- [ ] No regression in existing image route tests +- [ ] zazz-board-api skill updated if paths change +- [ ] Owner sign-off for AC7 (audit list) + +--- + +## 7. Test Requirements + +### API Tests (PactumJS) + +- **Image routes**: For each of GET/POST /tasks/:taskId/images, GET /images/:id, GET /images/:id/metadata, DELETE /tasks/:taskId/images/:imageId: + - Happy path: token for project A, task in project A → 200/201 + - Cross-project: token for project A, task in project B → 403 + - No auth: missing/invalid token → 401 + - Not found: valid taskId/imageId that doesn't exist → 404 +- **Setup**: Create deliverables and tasks in ZAZZ and APIMOD (or MOBDEV); use seeded tokens for each project; attempt cross-project access + +--- + +## 8. Agent Constraints & Guidelines + +### Always Do + +- Follow testing.md: PactumJS tests for new/updated routes +- Resolve task → deliverable → project to verify project ownership before returning image data + +### Ask First (Escalate When) + +- Whether to change route paths (e.g. `/projects/:code/tasks/:taskId/images`) vs. keep paths and add project verification in handler +- Tags or other non-image routes that might need project scoping + +### Never Do + +- Return project B's data to a token scoped to project A +- Skip the 403 cross-project tests + +### Prefer When Multiple Options + +- Prefer adding project verification in the handler (resolve task → project, check token) over changing route paths, to minimize client/skill churn. If path change is cleaner, do it. + +--- + +## 9. Decomposition (if complex) + +### Components + +- Route audit (manual/code review) +- Image route handlers: add project verification +- PactumJS tests for cross-project 403 +- zazz-board-api skill update (if paths change) + +### Break Patterns for Planner + +- Phase 1: Audit and document routes; implement project verification for image routes +- Phase 2: Add PactumJS tests; update skill if needed + +--- + +## 10. Evaluation + +- **Functional**: All image routes return 403 for cross-project access; existing same-project flows unchanged +- **Quality**: Tests pass; no new lint issues +- **Completeness**: DoD checklist satisfied +- **Owner verification**: Audit list (AC7) reviewed + +--- + +## 11. Technical Context + +- **Integration**: Auth middleware provides `request.user` and project context from token. Image routes need to resolve task → project and compare with token's project. +- **Modified**: `api/src/routes/images.js`; possibly `api/src/middleware/authMiddleware.js` if shared helper for project verification +- **Dependencies**: None + +--- + +## 12. Edge Cases & Constraints + +- **Task not found**: Return 404 before 403 (don't leak existence of tasks in other projects) +- **Image not found**: Return 404; if we resolve image → task → project for 403, ensure we don't leak image existence across projects +- **Performance**: Resolving task → project adds one DB lookup per request; acceptable for image routes diff --git a/.zazz/deliverables/multiple-agent-tokens-feature-SPEC.md b/.zazz/deliverables/ZAZZ-6-multiple-agent-tokens-feature-SPEC.md similarity index 100% rename from .zazz/deliverables/multiple-agent-tokens-feature-SPEC.md rename to .zazz/deliverables/ZAZZ-6-multiple-agent-tokens-feature-SPEC.md diff --git a/.zazz/deliverables/index.yaml b/.zazz/deliverables/index.yaml index 3b4193c4..12a5c31d 100644 --- a/.zazz/deliverables/index.yaml +++ b/.zazz/deliverables/index.yaml @@ -6,3 +6,9 @@ deliverables: spec: deliverables-feature-SPEC.md plan: deliverables-feature-PLAN.md # status: IN_PROGRESS (Zazz Board is source of truth) + - id: ZAZZ-5 + name: fix-routes-no-project + spec: ZAZZ-5-fix-routes-no-project-SPEC.md + - id: ZAZZ-6 + name: multiple-agent-tokens-feature + spec: ZAZZ-6-multiple-agent-tokens-feature-SPEC.md diff --git a/.zazz/future-fixes.md b/.zazz/future-fixes.md index dbd0f656..84b6be27 100644 --- a/.zazz/future-fixes.md +++ b/.zazz/future-fixes.md @@ -72,4 +72,8 @@ Cleanup and improvements **outside the scope** of current deliverables. Add item **Design**: Non-project routes just work—no agent-token restriction. Authorization is limited to project-scoped routes. When a route gains project scoping (e.g. images become project-scoped), add agent-token authorization there. Authorization follows route scoping; don't restrict until the route has project context. - use **Dredd/Schemathesis** only if you need full contract testing. \ No newline at end of file + use **Dredd/Schemathesis** only if you need full contract testing. + + need to fix dedFilePath to **specFilePath** + + we are going to need to create new seed data for these two deliverables before fix the column \ No newline at end of file From fbaf01d4b16405d3083835b35df0fbea5aefd70b Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Thu, 5 Mar 2026 12:47:04 -0500 Subject: [PATCH 08/10] Standards: validation separation, file org (why: agent file locks), project-scoped pattern - coding-styles: API validation vs business logic; schema org; project-scoped handler pattern; business error mapping; file org (why: parallel work when agents lock files) - system-architecture: link to coding-styles validation section - testing: document openapi.test.mjs Made-with: Cursor --- .zazz/standards/coding-styles.md | 43 ++++++++++++++++++++++++++ .zazz/standards/system-architecture.md | 2 +- .zazz/standards/testing.md | 1 + 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/.zazz/standards/coding-styles.md b/.zazz/standards/coding-styles.md index 6af6ec11..08411a51 100644 --- a/.zazz/standards/coding-styles.md +++ b/.zazz/standards/coding-styles.md @@ -4,6 +4,49 @@ - ESLint for linting - Consistent naming: camelCase for JS, snake_case for DB columns +## File organization: avoid large files spanning multiple areas of functionality + +**Why**: Agents lock files when editing. Splitting by domain or responsibility enables parallel work—multiple agents can edit different files without blocking each other. + +**How**: Keep files focused. Avoid large files that encompass many routes, functions, or areas of functionality. Split by domain or responsibility so work stays scoped. Applied to: `api/src/schemas/` (one file per domain), `api/src/routes/` (one file per domain). + +## API validation (separate from business logic) + +**Schema validation** (request shape, types, patterns) runs **before** the route handler via Fastify + AJV. It validates `params`, `body`, `querystring` against JSON Schema. Invalid requests return 400 without reaching the handler. + +**Business logic validation** (e.g. "resource belongs to project", "deliverable must be approved before creating tasks") lives **in the route handler**. It uses `databaseService` to resolve resources and returns 403/404 when authorization or existence checks fail. + +- **Do not** put business rules in schema (e.g. "task must belong to project") — schemas describe structure, not cross-resource relationships. +- **Do** use schema for: required fields, types, patterns (e.g. `^\\d+$` for numeric ids), enums, min/max length. +- **Prefer schema** over handler checks for request validation; avoid redundant validation in handlers when schema already covers it. + +### Schema organization (`api/src/schemas/`) + +Schemas are split into **separate domain files** per the file organization principle above. + +- **common.js** — Shared params (`idParam`, `codeParam`, `taskIdParam`), response shapes (`deliverableResponseSchema`, `taskResponseSchema`), patterns (`tagNamePattern`) +- **Domain files** — One per domain: `deliverables.js`, `projects.js`, `tasks.js`, `taskGraph.js`, `tags.js`, `users.js`, `images.js`, `core.js` +- **Each route schema** — `params` (property names must match route param names, e.g. `:projectCode` → `projectCode`), `body` (if applicable), `querystring` (optional filters), `response` (status code → schema) +- **Body schemas** — Use `additionalProperties: false` to reject unknown fields (see deliverables, projects, tags) +- **Import** — Routes use `import { deliverableSchemas } from '../schemas/validation.js'` (barrel re-exports from `index.js`) + +New routes: add schema to the appropriate domain file; compose from `common.js` when possible. The OpenAPI spec is generated from these schemas; `api/__tests__/routes/openapi.test.mjs` validates spec correctness. + +### Project-scoped handler pattern + +For routes under `/projects/:code/...` that operate on project-owned resources (deliverables, tasks, relations): + +1. `const project = await dbService.getProjectByCode(code)`; if `!project` → 404 +2. Resolve the resource (e.g. `getDeliverableById`, `getTaskById`) +3. Verify `resource.projectId === project.id`; if not → 404 (or 403 for cross-project access) +4. Proceed with operation + +See `deliverables.js`, `taskGraph.js`, `projects.js` for examples. + +### Business error mapping + +Map service/database errors to appropriate HTTP codes in the handler: business rule violations (cycle, self-ref, not found) → 400; duplicate/conflict → 409; authorization failure → 403. See `taskGraph.js` createTaskRelation for error-mapping pattern. + ## UPPER_SNAKE_CASE for status codes and enum-like values Status codes, priorities, and other enum-like values use **UPPER_SNAKE_CASE** (e.g. `TO_DO`, `IN_PROGRESS`, `QA`, `COMPLETED`, `LOW`, `MEDIUM`, `FEATURE`, `BUG_FIX`). These values are stored in the DB, used in the API, and double as i18n translation keys. diff --git a/.zazz/standards/system-architecture.md b/.zazz/standards/system-architecture.md index 5a3371ef..ece2b634 100644 --- a/.zazz/standards/system-architecture.md +++ b/.zazz/standards/system-architecture.md @@ -11,7 +11,7 @@ ## Layers -- **API**: Fastify routes, JSON Schema validation, auth middleware +- **API**: Fastify routes, JSON Schema validation (see [coding-styles.md](./coding-styles.md#api-validation-separate-from-business-logic)), auth middleware - **Services**: `databaseService` (Drizzle), `tokenService` - **Client**: React, Vite, Mantine, react-router-dom diff --git a/.zazz/standards/testing.md b/.zazz/standards/testing.md index 200f9b71..6c6ba29c 100644 --- a/.zazz/standards/testing.md +++ b/.zazz/standards/testing.md @@ -46,6 +46,7 @@ For each route, add a PactumJS test file covering: ## Patterns - `beforeEach` calls `clearTaskData()` — deletes TASK_RELATIONS, TASK_TAGS, TASKS, DELIVERABLES; ensures isolation +- **OpenAPI spec tests** (`openapi.test.mjs`) — Validates the generated spec is valid OpenAPI 3.x and documents core agent routes (create deliverable, create task). Run with the full test suite. - Create test data via `createTestDeliverable()`, `createTestTask()`; tasks require `deliverableId` - Tests use port 3031; API base URL from env - Token: `550e8400-e29b-41d4-a716-446655440000` (seeded user) From ab41ea68ee117b4dc2239b5b699e9f72ac3053fe Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Thu, 5 Mar 2026 12:47:50 -0500 Subject: [PATCH 09/10] =?UTF-8?q?Skills:=20planner=20note=20on=20plan=20he?= =?UTF-8?q?ader=20(project/deliverable=20codes);=20zazz-board-api=20PROJEC?= =?UTF-8?q?T=5FCODE=20=E2=86=92=20ZAZZ=5FPROJECT=5FCODE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .agents/skills/planner-agent/SKILL.md | 2 ++ .agents/skills/zazz-board-api/SKILL.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.agents/skills/planner-agent/SKILL.md b/.agents/skills/planner-agent/SKILL.md index 341adce7..c062c47c 100644 --- a/.agents/skills/planner-agent/SKILL.md +++ b/.agents/skills/planner-agent/SKILL.md @@ -84,3 +84,5 @@ export AGENT_ID="planner" export ZAZZ_WORKSPACE="/path/to/project" export ZAZZ_STATE_DIR="${ZAZZ_WORKSPACE}/.zazz" ``` + +Notes - in the top of the plan we shoud call out the Procect Code and the Delerable code and the deliverable Id (integer) so the planner agent does not need to look up these values \ No newline at end of file diff --git a/.agents/skills/zazz-board-api/SKILL.md b/.agents/skills/zazz-board-api/SKILL.md index 33a094ae..fe5f0aea 100644 --- a/.agents/skills/zazz-board-api/SKILL.md +++ b/.agents/skills/zazz-board-api/SKILL.md @@ -25,7 +25,7 @@ All API requests (except `/openapi.json`, `/health`, `/`, `/db-test`, `/token-in |----------|----------|---------| | `ZAZZ_API_BASE_URL` | `http://localhost:3030` | API base; spec at `{base}/openapi.json` | | `ZAZZ_API_TOKEN` | `550e8400-e29b-41d4-a716-446655440000` | Auth token | -| `PROJECT_CODE` | `ZAZZ` | Default project code | +| `ZAZZ_PROJECT_CODE` | `ZAZZ` | Default project code | --- From b30cba52f2dc6279350ebbc3624ce933383fbcb6 Mon Sep 17 00:00:00 2001 From: michaelwitz Date: Thu, 5 Mar 2026 12:48:20 -0500 Subject: [PATCH 10/10] =?UTF-8?q?Fix=20typos=20in=20planner-agent=20SKILL:?= =?UTF-8?q?=20Procect=E2=86=92Project,=20Delerable=E2=86=92Deliverable,=20?= =?UTF-8?q?shoud=E2=86=92should?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .agents/skills/planner-agent/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/planner-agent/SKILL.md b/.agents/skills/planner-agent/SKILL.md index c062c47c..be63aca6 100644 --- a/.agents/skills/planner-agent/SKILL.md +++ b/.agents/skills/planner-agent/SKILL.md @@ -85,4 +85,4 @@ export ZAZZ_WORKSPACE="/path/to/project" export ZAZZ_STATE_DIR="${ZAZZ_WORKSPACE}/.zazz" ``` -Notes - in the top of the plan we shoud call out the Procect Code and the Delerable code and the deliverable Id (integer) so the planner agent does not need to look up these values \ No newline at end of file +Notes - in the top of the plan we should call out the Project Code and the Deliverable code and the deliverable Id (integer) so the planner agent does not need to look up these values \ No newline at end of file