Middle Agent Workspace
Middle Agent Workspace
Section titled “Middle Agent Workspace”Middle agents (brokers, regional intermediaries) offer artists they do not exclusively represent. Kisum models them with private workspace artist cards and a dual-sided offer pipeline (buyer vs source) without allowing false official representation claims.
Entitlement (Option B — shipped 2026-06-26)
Section titled “Entitlement (Option B — shipped 2026-06-26)”| Layer | Requirement |
|---|---|
| Core | Active middle_agency addon → enabledModules includes middle_agency |
| Auth | User grants on middle_agency.* (intersection with company modules) |
| Artists workspace | organization_type ∈ {middle_agent, hybrid} for operator nav/API |
| Promoter buyer | Standard promoter booking permissions (promoter.artist.offer.view / .edit) — does not require middle_agency addon on the buyer org |
Gate order: JWT + canonical x-org → Core addon → Auth permission → workspace type (Artists operator only).
Phase 1 purchase path: Admin assign (POST /api/v1/admin/companies/:id/addons, addonKey: middle_agency). Checkout SKU deferred.
Direct agency flows (booking-requests, official roster, claims) do not require the addon.
Where (SoT)
Section titled “Where (SoT)”| Data | Owner |
|---|---|
| Workspace profile, roster cards, offers, chains, commissions | Backend-Kisum-Artists PostgreSQL |
| Official artists, representation claims | Backend-Kisum-Artists (existing directory) |
| Permissions | Backend-Kisum-Auth (middle_agency.*, module_key=middle_agency) |
| Module entitlement | Backend-Kisum-Core — middle_agency addon (company_addons) |
| Payments / settlement truth | Backend-Kisum-Finance |
| Promoter browser API | Backend-Kisum-Promoters BFF /api/artists-network/middle-agent/* (buyer) and /api/artists-network/middle-agent-workspace/* (operator) |
How — operator (middle agent)
Section titled “How — operator (middle agent)”- Admin assigns Middle Agency addon to the org; user re-logins.
- Set
organization_type = middle_agenton workspace profile (or use onboarding at/profile/middle-agent-workspacein Promoters). - Create WorkspaceArtist cards (
artist_record_scope = workspace_private). - Manage offers with separate buyer_status and source_status.
- Optional: publish listing → promoter marketplace.
Apps:
| Persona shell | Browser contract | Upstream |
|---|---|---|
Frontend-Kisum-Artists | Direct GET/PATCH /api/v1/middle-agent/* (403 without addon + approved company claim) | Artists BE |
Frontend-Kisum-Promoters | /middle-agent/* operator desk → BFF /api/artists-network/middle-agent-workspace/* (403 without addon; no source-field redaction on operator routes) | Artists BE |
Promoter org bridge (2026-07-09): Promoter-only orgs with middle_agency addon but no approved artists_company_claims row are auto-provisioned on first operator API call (ResolveOrProvisionCompanyIDByOrg): inserts a middle_agent_operators row keyed by Core core_company_id (organization_type=middle_agent, display_name=Middle Agent …). No fake companies directory row and no claim bridge. Real agencies keep their directory companies row; the operator row links via directory_company_id. All middle_agent_* tables use company_id → middle_agent_operators(id).
Operator lifecycle (internal, optional): POST /internal/middle-agent-operators/{orgID}/deactivate|activate flips middle_agent_operators.status (active/inactive). Access remains gated by the Core middle_agency entitlement guard; Admin/Core caller wiring is a follow-up.
Buyer vs operator in Promoters: /booking/* middle-agent flows (marketplace Secondary tab, offers inbox, enquiry wizard) stay on /api/artists-network/middle-agent/* and remain promoter-safe (redacted). /middle-agent/* is the broker operator desk only.
How — promoter
Section titled “How — promoter”- Open Booking → Marketplace (no Middle Agency addon required on the promoter org).
- Browse
GET /api/artists-network/marketplace/artist-availabilities(BFF → Artists unifiedGET /api/v1/marketplace/artist-availabilities). One response includes both exclusive (official: true) and secondary (official: false) rows; UI tabs filter client-side or viaofficial=true|false. LegacyGET .../middle-agent/availability-listingsstill works (proxies unified withofficial=false). Deprecated workspace-cardGET .../middle-agent/listings. POST .../middle-agent/enquirieswithavailabilityListingIdand structuredoffersheet(wizard v1) → middle-agent incoming offer tied to the published listing. Full payload stored inmiddle_agent_offers.metadata_json.offersheet; legacy columns (event_name,venue_name,territory_text,country_id,city_id,buyer_price_*, tour dates) derived for inbox compatibility.- Track
GET .../middle-agent/enquiries→ ArtistsGET /api/v1/marketplace/middle-agent-enquiries(buyer org scoped viametadata.buyerCoreCompanyId). - Declined/cancelled offers —
PATCH/DELETE .../middle-agent/enquiries/:idto revise (resubmit asincoming) or soft-delete; revision logged inmiddle_agent_offer_activity(buyer_offer_revised,buyer_offer_deleted).
Buyer offer detail + revise (2026-06-26)
Section titled “Buyer offer detail + revise (2026-06-26)”| Piece | Route / behavior |
|---|---|
| Detail enrich | GET .../middle-agent-enquiries/:id returns sellerCompanyName, officialArtistId, imageUrl, timeline, revisionHistory (from activity + listing/metadata joins) |
| Revise | PATCH /api/v1/marketplace/middle-agent-enquiries/:id — only when buyer_status ∈ {declined, closed_lost, cancelled, lost}; full offersheet required; sets buyer_status=incoming |
| Delete | DELETE .../:id — hard delete (row removed from middle_agent_offers; gone from agency + promoter lists). Only when buyer_status ∈ {declined, closed_lost, cancelled, lost}. |
| BFF | Promoters `PATCH |
| Promoter UI | /booking/offers/[id] — artist image + profile link (target=_blank), seller name, venue profile link when shows[].venueId is set; Edit & resubmit / Delete on declined offers; revision history card |
| Edit wizard | /booking/marketplace/enquire?listingId=…&editOfferId=… prefills offersheet; editOfferId alone resolves listing from stored enquiry |
| MA operator visibility | buyer_offer_revised appears in existing Activity tab (GET /middle-agent/offers/:id/activity) |
Offersheet venue link: store shows[].venueId (+ venueName). Links go to /venues/{venueId} (promoter venue detail). Marketplace route /venues/marketplace/* is for browse/booking flow only — not offer/enquiry links.
Structured offersheet enquiry (2026-06-25)
Section titled “Structured offersheet enquiry (2026-06-25)”| Piece | Route / storage |
|---|---|
| Allowed territories | GET /api/v1/marketplace/middle-agent-availability-listings/:id/allowed-territories — BFF GET .../middle-agent/availability-listings/:id/allowed-territories |
| Geo cities | BFF GET /api/artists-network/geo/countries/:id/cities?q= → Artists GET /api/v1/countries/:id/cities |
| Venue search (country-scoped) | Promoters searchMarketplaceVenues({ countryIso2 }) → Venues admin iso2 filter |
| Platform T&C | GET /api/v1/marketplace/offersheet-terms?version=2026-06-v1 — returns platformTerms (Kisum platform disclaimer) and performanceOfferTerms (performance-offer clauses). Top-level markdown remains performance-offer terms for backward-compatible clients. |
| Org templates | promoter_offersheet_templates (buyer_core_company_uuid = promoter x-org); CRUD under /api/v1/marketplace/offersheet-templates |
| Promoter UI | Frontend-Kisum-Promoters — 10-step wizard in enquiry sheet; settings at /booking/offer-templates |
| MA read view | Frontend-Kisum-Artists — OffersheetSummary on /middle-agent/offers/:id from metadata_json.offersheet |
Offersheet schema version: 2026-06-v1 — shows (listing-bound countries), fee (total or per-row), key-details matrix, clause sections (default + append + optional replace HTML), additional conditions, T&C acceptance (terms.accepted, version, timestamp).
Artist binding (2026-06-26, updated 2026-06-30): Promoter enquiry UI does not allow editing the artist name. Each submit carries offersheet.targets[] with { listingId, officialArtistId, artistDisplayName } for the listing being enquired on; officialArtistId is always Postgres public.artists.id (numeric string) — not artists.uuid. Optional eventTitle appends to the derived eventName (e.g. Tyga — Asia Tour 2026). Artists BE validates target listing/artist against the published listing and resolves event_name from targets server-side.
Date mode (2026-06-26): When listing flexibleDates is true (open / flexible routing), step 1 shows tour window start/end and optional per-show dates. When the listing has fixed availableFrom / availableTo, those pickers are hidden and the offer uses the listing dates on submit.
Multi-show tour link (2026-07-15): When the offersheet has 2+ shows (or eventLinkMode=tour), Promoters BFF creates one promoter event per show, then groups them in a tour (EventGroupCommand). Each created event stores Postgres artists.id in lineup (from offersheet.targets[].officialArtistId). Tour creation validates and hydrates lineup through Artists directory — not legacy Mongo artists ObjectIds. event_group.description is optional (empty string default) — marketplace tour auto-create does not supply one.
App: Frontend-Kisum-Promoters — Non-direct tab always visible; merged Offers inbox includes middle-agent rows (tagged Middle agent).
Open request broadcast (2026-06-26)
Section titled “Open request broadcast (2026-06-26)”Promoter booking interest from artist profile (not marketplace listings) uses booking_requests + MA fan-out — ADR-compliant (does not extend booking_offers).
| Step | Route / behavior |
|---|---|
| Create | Promoter POST /api/artists-network/booking-requests → Artists auto-routes: approved representation → routing_mode=direct_agency + target_company_id; else routing_mode=middle_agent_broadcast + fan-out |
| Fan-out | One middle_agent_offers row per eligible MA workspace (offer_origin=open_request_broadcast, buyer_status=incoming); metadata links bookingRequestId, buyerCoreCompanyId, artist snapshot |
| MA inbox | GET /api/v1/middle-agent/open-requests (+ detail, POST .../:id/quote simple fee/currency/note) |
| Promoter responses | GET .../booking-requests/:id includes broadcastResponses + responseCount; quotes appear in merged GET .../middle-agent/enquiries (Offers inbox) |
| Accept | POST /api/v1/marketplace/middle-agent-enquiries/:id/accept → BFF POST .../middle-agent/enquiries/:id/accept; closes sibling broadcast offers + fulfills booking request |
| Withdraw | POST .../booking-requests/:id/withdraw sets request status cancelled and closes linked broadcast MA offers |
| Delete | DELETE .../booking-requests/:id — promoter hard-delete when status is cancelled (legacy withdrawn rows also allowed until migrated) |
Migration: Artists 000023 — booking_requests.routing_mode; extends middle_agent_offers.offer_origin CHECK for open_request_broadcast.
UI: Promoter Request booking on /artists/[id] (city/country-first). MA Open requests nav in Frontend-Kisum-Artists. Marketplace direct avails no longer expose Send request (profile is the entry point).
How — direct / Exclusive agency
Section titled “How — direct / Exclusive agency”Verified direct/exclusive agencies receive official marketplace offers through the normal booking workflow:
GET /api/v1/booking-offersand/booking-offers/{id}under normal Artist/agency access.- Agency inbox (2026-07-15): incoming exclusive offers also appear in
/booking-requests(Booking inbox UI) with a nav badge fromGET /api/v1/booking-inbox/summary; list filterGET /api/v1/booking-offers?inbox=incoming&channel=exclusive. On submit,NotifyNewExclusiveMarketplaceOfferemails configured recipients (MA_ENQUIRY_NOTIFY_EMAILS/EMAIL_FROM) — same pattern as middle-agent enquiry notifications. - No
middle_agent_operatorsrow, Middle Agent addon, ormiddle_agency.*permission is required. - The agency may counter, accept, or decline. Acceptance creates the normal confirmed booking that unlocks contract, touring, and logistics workspaces.
- Auto-provisioned paperwork on accept (2026-07-15): accepting an exclusive offer now best-effort auto-creates, right after the booking, a per-booking logistics workspace, a tour plan + stop, a DRAFT contract (only when the agency already has a contract template; skipped until one is uploaded), and — when the offer carries a structured payment plan — a receivable Finance invoice for the agency (
POST /api/income, one term per installment,amount= sum of installments). Promoters capture the plan in the offer wizard Fee step (offersheet.paymentPlan.installments[]); Artists posts the invoice withAuthorization: Bearer <FINANCE_INTERNAL_API_KEY>+x-org, gated on envFINANCE_ARTISTS_INCOME_PATH_TEMPLATE. All steps never send signature emails, are idempotent (no-ops once logistics exists), and can never fail the accept (best-effort, panic-recovered). Middle-agent confirm now runs the SAME paperwork — booking + DRAFT contract + receivable invoice — MINUS touring/logistics (the only exclusive-vs-MA difference). The contract + invoice logic is shared (internal/artists/paperwork). MA bookings originate from amiddle_agent_offer(migration000039:bookings.middle_agent_offer_id, nullableoffer_id); the operator’smiddle_agent_operators.core_company_idbills the invoice anddirectory_company_idowns the contract (pure MA desks have none → contract skipped). A MA offer with no official artist creates no booking/contract but still invoices. Idempotency:bookings.middle_agent_offer_idunique +middle_agent_offers.canonical_booking_id+metadata_json.autoInvoiceCreated. - Seller resolution (2026-07-15): the receiving agency is resolved from the availability’s creating org (
artist_availabilities.created_by_core_company_id) via its approvedartists_company_claimscompany, preferring an artist-specific approvedartist_representation_claimswhen one exists. It does not require anis_exclusiverepresentation claim to exist — an official availability an agency publishes for an artist it owns/manages (with no formal exclusive claim) still routes to that agency’sbooking_offersinbox, and the resolved company satisfies theIsCompanyOwnedByOrgaccept-side check. Resolution is NULL-safe: a genuinely unrepresented availability degrades to a clear400, never a500. SharedofficialAvailabilitySellerJoinbacks both the marketplace listing card and the offer enquiry.
GET /api/v1/middle-agent/inbound-source-offers remains a Middle Agent source/partner surface and must not be used as the Exclusive agency inbox.
Security
Section titled “Security”- Server redacts
source_fee,margin,commissionwithoutmiddle_agency.offer.*financial keys. - BFF strips the same fields for promoters on buyer routes (
/api/artists-network/middle-agent/*). - Operator BFF (
/api/artists-network/middle-agent-workspace/*) does not redact margin/source fields — entitled operator users receive full deal-desk data permiddle_agency.offer.*grants. - Artists BE operator routes / Promoters BFF operator surfaces return “Middle Agency addon is required” when Core entitlement missing on the caller org. Marketplace buyer browse/enquiry routes accept promoter booking permissions without that addon.
- Cannot set
representation_typeto direct/exclusive without approved representation claim.
Ops — grandfather MVP orgs
Section titled “Ops — grandfather MVP orgs”- Discover Core company UUIDs from Artists DB (
middle_agent_operators,middle_agent_offers). - Run
Backend-Kisum-Core/scripts/grandfather_middle_agency_addon.sql. - Re-run Auth
migrate-permissions; users re-login.
Availability listings + guest intake (2026-06-25 completion)
Section titled “Availability listings + guest intake (2026-06-25 completion)”| Flow | Browser / API |
|---|---|
| Listing wizard | Frontend-Kisum-Artists → /middle-agent/listings/new (5-step: Artist → Territory → Dates → Economics → Review; overlap check via GET /middle-agent/availability-listings/overlap-check) |
| Listing detail (edit) | Frontend-Kisum-Artists → /middle-agent/listings/:id — read-only summary by default; Edit opens single-page form (artist, territory picker, dates, event types, fees, commission, expiry). PATCH /api/v1/middle-agent/availability-listings/:id. Overlap warning uses GET .../overlap-check?excludeListingId=:id. Published rows keep share-intake actions on the same page (view mode). Delete (next to Edit) when status is draft or archived (not published); DELETE /api/v1/middle-agent/availability-listings/:id. |
| Directory artist search | GET /api/v1/middle-agent/directory/artists?q= — global directory (not agency roster); returns imageUrl, name, embedded country; typeahead skips COUNT(*) unless includeTotal=true; backed by migration 000018 (pg_trgm on artists.name / sort_name) |
| Link A offer share | Mint on listing share (operator desk on Promoters or Artists) → guest URL /form/offer/{token} on Fullstack-Kisum-Public (no Kisum login — isolated /form/* sub-app with Kisum app-shell chrome; browser calls same-origin /api/form/* BFF only). BFF proxies token routes to **`GET |
| Link B offer share | /middle-agent/intake-links org token mint with optional allowedArtistIds |
| Token mgmt | `GET |
| Inbox summary | GET /api/v1/middle-agent/inbox-summary → unread incoming count for nav badge |
| Marketplace report | Promoter POST /api/artists-network/middle-agent/availability-listings/:id/report → Artists marketplace report |
| Channels (MA-CHANNEL-2) | POST /middle-agent/channel-integrations/:channel/connect; public webhooks `GET |
Artists SES via EMAIL_ENABLED, SES_*, MA_ENQUIRY_NOTIFY_EMAILS on new enquiry | |
| SEO | Guest intake routes: noindex metadata + X-Robots-Tag: noindex |
Public offersheet guest form (2026-06-29)
Section titled “Public offersheet guest form (2026-06-29)”The public guest link (/form/offer/{token} on Fullstack-Kisum-Public) for a listing token hosts the full structured offersheet wizard — the same offersheet a logged-in promoter sends, but unauthenticated and without any Kisum event (guests have no events). A guest submission becomes a real middle_agent_offers row (offer_origin = external_offer, legacy rows may use external_intake; buyer_status = incoming) with the complete offersheet in metadata_json.offersheet, landing in the seller’s offers inbox exactly like an authenticated enquiry.
| Concern | Behaviour |
|---|---|
| Host UI | Fullstack-Kisum-Public — isolated /form/* layout with canonical Kisum app-shell (DESIGN.md §6): logo row, SideNavRail, Register (header slot → www.kisum.io/pricing), Log in (footer slot → app.kisum.io). No company switcher, no module menu. Two-column enquire page (locked listing context + wizard card). |
| Browser API | Same-origin only: /api/form/offer/{token}/*, /api/geo/*. No Artists/Venues base URLs or internal keys in the browser bundle. |
| Wizard | Fullstack-Kisum-Public guest-mode copy of the offer-enquiry wizard at src/components/form/offer/offer-enquiry-wizard/. 11 steps: Your details (Name, Company, Address, Email, Country, Phone/WhatsApp — all required) → Shows → Fee → Key details → Marketing/PR → VIP → Special provisions → Exclusivity → Payment schedule → Additional → Review. No org context, no promoter-event linkage (shows use country picker + venue search + free-text city/venue). Browser-local drafts (2026-06-30): multi-draft localStorage per offer token (kisum:guest-offer-drafts:{token}), OfferEnquiryDraftsPanel below locked listing (same UX as Promoters — load/delete/new offer; form starts blank until a draft is picked); cleared on successful submit. Not synced server-side. Guest address + country ride in IntakeSubmitInput and are stored in the offer’s metadata_json guest block. |
| Submit | POST /api/v1/public/offer/{token} (legacy /public/middle-agent/intake/{token}) body extended with optional offersheet (the OffersheetPayload shape). Backend runs the same pipeline as the authenticated path — validateOffersheet(…, requireEventLinks=false), validateOffersheetTargets, legacy-field derivation, mergeOffersheetMetadata — then createOfferWithAvailabilityListing. Backward compatible: a contact-only submission (no offersheet) still works. |
| Public reads | GET /api/v1/public/offer/{token}/territories (allowed countries for the listing) and GET …/{token}/offersheet-terms (platformTerms + performanceOfferTerms markdown blocks; top-level markdown = performance-offer terms for legacy clients). Guest review step requires both acceptances; public submit validates offersheet.platformTerms.accepted when requireEventLinks=false. GET …/{token}/venues — token-gated proxy to Venues GET /internal/admin/venues (machine VENUE_INTERNAL_API_KEY; query params search, iso2, city, page, limit; always includes sleeping venues). Guest offersheet venue picker uses this route — browsers never see the Venues internal key. The Your details step’s Country dropdown + phone dial-code come from GET /api/v1/public/countries (Artists geo SoT; returns dial = phone_prefix and flag = flag_emoji alongside code/name). Fee step currency uses GET /api/v1/public/currencies — distinct currency_code values from the Artists countries table; priority order USD, EUR, GBP, AUD, SGD, CNY, then A–Z; default USD. Public Form BFF proxies both geo routes at /api/geo/public-countries and /api/geo/public-currencies. Clause/key-detail defaults are version-stamped client constants (2026-06-v1, matching backend OffersheetPlatformVersion). |
| Representation-agnostic | The offer GET payload surfaces sellerOrganizationType (middle_agent / agency / hybrid) and the listing’s true representationLabel. Guest UI copy distinguishes middle-agent vs official agency (disclaimer, seller field label, representation badge). Agency orgs publish listings with representation_label = exclusive + official disclaimer. Legacy /form/middle-agent/{token} redirects to /form/offer/{token}. Fail-safe (2026-06-29): when neither sellerOrganizationType nor representationLabel gives a clear signal, the guest UI resolves an unknown seller kind — neutral copy + subject_to_approval badge, never treated as official — instead of silently defaulting to middle-agent. |
| Guest IP (2026-06-29) | The Fullstack-Kisum-Public BFF forwards the guest’s real IP (X-Forwarded-For / X-Real-IP) when proxying the submit to Artists. The backend SubmitPublicOffer resolves it via clientIPFromCtx (forwarded header → c.IP() fallback) so per-IP rate limiting and the stored audit IP reflect the guest, not the BFF’s egress IP. Rate-limit key leads with the token hash, so a spoofed header can’t widen the per-token ceiling. |
| Guest locked context UI | GET …/offer/{token} includes imageUrl on the listing artist and each allowedArtists[] row (directory headshot + profile JSON fallbacks). The guest page shows a circular avatar beside the artist name linking to the Promoters directory profile at https://app.kisum.io/artists/{uuid} — one artist as a title row, multiple as cards. Listing dates display as d MMM, yyyy (date only, no time). |
| Validation gate | requireEventLinks flag on validateOffersheet: authenticated promoter/marketplace callers pass true (must link Kisum events); the public guest path passes false. |
Org tokens (Link B) have no single listing, so they keep the lightweight contact + city-rows form (offersheet submission requires a listing token).
Env (channels + email): WHATSAPP_APP_ID, WHATSAPP_VERIFY_TOKEN, GMAIL_OAUTH_CLIENT_ID, GMAIL_OAUTH_REDIRECT_URI, ARTISTS_PUBLIC_BASE_URL, MA_ENQUIRY_NOTIFY_EMAILS.
Migrations (run per env): Artists 000014–000020; Auth 000015 + permission reseed.
Availability listing economics (2026-06-26)
Section titled “Availability listing economics (2026-06-26)”| Field | Storage | Notes |
|---|---|---|
eventTypes | middle_agent_availability_listings.event_types_json | Optional multi-select. Allowed values: concert, private_corporate, club, festival, other. Same API for middle_agent, agency, and hybrid workspace org types. Legacy single eventType accepted on write only. |
artistFee | fee_terms_json.artistFee (number) | Optional numeric fee; marketplace redaction exposes artistFee (not internal commission). Legacy feeBandText still accepted on read for older rows. |
feeTerms.notes | fee_terms_json.notes | Free-text fee notes. |
commission | commission_json | Internal; stripped on promoter marketplace reads. |
Create/update body: AvailabilityListingInput.eventTypes (array) + feeTerms.artistFee + territories (array of region:{id} / subregion:{id} / country:{id} keys from the territory picker) + optional countryId when a single country is selected.
Listings search (operator workspace, 2026-06-26)
Section titled “Listings search (operator workspace, 2026-06-26)”| Surface | Behavior |
|---|---|
GET /api/v1/middle-agent/availability-listings | Operator list (company-scoped). status: published (UI default), unpublished (draft + archived), or exact draft / archived / published. q matches official artist name (not territory text). Territory filters: regionId, subregionId, countryId (bigint geo ids). Filters combine with AND when multiple territory params are sent. |
GET /api/v1/middle-agent/availability-listings/territory-facets | Returns { countries, regions, subregions } derived from this company’s listings only. Country dropdown in UI is limited to countries from this endpoint. Legacy rows with only territory_text are matched by country name for facets + filters. |
Frontend-Kisum-Artists /middle-agent/listings | Status filter defaults to Published; Unpublished & drafts shows draft + archived. Two search modes: Artist name (text) or Territory (region / subregion / country selects). Modes are mutually exclusive on apply. |
- Artist module
- Artist SoT registry
- Promoters BFF integration
- ADR:
Backend-Kisum-Artists/docs/adr/20260626-middle-agency-addon-entitlement.md - Repo plans:
Backend-Kisum-Artists/MIDDLE_AGENT_WORKSPACE.md - Workspace overview: monorepo root
README_MIDDLE-AGENTS.md— product intent, repos, flows, verification