{"version":"2.2.0","purpose":"Product Law v2 — machine-readable constitution so every writer (UI, API, MCP, native agent) keeps the commercial graph clean: real clients, real properties, no twin docs.","preferredIntegration":{"tool":"forge_agent","rest":"POST /api/v1/agent/run","why":"One call handles multi-step create/update with duplicate guards and quality rules."},"laws":[{"id":"prefer_native_agent","law":"Prefer POST /api/v1/agent/run (or MCP forge_agent) for multi-step work. Thin CRUD is for simple single-record ops when you already have IDs."},{"id":"never_create_to_update","law":"NEVER call create to apply an edit. If the user says update/fix/change/revise a J-#### / Q-#### / INV-####, use the matching update/revise tool on that same number."},{"id":"list_before_create","law":"Before creating a job/invoice/quote for a client that may already have one, list/search first (forge_list_jobs, forge_list_invoices, forge_search)."},{"id":"ids_accept_numbers","law":"All get/update tools accept UUID OR human numbers: J-0042, Q-0016, INV-0038. Pass the number the user said."},{"id":"sent_is_not_abandoned","law":"Issued/emailed/sent unpaid invoices are editable in place via forge_revise_invoice or forge_update_invoice (incl. applyTax for missing GST and targetTotal for price fixes). Same INV number is kept. Unpaid QB-synced invoices revise in place and push to the existing QuickBooks invoice. Never void to add GST. Never mint INV-0050 to fix INV-0049."},{"id":"duplicates_are_errors","law":"If a tool returns code DUPLICATE_DETECTED or ALREADY_INVOICED, use existing.id / existing.number with the suggestedTool. Do not retry create with force unless the user explicitly wants a second record."},{"id":"obey_recovery","law":"Every error includes recovery.suggestedTool + recovery.suggestedArgs. Call that tool next. Do not invent unlock endpoints, revised invoice numbers, or manual workarounds."},{"id":"field_aliases","law":"email→billingEmail, phone→billingPhone, address→billingAddress, state→province, zip→postal. Both forms accepted."},{"id":"line_items_not_notes","law":"Put scope and pricing in lineItems (description, qty, unitPrice). Never dump price-only into title/notes."},{"id":"no_invention","law":"If a tool is missing from your host, say so and route to forge_agent /agent/run. Do not claim Forge lacks list/update/revise when the API has them — check GET /api/v1/agent-laws and /.well-known/api.json."},{"id":"idempotency_keys","law":"On create retries (timeouts/network), send Idempotency-Key header (REST), args.idempotencyKey on MCP forge_create_* / forge_agent, or the same key on POST /agent/run. Safe for 24h — prevents double-create on retry."},{"id":"follow_links","law":"Success payloads may include _links[] and nextActions[]. Prefer those tools/args over inventing new API shapes."},{"id":"search_numbers","law":"forge_search and list tools accept human numbers (J-0042, INV-0038, Q-0016) and PO tokens. Search before create when the user cites a number or PO."},{"id":"convert_not_orphan_invoice","law":"To bill a completed job, use forge_convert_job_to_invoice or playbook_invoice_from_job — not a detached forge_create_invoice that skips the job link (unless user wants a standalone non-job invoice)."},{"id":"approvals_are_success","law":"needs_approval / needsApproval[] means the plan worked and is parked for human confirm — not a failure. Do not retry the same send."},{"id":"one_client_identity","law":"Never attach work to a weak name match. If forge_list_clients returns multiple candidates, disambiguate or create the exact named client — do not silently pick candidates[0]."},{"id":"product_law_balanced","law":"Product Law mode=balanced is the runtime constitution for all commercial writes (UI/API/MCP/agent). Clear email/phone/name match → reuse client. Clear site in fields or notes → link or create property (never leave address only in notes). Ambiguous matches → stop and ask. force:true should include forceReason and is audited."},{"id":"site_is_property","law":"Service location is a Property record linked via propertyId. Do not park site address only in quote/job notes. Prefer propertyAddress/propertyLabel fields or playbook_quote_from_brief; Product Law will extract and resolve when site text is present."},{"id":"house_style_per_tenant","law":"Each company account has one quote house style, shared by every employee on that account. Before writing a quote, follow GET /api/v1/quote-house-style (titles, line description vs detail, voice). Copy that company's shape — never another tenant's wording, never a new voice."},{"id":"one_open_invoice","law":"A job gets at most one open invoice. If OPEN_INVOICE_EXISTS or ALREADY_INVOICED, revise the existing INV — never mint a twin."},{"id":"force_is_exception","law":"force:true bypasses Product Law only for intentional exceptions. Prefer recovery.suggestedTool. When forcing, pass forceReason for the audit log."},{"id":"site_walk_is_a_visit","law":"A site walk is a site visit, not a job. Use playbook_log_site_walk (or the in-app site visit flow). Do not create a $0 job to stand in for one. Do not invent a quote from the walk unless the user asks — then use playbook_quote_from_site_note."},{"id":"one_saved_property","law":"If a client has exactly one saved property, file the quote, job, or invoice on that property even when a billing address also exists. Two or more saved properties: ask which site. Never persist the billing placeholder id __billing_address__ as a property."},{"id":"client_ready_photos","law":"Never attach job photos to a client email or send unless they are marked client-ready. Selecting a photo number is not permission to send it."},{"id":"bulk_import_via_ui","law":"Spreadsheet, CSV, Excel, vCard, Jobber, or QuickBooks client dumps go to Clients → Import Clients. Do not create hundreds of clients one by one from a file. A short typed list (about five, no file) may be created in chat. Nothing from a file is saved until the user taps Import. Never claim an import finished unless they did."}],"commonMistakes":[{"mistake":"User wants a job, quote, invoice, or property moved to another client, or a job pointed at another property, quote, or invoice → agent calls forge_update_job or creates a twin","fix":"forge_move_record. Preview with confirm false. Write only after the user agrees, with confirm true. A property move takes its quotes, jobs, and invoices. A job link is allowed only when the property, quote, or invoice belongs to the same client. Does not change QuickBooks."},{"mistake":"User says update J-0042 → agent calls forge_create_job → J-0043 duplicate","fix":"forge_update_job with id: 'J-0042' (or forge_list_jobs first)"},{"mistake":"Sent INV-0038 has wrong address → agent creates INV-0040","fix":"forge_revise_invoice id: 'INV-0038' with refreshBillingAddress + refreshPropertyAddress"},{"mistake":"Agent claims Forge has no list/update tools","fix":"Tools exist in MCP + REST. If the host only exposed create-*, that is a host gap — use forge_agent /agent/run instead."},{"mistake":"Agent invents unlock endpoint or INV-####-REV numbering","fix":"Use forge_reopen_invoice / forge_revise_invoice. Numbers stay stable for unpaid invoices."},{"mistake":"Retried create after timeout without Idempotency-Key → duplicate record","fix":"Send the same Idempotency-Key (REST header) or args.idempotencyKey (MCP) on retry — 24h replay, no second record"},{"mistake":"Ignored _links/nextActions and invented a new endpoint","fix":"Call the tool/path listed in _links or recovery.suggestedTool"},{"mistake":"Created standalone invoice instead of convert_job_to_invoice","fix":"forge_convert_job_to_invoice or playbook_invoice_from_job for completed jobs"},{"mistake":"Told user quote was emailed when only status=sent / mark-sent ran","fix":"Require emailSent:true (+ resendMessageId). POST /quotes/:id/send must call Resend; mark-sent is external delivery only"},{"mistake":"Put job site only in notes → client has no Property, PDF site blank, agents invent twin clients","fix":"Pass propertyId or propertyAddress/propertyLabel (or playbook_quote_from_brief). Product Law auto-links/creates property in balanced mode and strips site lines from notes."},{"mistake":"Same company, new spelling → second client","fix":"Product Law blocks normalized name + email/phone matches. Use existing id from 409 existing / recovery."},{"mistake":"Second invoice because first was 'wrong'","fix":"OPEN_INVOICE_EXISTS / ALREADY_INVOICED → forge_revise_invoice on same INV number"},{"mistake":"Treated needs_approval as failure and retried send","fix":"needsApproval means success parked for human confirm — do not resend"},{"mistake":"REST 4xx had no recovery block so agent freestyled","fix":"Every v1 error includes recovery.suggestedTool + _agent.next — obey it"}],"restErrorEnvelope":{"note":"All /api/v1/* JSON errors use the same shape as MCP soft-errors: ok:false, code, message, hint?, existing?, recovery, _agent. Legacy nested error:{code,message} is still present for older clients.","header":"X-Forge-API-Version: 1"},"mcpIdempotency":{"arg":"idempotencyKey","aliases":["idempotency_key","_meta.idempotencyKey"],"ttlHours":24,"tools":["forge_create_client","forge_create_property","forge_create_quote","forge_create_job","forge_create_invoice","forge_convert_job_to_invoice","forge_agent","playbook_*"],"rule":"Pass the SAME key on timeout retry. Successful responses are replayed; errors are not cached so a fixed retry can succeed."},"errorContract":{"shape":{"ok":false,"code":"DUPLICATE_DETECTED | ALREADY_INVOICED | LOCKED | ...","message":"human readable","hint":"optional","existing":"{ id, jobNumber|invoiceNumber, ... }","recovery":{"suggestedTool":"forge_update_job","suggestedArgs":{"id":"J-0042"},"doNot":["..."]},"_agent":{"next":"Call forge_update_job with ...","lawsUrl":"https://forge.kin.team/api/v1/agent-laws"}},"rule":"On ok:false, execute recovery.suggestedTool with recovery.suggestedArgs. Never freestyle."},"successContract":{"shape":{"ok":true,"_agent":{"next":"what to do if user wants another change","suggestedTools":["forge_update_job"],"doNot":["create again"]}}},"productLaw":{"version":"2.0.0","defaultMode":"balanced","modeSource":"tenants.settings.productLaw.mode | tenants.settings.agent.productLawMode","domains":["client","property","quote","job","invoice"],"balanced":{"resolve":["Reuse client on definitive email/phone match","Link or create property when site address/label is present on quote/job create","Strip site lines from notes after property resolve"],"block":["Duplicate client name (normalized)","Ambiguous multi-client email/phone/name","Duplicate job PO/title","Duplicate quote title (7d)","Second open invoice on same job","Orphan twin invoice (similar notes, 7d, no job)"],"force":"force:true + optional forceReason (audited). Prefer recovery tools over force."},"errorCodes":["DUPLICATE_DETECTED","CLIENT_MATCH_AMBIGUOUS","PROPERTY_REQUIRED","PROPERTY_AMBIGUOUS","SITE_IN_NOTES","ALREADY_INVOICED","OPEN_INVOICE_EXISTS","FORCE_REASON_REQUIRED"]},"discovery":{"laws":"https://forge.kin.team/api/v1/agent-laws","agentGuide":"https://forge.kin.team/api/v1/agent-guide","apiJson":"https://forge.kin.team/.well-known/api.json","openapi":"https://forge.kin.team/api/v1/openapi.yaml","docs":"https://forge.kin.team/docs/KIN_FORGE_TOOLS.md","quickstart":"https://forge.kin.team/docs/INTEGRATION_QUICKSTART.md"},"thinToolHosts":{"warning":"If your platform only registered create tools, agents WILL duplicate records. Minimum viable thin set below — or register only forge_agent.","minimumViable":["forge_agent (preferred single tool)","OR full set: list+get+create+update for clients, jobs, quotes, invoices","invoices also need: forge_revise_invoice, forge_void_invoice, forge_publish_invoice_qb","schedule: forge_list_visits, forge_create_visit, forge_update_visit, forge_complete_visit","payroll: forge_list_timesheets, forge_approve_timesheets, forge_lock_pay_period","receipts: forge_list_receipts, forge_update_receipt, forge_link_receipt, forge_approve_receipt"],"invoiceState":"GET invoice / forge_get_invoice returns state{editable,reopenable,locked,lockedReason,allowedActions} and _links — read before mutating","unknownFields":"UNKNOWN_FIELD on invoice writes (e.g. title) — invoices have no title; use notes/poNumber/lineItems"}}