@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
1,864 lines • 79.6 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
/**
* Recovery Hints Registry — ADR-149 Phase 2
*
* Per-tool failure-mode declarations. When a tool fails, the hint tells
* the AI what to try next — forming a recovery graph.
*/
import { WORK_TYPE_VALUES } from "./catalog-enums.js";
// The two typed 409s from lib/utils/recording-split-errors.ts. Shared by every
// tool whose write can land on recording_splits: pica_recording_splits_create
// and _update directly, and pica_credits_update / pica_credit_add through an
// `owner` credit. A wrong existing share is fixed in place with
// pica_recording_splits_update or taken out of the total with
// pica_recording_splits_end (2026-10-03, ops issue 9e1abda7 — before, no tool
// could, and the user had to fix it by hand in /inspect).
const RECORDING_SPLIT_REFUSAL_HINTS = [
{
error_code: "RECORDING_SPLIT_EXISTS",
suggestion: "this person's place on the recording for that split_type + territory is already taken (possibly by an ended share) — the row is details.existing_split_id. Change that row with pica_recording_splits_update instead of adding a second one (if it has ended, pass end_date: null to make it count again)",
next_tool: "pica_recording_splits_update",
},
{
error_code: "RECORDING_SPLIT_TOTAL_EXCEEDED",
suggestion: "the active shares already recorded leave no room for this percentage (details.active_total / resulting_total) — list the splits; if one is wrong (e.g. a holder-less 'unknown' row), fix it with pica_recording_splits_update (relink person_id, change percentage) or end it with pica_recording_splits_end, then retry",
next_tool: "pica_recording_splits_list",
},
];
// Refusals from recordingSplitsService.editSplit / endSplit
// (RECORDING_SPLIT_EDIT_ERRORS in lib/services/recording-splits.ts), plus
// SPLIT_LOCKED, the locked-master database guard both writes can hit.
const RECORDING_SPLIT_EDIT_HINTS = [
{
error_code: "SPLIT_NOT_FOUND",
suggestion: "that split_id is not a share on this recording in this organisation — list the recording's splits to find the right id",
next_tool: "pica_recording_splits_list",
},
{
error_code: "PERSON_NOT_FOUND",
suggestion: "person_id is not a person in this organisation — find the right person (or create them) first",
next_tool: "pica_people_query",
},
{
error_code: "PERSON_DELETED",
suggestion: "that person was deleted; when details.merged_into_person_id is set, use that id instead",
next_tool: "pica_people_query",
},
{
error_code: "SPLIT_LOCKED",
suggestion: "this recording's master split is locked — its shares change only through a split change started on the recording (details.page); tell the user, do not retry",
},
];
export const RECOVERY_HINTS = {
// ── billing ──
// pica_billing_pay is risk:"safe", so Rule 10 does not demand a hint here.
// It gets one anyway because its refusals are the ones an agent is most
// likely to misread as "pica is broken": every code below means the money
// question has a different answer, not that the call failed.
pica_billing_pay: [
{
error_code: "nothing_held_for_entity",
suggestion: "nothing is held on that work — check what is actually waiting across the catalogue before offering to pay",
next_tool: "pica_dashboard_briefing",
},
{
error_code: "offer_no_longer_applies",
suggestion: "the offers moved since you last looked — re-read the billing posture and mint against a current offer",
next_tool: "pica_subscription_status",
},
{
error_code: "billing not enabled",
suggestion: "this deployment does not charge, so there is no link to mint and nothing is owed — say so rather than retrying",
next_tool: "pica_subscription_status",
},
],
// ── inspect tools ──
// Primary-entity failures on inspect propagate as hard errors (404 via
// EntityNotFoundError since ADR-198; 400 invalid-id since the 2026-07-18
// works_inspect fail-quiet fix). These hints turn that refusal into a
// recovery step instead of a dead end.
pica_works_inspect: [
{
error_code: "VALIDATION_ERROR",
suggestion: "the id must be a work UUID — look the work up first and retry with its id",
next_tool: "pica_works_query",
},
{
error_code: "NOT_FOUND",
suggestion: "search for the work by title, ISWC, or ISRC",
next_tool: "pica_search_all",
},
],
pica_recordings_inspect: [
{
error_code: "VALIDATION_ERROR",
suggestion: "the id must be a recording UUID — look the recording up first and retry with its id",
next_tool: "pica_recordings_query",
},
{
error_code: "NOT_FOUND",
suggestion: "search for the recording by title or ISRC",
next_tool: "pica_search_all",
},
],
pica_physical_assets_inspect: [
{
error_code: "VALIDATION_ERROR",
suggestion: "the id must be an asset UUID — look the asset up first and retry with its id",
next_tool: "pica_physical_assets_query",
},
{
error_code: "NOT_FOUND",
suggestion: "list the org's physical assets to find the right id",
next_tool: "pica_physical_assets_query",
},
],
// ── destructive tools ──
pica_works_delete: [
{
error_code: "NOT_FOUND",
suggestion: "search for the work by title",
next_tool: "pica_works_query",
},
{
error_code: "HAS_DEPENDENCIES",
suggestion: "check what's linked to this work first",
next_tool: "pica_works_inspect",
},
],
pica_works_bulk_delete: [
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some IDs not found — list your works to verify",
next_tool: "pica_works_query",
},
],
pica_works_bulk_move_to_performances: [
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some IDs not found — list your works to verify",
next_tool: "pica_works_query",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "inspect a sample work to see whether the title/artist fields map cleanly",
next_tool: "pica_works_inspect",
},
],
pica_people_delete: [
{
error_code: "NOT_FOUND",
suggestion: "search for the person by name",
next_tool: "pica_people_query",
},
{
error_code: "PERSON_HAS_CREDITS",
suggestion: "check their credits before removing",
next_tool: "pica_people_inspect",
},
],
pica_people_bulk_delete: [
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some IDs not found — list your people to verify",
next_tool: "pica_people_query",
},
],
// ops 482424b7. Typed refusals (SPLIT_*) come back as structured results;
// these cover the thrown statuses.
pica_people_split_identity: [
{
error_code: "NOT_FOUND",
suggestion: "the person isn't in this catalogue — find the right id",
next_tool: "pica_people_query",
},
{
error_code: "FORBIDDEN",
suggestion: "this role can't change people — ask an owner or admin of the organisation",
},
],
pica_recordings_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list recordings for this work",
next_tool: "pica_recordings_query",
},
],
pica_releases_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list your releases to confirm the id",
next_tool: "pica_releases_query",
},
// A blocked delete: the route answers 409 DELETE_BLOCKED (both its
// tracks guard and the service's blocked outcome carry the code), with
// the tracks named in the message and `blocking` list. Tracks come off
// one at a time with pica_releases_detach_track, then the delete is retried.
{
error_code: "DELETE_BLOCKED",
suggestion: "the release still has tracks (or active share links) — detach each track with pica_releases_detach_track {release_id, disc_number, track_number}, then retry the delete",
next_tool: "pica_releases_detach_track",
},
],
pica_audio_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list audio files to confirm the id",
next_tool: "pica_audio_query",
},
],
pica_documents_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list documents to confirm the id",
next_tool: "pica_documents_query",
},
],
pica_audio_assign: [
{
error_code: "NOT_FOUND",
suggestion: "list audio files or works to confirm the ids",
next_tool: "pica_audio_query",
},
],
pica_audio_bulk_assign_suggested: [
{
// The bulk takes no args, so the realistic failure is a transient
// server error (surfaces as the route's structured INTERNAL_ERROR) —
// after which the honest next step is to see what is still unattached
// and continue per-file.
error_code: "INTERNAL_ERROR",
suggestion: "list the remaining orphan audio (unassigned:true) and attach individually",
next_tool: "pica_audio_query",
},
{
// Body-less transport failures fall through formatError's classifiers
// to the UNKNOWN_ERROR catch-all — same next step.
error_code: "UNKNOWN_ERROR",
suggestion: "list the remaining orphan audio (unassigned:true) and attach individually",
next_tool: "pica_audio_query",
},
],
pica_audio_dismiss_match: [
{
// The route returns a typed 404 code:NOT_FOUND when either id trips the
// audio_match_dismissals FK (no existence pre-check server-side).
error_code: "NOT_FOUND",
suggestion: "list the orphan audio (unassigned:true) to confirm the audio_file_id, and the suggested recording_id from its probable match",
next_tool: "pica_audio_query",
},
],
pica_audio_update: [
{
error_code: "NOT_FOUND",
suggestion: "list audio files to confirm the id",
next_tool: "pica_audio_query",
},
],
pica_audio_bulk_update: [
{
// The whole-request refusal (not an array, empty, over 200 items).
// Per-item problems are not errors — they come back as failed rows.
error_code: "VALIDATION_ERROR",
suggestion: "send items as an array of 1-200 { id, file_type?, stem_label?, version_label?, title? } — split a larger batch",
},
{
// Whole-call 500: the existence read failed before any write, so this
// call applied nothing. (Per-item failures are rows in a 200, not this.)
error_code: "INTERNAL_ERROR",
suggestion: "the batch did not run — list the audio files to confirm their current values, then re-send",
next_tool: "pica_audio_query",
},
],
// ── ADR-259 recording sample provenance ──
pica_recording_samples_add: [
{
error_code: "NOT_FOUND",
suggestion: "check the recording_id is a UUID you own; source and sample_name are required",
next_tool: "pica_recordings_query",
},
],
pica_recording_samples_set_status: [
{
error_code: "NOT_FOUND",
suggestion: "list the recording's samples to get a valid sample_id",
next_tool: "pica_recording_samples_list",
},
],
pica_recording_samples_remove: [
// This key only started matching on 2026-08-24. Until then
// recordingSamplesService.remove() was a bare PostgREST DELETE, so a
// sample_id naming no row produced {success: true} and the tool answered
// "Sample <id> removed" — no error to classify, so NOT_FOUND was never
// asked for. The service now refuses (RecordingSampleNotFoundError) and
// the DELETE route answers 404 "sample not found", which mcp-utils
// formatError classifies as NOT_FOUND via its /\bnot found\b/i branch.
{
error_code: "NOT_FOUND",
suggestion: "list the recording's samples to get a valid sample_id",
next_tool: "pica_recording_samples_list",
},
],
pica_agreements_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list your agreements to find the right one",
next_tool: "pica_agreements_query",
},
{
error_code: "HAS_LINKED_WORKS",
suggestion: "check what works are linked",
next_tool: "pica_agreements_inspect",
},
],
pica_projects_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list your projects",
next_tool: "pica_projects_query",
},
],
pica_merge_duplicates: [
{
error_code: "ENTITY_NOT_FOUND",
suggestion: "check both entities exist",
next_tool: "pica_find_duplicates",
},
{
error_code: "MERGE_CONFLICT",
suggestion: "compare the entities first",
next_tool: "pica_enrichment_compare",
},
],
// ADR-321 §9. A refusal (422 — not ready, parties don't add up) is returned
// as a typed result with its reasons, not thrown, so it gets no hint here;
// these cover the thrown statuses.
pica_rights_propose: [
{
error_code: "NOT_FOUND",
suggestion: "the song (or the recording, for a master rule) isn't in this catalogue — find the right id",
next_tool: "pica_search_all",
},
{
error_code: "FORBIDDEN",
suggestion: "this role can't change the catalogue — ask an owner or admin of the organisation to propose it",
},
],
// ADR-321 Phase 3. A 400/404/409 refusal is returned as a typed result, not
// thrown; these cover the thrown statuses.
pica_rights_record_income: [
{
error_code: "NOT_FOUND",
suggestion: "the song, recording or licensing enquiry isn't in this catalogue — find the right id",
next_tool: "pica_search_all",
},
{
error_code: "FORBIDDEN",
suggestion: "recording income needs finance access — the catalogue's owner can turn it on in settings",
},
],
pica_notes_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list your notes",
next_tool: "pica_notes_query",
},
],
pica_memory_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list your memories",
next_tool: "pica_memory_list",
},
],
pica_physical_assets_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list your assets",
next_tool: "pica_physical_assets_query",
},
],
// 2026-07-04 discoverability audit (Rule 17): pica_agreement_templates_delete,
// pica_producer_agreements_delete, and pica_work_for_hire_delete were retired
// (superseded by the unified pica_agreement_types_* dispatch surface) but
// their hint entries were left behind, each pointing at an equally-retired
// "_list" tool — 3 stale keys, per the ADR-180 Rule 10 audit gap ("only
// checks the covered direction"). Removed rather than repointed.
// ── enrichment tools ──
//
// ADR-154 F13 (2026-04-21): the seven per-source `pica_enrich_work_*`
// and `pica_people_enrich_*` tools have been removed — their
// per-error hints lived here and were removed with them. The
// outcome-shaped resolvers `pica_resolve_work` / `pica_resolve_person`
// own the recovery surface now; NO_MATCH per source is returned in
// the structured `errors[]` receipt instead of via a tool-scoped hint.
// pica_resolve_work CREDIT_INSUFFICIENT hint dropped 2026-04-22 — the only
// hint it carried pointed at pica_credits_balance, which no longer exists
// (billing model is subscription). The resolver's own error receipt is
// authoritative. See docs/MCP_REFINEMENT_LOG.md entry B-001.
// ── mutating tools ──
// ADR-299 Phase 1 Task 9 (corrected 2026-07-28): this entry predated any
// real duplicate-work gate — `DUPLICATE_WORK` was never a code any route
// or service threw (confirmed via repo-wide grep: it appeared nowhere but
// here). The actual gate lives in lib/services/works/crud.ts and throws
// `WORK_ALREADY_EXISTS` — corrected in place, mirroring the identical
// DUPLICATE_ISRC (Task 5) / DUPLICATE_RELEASE (Task 6) fixes.
pica_works_create: [
{
error_code: "WORK_ALREADY_EXISTS",
suggestion: "a work with this title + artist already exists (the message names its id) — if it is the same song, use it or merge; if it is a genuinely different song, retry pica_works_create with disambiguation (a short note such as 'theme' or '2019 song')",
next_tool: "pica_works_query",
},
],
pica_register_set: [
{
error_code: "WORK_ALREADY_EXISTS",
suggestion: "a work with this title + artist already exists — add a version or complete it instead of registering anew",
next_tool: "pica_recordings_create",
},
{
error_code: "VALIDATION_ERROR",
suggestion: `title and artist_name are required; work_type must be one of ${WORK_TYPE_VALUES.join("/")}`,
next_tool: "pica_register_set",
},
],
pica_works_update: [
{
error_code: "NOT_FOUND",
suggestion: "search for the work by title first",
next_tool: "pica_works_query",
},
{
error_code: "WORK_ALREADY_EXISTS",
suggestion: "another work by the same artist already has that title (the message names its id) — if they are the same song, merge them; if they are different songs, set disambiguation (a short note such as 'theme' or '2019 song') on one of them",
next_tool: "pica_works_inspect",
},
{
error_code: "TUNECODE_ALREADY_IN_USE",
suggestion: "another work holds this tunecode (the message names its id, and says so if it is a deleted work) — inspect it, and clear its tunecode first if it is the wrong holder",
next_tool: "pica_works_inspect",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "inspect the work to see the current shape",
next_tool: "pica_works_inspect",
},
],
pica_works_bulk_update: [
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some work IDs not found — list works to verify",
next_tool: "pica_works_query",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "inspect a sample work to see which fields are accepted",
next_tool: "pica_works_inspect",
},
],
pica_works_bulk_link_artist: [
{
error_code: "PERSON_NOT_FOUND",
suggestion: "person_id not in catalog — search by name first",
next_tool: "pica_people_query",
},
{
error_code: "STAGE_NAME_MISMATCH",
suggestion: "artist_name does not match any of the person's stage_names — inspect to see allowed names",
next_tool: "pica_people_inspect",
},
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some work IDs not found — list works to verify",
next_tool: "pica_works_query",
},
],
pica_recordings_bulk_reassign_artist: [
{
error_code: "VALIDATION_ERROR",
suggestion: "provide exactly one selector — recording_ids (non-empty) or from_person_id — and a to_person_id (or null to clear)",
next_tool: "pica_recordings_query",
},
{
error_code: "NOT_FOUND",
suggestion: "to_person_id is not a person in your catalog — resolve the real artist first",
next_tool: "pica_people_query",
},
],
pica_works_bulk_add_collaborator: [
{
error_code: "PERSON_NOT_FOUND",
suggestion: "collaborator.person_id not in catalog — search by name first",
next_tool: "pica_people_query",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "check share_percentage is between 1 and 100",
next_tool: "pica_works_inspect",
},
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some work IDs not found — list works to verify",
next_tool: "pica_works_query",
},
],
pica_work_writers_add: [
{
error_code: "NOT_FOUND",
suggestion: "work_id not in catalog — verify the work exists first",
next_tool: "pica_works_query",
},
{
error_code: "AMBIGUOUS_NAME",
suggestion: "one or more names matched multiple people — search by name and pass the resolved person_id via pica_works_bulk_add_collaborator instead",
next_tool: "pica_people_query",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "names must be a non-empty array of strings",
next_tool: "pica_works_inspect",
},
],
pica_works_bulk_copy_credits: [
{
error_code: "SOURCE_NOT_FOUND",
suggestion: "source_work_id not in catalog — search by title first",
next_tool: "pica_works_query",
},
{
error_code: "SOURCE_HAS_NO_COLLABORATORS",
suggestion: "source work has no collaborators to copy — inspect it to confirm",
next_tool: "pica_works_inspect",
},
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some target work IDs not found — list works to verify",
next_tool: "pica_works_query",
},
],
pica_people_create: [
{
error_code: "DUPLICATE_PERSON",
suggestion: "a similar person already exists — search first",
next_tool: "pica_people_query",
},
{
error_code: "PERSON_EMAIL_EXISTS",
suggestion: "that email is already on a person in this org — find and reuse that record instead of creating a duplicate",
next_tool: "pica_people_query",
},
],
pica_people_update: [
{
error_code: "NOT_FOUND",
suggestion: "search for the person by name first",
next_tool: "pica_people_query",
},
{
error_code: "PERSON_EMAIL_EXISTS",
suggestion: "that email is attached to another person in this org — pick a different email or update that record instead",
next_tool: "pica_people_query",
},
],
pica_people_bulk_update_roles: [
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some person IDs not found — list people to verify",
next_tool: "pica_people_query",
},
],
pica_people_bulk_add_tags: [
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some person IDs not found — list people to verify",
next_tool: "pica_people_query",
},
],
pica_artist_projects: [
{
error_code: "PERSON_NOT_FOUND",
suggestion: "person_id is not in this catalogue — search by name first",
next_tool: "pica_people_query",
},
{
error_code: "NOT_FOUND",
suggestion: "search for the person by name first",
next_tool: "pica_people_query",
},
],
pica_recordings_update: [
{
error_code: "NOT_FOUND",
suggestion: "list recordings to find the right id",
next_tool: "pica_recordings_query",
},
],
pica_credits_update: [
...RECORDING_SPLIT_REFUSAL_HINTS,
{
error_code: "SPLITS_NOT_100",
suggestion: "credit splits must total 100% — check current splits",
next_tool: "pica_credits_list",
},
{
error_code: "PERSON_NOT_FOUND",
suggestion: "person not in catalog — create them first",
next_tool: "pica_people_create",
},
],
pica_recording_credits_update: [
{
error_code: "RECORDING_NOT_FOUND",
suggestion: "list recordings to find the right id",
next_tool: "pica_recordings_query",
},
{
error_code: "PERSON_NOT_FOUND",
suggestion: "person not in catalog — create them first",
next_tool: "pica_people_create",
},
// ADR-264 WS4 — the batch form (credits[]). These two came from the
// standalone pica_recording_credits_bulk_update, removed in 2.99.0.
{
error_code: "VALIDATION_ERROR",
suggestion: "nothing was applied — for a batch, supply exactly one of release_id / recording_ids, every credit needs credited_name + a valid recording role, and caps are 100 recordings / 50 credits / 500 rows; preview with dry_run: true",
next_tool: "pica_recording_credits_update",
},
{
error_code: "ENTITY_NOT_FOUND",
suggestion: "the release or a recording isn't in your catalog — list releases/recordings to find valid ids",
next_tool: "pica_releases_query",
},
],
// ADR-232 atomic credit primitives.
pica_credit_add: [
...RECORDING_SPLIT_REFUSAL_HINTS,
{
error_code: "VALIDATION_ERROR",
suggestion: "the message names the field — share_pct is 0-100, or leave it out (or null) when the share is unknown; credit_type must be one of the listed values",
},
{
error_code: "PERSON_NOT_FOUND",
suggestion: "person not in catalog — create them first",
next_tool: "pica_people_create",
},
{
error_code: "WORK_NOT_FOUND",
suggestion: "list works to find the right id",
next_tool: "pica_works_query",
},
{
error_code: "RECORDING_NOT_FOUND",
suggestion: "list recordings to find the right id",
next_tool: "pica_recordings_query",
},
],
pica_credit_remove: [
{
error_code: "INSUFFICIENT_SCOPE",
suggestion: "credit_id is not in scope — it must belong to this org AND to the entity_id you named; list that entity's credits to find a valid one",
next_tool: "pica_credits_list",
},
],
pica_credit_update: [
{
error_code: "INSUFFICIENT_SCOPE",
suggestion: "credit_id is not a credit on the work_id you named — list that work's credits to find a valid one",
next_tool: "pica_credits_list",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "pass at least one field to change (share_pct / credit_type / credited_name / notes / instrument / person_id); share_pct is 0-100 or null for unknown",
next_tool: "pica_credits_list",
},
{
error_code: "PERSON_NOT_FOUND",
suggestion: "person_id is not a person in this organisation — find the right one, or create the person first",
next_tool: "pica_people_query",
},
{
error_code: "PERSON_DELETED",
suggestion: "that person record was deleted — link the credit to a current person record",
next_tool: "pica_people_query",
},
{
error_code: "PERSON_NAME_MISMATCH",
suggestion: "the credited name and the person's name differ — ask the user whether they are the same person; if yes, retry with confirm_name_mismatch: true",
next_tool: "pica_people_inspect",
},
{
error_code: "CREDIT_PERSON_CONFLICT",
suggestion: "that person already holds this role on the song or recording (existing_credit_id) — keep one credit and remove the other instead of linking a second",
next_tool: "pica_credit_remove",
},
],
pica_recording_splits_create: [
{
error_code: "RECORDING_NOT_FOUND",
suggestion: "list recordings to find the right id",
next_tool: "pica_recordings_query",
},
// The typed 409s from lib/utils/recording-split-errors.ts. These replaced a
// SPLITS_NOT_100 entry nothing emitted, whose next_tool
// (pica_recordings_inspect) has no splits section — an agent following it
// could not see the share that caused the refusal (ops_issue 7215bcdb).
...RECORDING_SPLIT_REFUSAL_HINTS,
],
pica_recording_splits_update: [
...RECORDING_SPLIT_EDIT_HINTS,
// An edit that moves a share onto a person / type / territory already
// held — the create-side wording ("change that row instead of adding")
// would be wrong here.
{
error_code: "RECORDING_SPLIT_EXISTS",
suggestion: "the person / split_type / territory you moved this share onto already holds a share (details.existing_split_id) — change or end that row first, or edit it instead of this one",
next_tool: "pica_recording_splits_list",
},
...RECORDING_SPLIT_REFUSAL_HINTS.filter((h) => h.error_code !== "RECORDING_SPLIT_EXISTS"),
],
pica_recording_splits_end: [
...RECORDING_SPLIT_EDIT_HINTS,
{
error_code: "SPLIT_ALREADY_ENDED",
suggestion: "this share has already ended and no longer counts toward the total — nothing to do; list the splits to see the active ones",
next_tool: "pica_recording_splits_list",
},
],
pica_split_sheet_generate: [
{
error_code: "SPLITS_NOT_100",
suggestion: "fix credits before generating split sheet",
next_tool: "pica_credits_update",
},
],
// 2026-09-30. The forwarded-email reads refuse a member (owners and admins
// only) and answer 404 for an id that is not this organisation's.
pica_import_emails_query: [
{
error_code: "NOT_FOUND",
suggestion: "that email isn't one of this organisation's forwarded emails — list them without an id to find the right one",
next_tool: "pica_import_emails_query",
},
{
error_code: "FORBIDDEN",
suggestion: "forwarded emails are visible to owners and admins only — ask one of them to check",
},
],
pica_import_execute: [
{
error_code: "VALIDATION_ERROR",
suggestion: "validate the import first to see row-level issues",
next_tool: "pica_import_validate",
},
],
pica_import_streaming_link: [
{
error_code: "INVALID_URI",
suggestion: "search for the artist or track first to find a valid streaming URI",
next_tool: "pica_search_all",
},
{
error_code: "STREAMING_LINK_NOT_FOUND",
suggestion: "the provider could not find that link — not a PICA fault; check the link with the user and do not retry the same URL",
next_tool: "pica_search_all",
},
],
pica_recording_splits_verify: [
{
error_code: "VERIFICATION_REQUIRES_HUMAN_SESSION",
suggestion: 'verification records WHICH PERSON attested, so it cannot be done from an API key or agent grant — the split holder confirms it signed in, via "confirm my share" under ownership splits at /inspect/recordings/<recording_id>#ownership-splits; do not retry',
next_tool: "pica_recording_splits_list",
},
{
error_code: "ATTESTING_PERSON_NOT_FOUND",
suggestion: "the signed-in user has no person record in this organisation, so there is nobody to record as attesting — create/link their person record first",
next_tool: "pica_people_query",
},
],
pica_import_youtube_link: [
{
error_code: "INVALID_URI",
suggestion: "search for the artist or track first to find a valid YouTube link",
next_tool: "pica_search_all",
},
{
error_code: "YOUTUBE_LINK_NOT_A_VIDEO",
suggestion: "that was a channel or playlist link, or not a YouTube link at all — ask the user for one video's link and retry with it; do not retry the same URL",
},
{
error_code: "YOUTUBE_VIDEO_NOT_FOUND",
suggestion: "YouTube does not return that video (private, deleted or mistyped) — not a PICA fault; check the link with the user and do not retry the same URL",
},
],
// CREDIT_INSUFFICIENT hints for pica_export_song_registration /
// pica_export_catalog_asset_report / pica_audio_analyze dropped 2026-04-22 —
// all pointed at pica_credits_balance, which no longer exists.
// See docs/MCP_REFINEMENT_LOG.md entry B-001.
pica_export_catalog_csv: [
{
// A work missing the fields a format requires (e.g. PRO needs ISWC +
// at least one writer/collaborator) is caller-correctable, not a 500 —
// inspect completeness to see exactly what to fill in.
error_code: "EXPORT_NOT_READY",
suggestion: "the work is missing fields this format requires — inspect its completeness to see what to add",
next_tool: "pica_works_inspect",
},
{
error_code: "WORK_NOT_FOUND",
suggestion: "no work has that id — search for it by title",
next_tool: "pica_works_query",
},
],
pica_audio_identify: [
{
error_code: "NO_MATCH",
suggestion: "no fingerprint match — enter metadata manually",
next_tool: "pica_works_update",
},
{
// Ops issue 04188f17 — agents passed stale or recording/work ids and
// retried blind. identify takes an AUDIO FILE id; list the real ones.
error_code: "NOT_FOUND",
suggestion: "no audio file has that id — identify takes an AUDIO FILE id, not a recording or work id. list the catalog's audio (filter by work_id/recording_id or unassigned:true) and use the file's own id",
next_tool: "pica_audio_query",
},
],
pica_notify_person: [
{
error_code: "NOT_PICA_USER",
suggestion: "person isn't on PICA — invite them first",
next_tool: "pica_collaborators_invite",
},
],
// ADR-299 Phase 1 Task 5 (2026-07-28): this entry predates any real
// duplicate-ISRC gate — `DUPLICATE_ISRC` was never a code any route or
// service actually threw (confirmed via repo-wide grep: it appeared
// nowhere but here, since the registry's original commit). Corrected to
// the real code (`RECORDING_ALREADY_EXISTS`) now that
// `RecordingsService.create()` genuinely throws it (recordings.ts).
pica_recordings_create: [
{
error_code: "RECORDING_ALREADY_EXISTS",
suggestion: "a recording with this ISRC already exists — query it and link instead of duplicating; pass skip_duplicate_check only for a deliberate second row",
next_tool: "pica_recordings_query",
},
],
// ── agreements stack ──
pica_agreements_create: [
{
error_code: "MISSING_COUNTERPARTY",
suggestion: "look up the counterparty org by name to get its UUID",
next_tool: "pica_search_all",
},
],
pica_agreements_create_from_template: [
{
error_code: "COUNTERPARTY_UNRESOLVED",
suggestion: "none of the required_signers resolved to a named person and no " +
"other_party_name was given — pass other_party_name explicitly, or " +
"verify the signer ids",
next_tool: "pica_people_query",
},
{
error_code: "TEMPLATE_RENDER_FAILED",
suggestion: "the template_id was not found or a required variable was left " +
"blank — query templates (type:template) to confirm the id and " +
"which required_fields it needs, then resend with those filled",
next_tool: "pica_agreement_types_query",
},
],
pica_agreements_update: [
{
error_code: "NOT_FOUND",
suggestion: "list your agreements to find the right id",
next_tool: "pica_agreements_query",
},
],
pica_agreements_link_work: [
{
error_code: "AGREEMENT_NOT_FOUND",
suggestion: "list your agreements first",
next_tool: "pica_agreements_query",
},
{
error_code: "WORK_NOT_FOUND",
suggestion: "search for the work by title",
next_tool: "pica_works_query",
},
],
pica_agreement_source_splits: [
{
error_code: "DISCREPANCY",
suggestion: "the document splits disagree with the system — inspect the work's current credits before retrying",
next_tool: "pica_works_inspect",
},
{
error_code: "AGREEMENT_NOT_FOUND",
suggestion: "list your agreements to find the right id",
next_tool: "pica_agreements_query",
},
{
error_code: "WORK_NOT_FOUND",
suggestion: "search for the work by title",
next_tool: "pica_works_query",
},
],
pica_agreement_templates_duplicate: [
{
error_code: "NOT_FOUND",
suggestion: "list your agreement templates to find the right id",
next_tool: "pica_agreement_types_query",
},
],
pica_agreement_templates_set_default: [
{
error_code: "NOT_FOUND",
suggestion: "list your agreement templates to find the right id",
next_tool: "pica_agreement_types_query",
},
],
// ── releases ──
// ADR-299 Phase 1 Task 6 (corrected 2026-07-28): both entries below
// previously keyed on a phantom `DUPLICATE_RELEASE` code that no route
// or service ever threw (confirmed via repo-wide grep at fix time — it
// appeared nowhere but this file). `releasesService.create()` /
// `createWithTracks()` now genuinely throw `RELEASE_ALREADY_EXISTS` (the
// in-org UPC duplicate gate) — corrected in place rather than left as a
// stale second key, mirroring the identical DUPLICATE_ISRC fix Task 5
// made for recordings.
pica_releases_create: [
{
error_code: "RELEASE_ALREADY_EXISTS",
suggestion: "a release with this UPC already exists — the message carries its id (id: …). Update it with pica_releases_update {id} or attach tracks with pica_releases_attach_track; pass skip_duplicate_check only for a genuinely distinct release.",
next_tool: "pica_releases_update",
},
],
pica_releases_create_with_tracks: [
{
error_code: "RELEASE_ALREADY_EXISTS",
suggestion: "a release with this UPC already exists — the message carries its id (id: …). Update it with pica_releases_update {id} or attach tracks with pica_releases_attach_track; pass skip_duplicate_check only for a genuinely distinct release.",
next_tool: "pica_releases_attach_track",
},
{
error_code: "RECORDING_NOT_FOUND",
suggestion: "one or more track recording IDs not found",
next_tool: "pica_recordings_query",
},
],
pica_releases_update: [
{
error_code: "NOT_FOUND",
suggestion: "list releases to find the right id",
next_tool: "pica_releases_query",
},
{
error_code: "RELEASE_ALREADY_EXISTS",
suggestion: "another release in your catalogue already carries this UPC — inspect it with pica_releases_inspect {existing_release_id, or the id in the message}; if it is the same release, attach this one's tracks there and delete this one, otherwise correct the UPC",
next_tool: "pica_releases_inspect",
},
],
pica_releases_attach_track: [
{
error_code: "RELEASE_NOT_FOUND",
suggestion: "list releases to find the right id",
next_tool: "pica_releases_query",
},
{
error_code: "RECORDING_NOT_FOUND",
suggestion: "search recordings to find the track",
next_tool: "pica_recordings_query",
},
// The identifier_type+value and items[] forms. These two came from
// pica_releases_attach_track_by_identifier and
// pica_releases_bulk_attach_tracks, both removed in 2.99.0.
{
error_code: "NO_MATCH",
suggestion: "no recording matched the identifier — import from streaming first",
next_tool: "pica_import_streaming_link",
},
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some recording IDs not found — verify the track list",
next_tool: "pica_recordings_query",
},
],
pica_releases_attach_recording_with_work: [
{
error_code: "RELEASE_NOT_FOUND",
suggestion: "list releases to find the right id",
next_tool: "pica_releases_query",
},
{
error_code: "RECORDING_NOT_FOUND",
suggestion: "search recordings to find the track",
next_tool: "pica_recordings_query",
},
],
pica_releases_detach_track: [
{
error_code: "NOT_FOUND",
suggestion: "list the tracks on this release",
next_tool: "pica_releases_list_tracks",
},
],
pica_releases_move_track: [
{
error_code: "NOT_FOUND",
suggestion: "list the tracks on this release",
next_tool: "pica_releases_list_tracks",
},
],
pica_releases_reorder_tracks: [
{
error_code: "NOT_FOUND",
suggestion: "list the tracks on this release to see the current order",
next_tool: "pica_releases_list_tracks",
},
],
// ── projects ──
pica_projects_create: [
{
error_code: "DUPLICATE_PROJECT",
suggestion: "a project with this name already exists",
next_tool: "pica_projects_query",
},
],
pica_projects_update: [
{
error_code: "NOT_FOUND",
suggestion: "list your projects to find the right id",
next_tool: "pica_projects_query",
},
],
// ADR-319 — saved catalog selections.
pica_selections_update: [
{
error_code: "NOT_FOUND",
suggestion: "list saved selections to find the right id",
next_tool: "pica_selections_query",
},
],
pica_selections_delete: [
{
error_code: "NOT_FOUND",
suggestion: "list saved selections to find the right id",
next_tool: "pica_selections_query",
},
],
pica_projects_attach_works: [
{
error_code: "PROJECT_NOT_FOUND",
suggestion: "list your projects to find the right id",
next_tool: "pica_projects_query",
},
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "some work IDs not found — search by title",
next_tool: "pica_works_query",
},
],
pica_projects_participants: [
{
error_code: "NOT_FOUND",
suggestion: "list your projects to find the right project_id",
next_tool: "pica_projects_query",
},
{
error_code: "PARTIAL_NOT_FOUND",
suggestion: "the person wasn't found — search people first",
next_tool: "pica_people_query",
},
],
pica_projects_multimedia: [
{
error_code: "NOT_FOUND",
suggestion: "list your projects to find the right project_id",
next_tool: "pica_projects_query",
},
],
pica_bookings_update: [
{
error_code: "NOT_FOUND",
suggestion: "list or search booking enquiries to find the right id",
next_tool: "pica_bookings_query",
},
],
// ADR-313 — live shows and tour credits
pica_setlist_match: [
{
error_code: "NOT_FOUND",
suggestion: "no setlist item with that id in this org — query the show to get its current setlist item ids",
next_tool: "pica_shows_query",
},
],
// ADR-289 Wave C2
pica_memory_update: [
{
error_code: "NOT_FOUND",
suggestion: "search or list memories to find the right id",
next_tool: "pica_memory_search",
},
],
pica_enrichment_clear: [
{
error_code: "NOT_FOUND",
suggestion: "find the work first",
next_tool: "pica_works_query",
},
],
pica_send_cancel: [
{
error_code: "NOT_FOUND",
suggestion: "query pending send hub items to find the right id",
next_tool: "pica_send_query",
},
],
// ── custody ──
// Codes are the typed refusals custodyClaimService.createClaim throws
// (lib/services/custody-claim.ts). The two keys this list carried before
// 2026-09-25 (ALREADY_CLAIMED, ENTITY_NOT_FOUND) were thrown nowhere — every
// refusal was a bare Error that reached the agent as INTERNAL_ERROR.
pica_custody_claim: [
{
error_code: "CUSTODY_ALREADY_HELD",
suggestion: "your catalogue already holds this chain — to record who owns a recording's master (e.g. the artist at 100%), use pica_recording_custody_assign on the recording",
next_tool: "pica_recording_custody_assign",
},
{
error_code: "WORK_NOT_FOUND",
suggestion: "search for the work first",
next_tool: "pica_search_all",
},
{
error_code: "CUSTODY_CLAIM_PENDING",
suggestion: "a claim is already pending for this work and chain",
next_tool: "pica_custody_list",
},
],
// ADR-300 — a zero-result readiness read is almost always an empty
// contributor set, not an empty ledger: consent hangs off credited people,
// so an uncredited catalogue has nobody to have consented.
pica_consent_readiness: [
{
error_code: "NOT_FOUND",
suggestion: "no credited contributors yet — add writer/performer credits first, then re-read readiness",
next_tool: "pica_credits_update",
},
],
pica_custody_respond: [
{
error_code: "NOT_FOUND",
suggestion: "list pending custody claims",
next_tool: "pica_custody_list",
},
],
pica_custody_reconcile: [
{
error_code: "NOT_FOUND",
suggestion: "see the custody history for this entity",
next_tool: "pica_custody_history",
},
],
// Registration integrity floor (spec 2026-07-09, Task 10). Client-side
// refusals (missing confirm, bad gap_code/entity_type, missing reason)
// return isError:true directly and never reach this registry — this
// covers the one server-thrown path: an invalid revoke_waiver_id (not a
// uuid) 400s from the PATCH /admin/integrity/waivers route. The
// duplicate-active-waiver 409 is caught in the tool itself and returned
// as a friendly non-error result, so it never surfaces here either.
pica_integrity_waive: [
{
error_code: "VALIDATION_ERROR",
suggestion: "check gap_code, entity_type, entity_id, reason, and revoke_waiver_id against the tool schema — a malformed revoke_waiver_id is the most common cause",
next_tool: "pica_integrity_waive",
},
],
// ADR-262 — master-side recording custody (the control chain).
pica_recording_custody_assign: [
{
error_code: "ENTITY_NOT_FOUND",
suggestion: "verify the recording exists and belongs to your org",
next_tool: "pica_recordings_query",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "transfer_type must be delivery|license|admin_grant|assignment|full_transfer, percentage must be in (0,100], and one custodian target is required; ownership transfers are refused when a partial percentage is ambiguous or exceeds the transferor's share — review the current trail first",
next_tool: "pica_recording_provenance",
},
],
// ADR-264 WS2 — verify-before-assert for registration gaps.
pica_works_verify_registrations: [
{
error_code: "VALIDATION_ERROR",
suggestion: "work_ids takes 1-25 work UUIDs and mode must be 'check' or 'confirm_unregistered' (society_code only applies to confirm_unregistered); list works to get valid ids",
next_tool: "pica_works_query",
},
{
error_code: "NOT_FOUND",
suggestion: "verify the work ids exist in your catalog",
next_tool: "pica_works_query",
},
],
pica_claim_credit: [
{
error_code: "NOT_FOUND",
suggestion: "list pending discoveries to find the right id",
next_tool: "pica_discoveries_query",
},
],
pica_claim_custody: [
{
error_code: "NOT_FOUND",
suggestion: "list pending discoveries to find the right id",
next_tool: "pica_discoveries_query",
},
],
pica_claim_artist: [
{
error_code: "NOT_FOUND",
suggestion: "list pending discoveries to find the right id",
next_tool: "pica_discoveries_query",
},
],
// ── enrichment proposals ──
pica_enrichment_proposal_apply: [
{
error_code: "NOT_FOUND",
suggestion: "list pending enrichment proposals",
next_tool: "pica_enrichment_proposals_list",
},
{
error_code: "DRIFT_DETECTED",
suggestion: "the entity changed since the proposal was created — review conflicts and retry with force=true",
next_tool: "pica_works_inspect",
},
],
pica_enrichment_proposal_reject: [
{
error_code: "NOT_FOUND",
suggestion: "list pending enrichment proposals",
next_tool: "pica_enrichment_proposals_list",
},
],
// ADR-264 C3: bulk review queue
pica_enrichment_proposals_bulk_apply: [
{
error_code: "VALIDATION_ERROR",
suggestion: "list proposals first to identify ids — bulk-apply takes explicit ids only, and caps at 100 per call",
next_tool: "pica_enrichment_proposals_list",
},
{
// Partial success is the documented normal shape for this tool: each id
// applies independently, so a non-empty `failed` beside a non-zero
// `applied` is not an error to retry wholesale.
error_code: "NOT_FOUND",
suggestion: "ids already applied or rejected come back in failed[] with a reason — re-list to get the still-pending ones rather than replaying the whole batch",
next_tool: "pica_enrichment_proposals_list",
},
],
pica_enrichment_proposals_bulk_reject: [
{
error_code: "VALIDATION_ERROR",
suggestion: "list proposals first to identify ids or use a filter, then bulk-reject",
next_tool: "pica_enrichment_proposals_list",
},
],
// ── multimedia ──
pica_multimedia_link_work: [
{
error_code: "WORK_NOT_FOUND",
suggestion: "search for the work by title",
next_tool: "pica_works_query",
},
{
error_code: "MULTIMEDIA_NOT_FOUND",
suggestion: "search multimedia to find the right id",
next_tool: "pica_multimedia_search",
},
],
pica_multimedia_link_youtube: [
{
error_code: "WORK_NOT_FOUND",
suggestion: "search for the work by title",
next_tool: "pica_works_query",
},
],
// ── physical assets ──
pica_physical_assets_create: [
{
error_code: "DUPLICATE_ASSET",
suggestion: "a similar asset already exists",
next_tool: "pica_physical_assets_query",
},
],
pica_physical_assets_update: [
{
error_code: "NOT_FOUND",
suggestion: "list your assets to find the right id",
next_tool: "pica_physical_assets_query",
},
],
pica_physical_assets_link_work: [
{
error_code: "ASSET_NOT_FOUND",
suggestion: "list your assets to find the right id",
next_tool: "pica_physical_assets_query",
},
{
error_code: "WORK_NOT_FOUND",
suggestion: "search for the work by title",
next_tool: "pica_works_query",
},
],
pica_physical_assets_link_recording: [
{
error_code: "ASSET_NOT_FOUND",
suggestion: "list your assets to find the right id",
next_tool: "pica_physical_assets_query",
},
{
error_code: "RECORDING_NOT_FOUND",
suggestion: "search recordings to find the right id",
next_tool: "pica_recordings_query",
},
],
pica_physical_assets_unlink_work: [
{
error_code: "NOT_FOUND",
suggestion: "inspect the asset to see what's currently linked",
next_tool: "pica_physical_assets_inspect",
},
],
pica_physical_assets_unlink_recording: [
{
error_code: "NOT_FOUND",
suggestion: "inspect the asset to see what's currently linked",
next_tool: "pica_physical_assets_inspect",
},
],
// ── publishers ──
pica_publishers_create: [
{
error_code: "DUPLICATE_PUBLISHER",
suggestion: "a publisher with this name already exists",
next_tool: "pica_publishers_query",
},
],
// ── labels ──
pica_labels_create: [
{
error_code: "VALIDATION_ERROR",
suggestion: "name and org_type ('label' | 'distributor') are required",
next_tool: "pica_labels_query",
},
{
error_code: "INSUFFICIENT_SCOPE",
suggestion: "creating a label organisation needs the write:catalog scope",
next_tool: "pica_labels_query",
},
],
// ── party claims (ADR-268) ──
pica_party_claim_invite: [
{
error_code: "NOT_FOUND",
suggestion: "no organisation with that id — find the party org via pica_labels_query first",
next_tool: "pica_labels_query",
},
{
error_code: "NOT_CLAIMABLE",
suggestion: "only label, distributor, or publisher organisations can be claimed",
next_tool: "pica_labels_query",
},
{
error_code: "ALREADY_CLAIMED",
suggestion: "this organisation is already claimed (verified or has members) — request an org-to-org link instead",
next_tool: "pica_party_claim_status",
},
{
error_code: "DUPLICATE_INVITE",
suggestion: "an open claim invite for this organisation and email already exists",
next_tool: "pica_party_claim_status",
},
{
error_code: "RECIPIENT_DO_NOT_CONTACT",
suggestion: "the recipient has opted out of outreach — do not retry with the same address",
next_tool: "pica_party_claim_status",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "target_organisation_id and a valid invited_email are required",
next_tool: "pica_labels_query",
},
],
// ── collaborators ──
// Claim your profile for assistants (2026-10-03). Refusals of the whole
// call; per-person skips come back inside a successful result instead.
pica_profile_claims_invite: [
{
error_code: "INVITE_REQUIRES_PERSON",
suggestion: "this credential belongs to no person: sending claim invites needs a signed-in owner or admin, or a key or connected app that belongs to one",
next_tool: "pica_profile_claims_status",
},
{
error_code: "INVALID_PERSON_IDS",
suggestion: "person_ids must be person ids from this catalogue",
next_tool: "pica_people_query",
},
{
error_code: "TOO_MANY_PEOPLE",
suggestion: "invite at most 200 people per call; split the list",
next_tool: "pica_profile_claims_invite",
},
{
error_code: "INVALID_EXPECTED_EMAILS",
suggestion: "expected_emails must map each person id to the address the preview showed; preview again and copy them",
next_tool: "pica_profile_claims_invite",
},
],
pica_collaborators_invite: [
{
error_code: "WORK_NOT_FOUND",
suggestion: "search for the work by title first",
next_tool: "pica_works_query",
},
{
error_code: "DUPLICATE_INVITE",
suggestion: "an invite to this email already exists for this work",
next_tool: "pica_collaborators_invites_list",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "pass exactly one of work_id, recording_id, release_id as the invite target",
next_tool: "pica_collaborators_invite",
},
],
pica_collaborators_accept: [
{
error_code: "NOT_FOUND",
suggestion: "list incoming invites to find the right id",
next_tool: "pica_collaborators_incoming_list",
},
],
pica_collaborators_dispute: [
{
error_code: "NOT_FOUND",
suggestion: "list incoming invites to find the right id",
next_tool: "pica_collaborators_incoming_list",
},
],
pica_collaborators_invite_resend: [
{
error_code: "NOT_FOUND",
suggestion: "list outgoing invites to find the right id",
next_tool: "pica_collaborators_invites_list",
},
],
// ADR-236 — code-based recipient claim/decline + sender revoke
pica_collaborators_claim_by_code: [
{
error_code: "invalid_code",
suggestion: "the code must be 8 chars from the 27-char alphabet (A-Z minus I/L/O/U, plus 2/3/4/5/7). re-check what the recipient pasted; hyphen-grouped forms like DKZH-4N7P are accepted.",
},
{
error_code: "invite_no_longer_valid",
suggestion: "the invite was revoked, declined, already accepted, or expired. ask the sender to issue a fresh invite if you still want to accept.",
next_tool: "pica_collaborators_incoming_list",
},
{
error_code: "not_recipient",
suggestion: "your account's workspace_email doesn't match the invite recipient. log in as the email the invite was sent to, or ask the sender to reissue to your current address.",
},
],
pica_collaborators_decline_by_code: [
{
error_code: "invalid_code",
suggestion: "the code must be 8 chars from the 27-char alphabet (A-Z minus I/L/O/U, plus 2/3/4/5/7).",
},
{
error_code: "invite_no_longer_valid",
suggestion: "the invite was already actioned (revoked, declined, accepted, or expired) — no further action needed.",
next_tool: "pica_collaborators_incoming_list",
},
{
error_code: "not_recipient",
suggestion: "your account's workspace_email doesn't match the invite recipient.",
},
],
pica_collaborators_invite_revoke: [
{
error_code: "NOT_FOUND",
suggestion: "the invite id wasn't found in your organisation. list outgoing invites to find the right id.",
next_tool: "pica_collaborators_invites_list",
},
{
error_code: "INVITE_NOT_PENDING",
suggestion: "only pending or counter_proposed invites can be revoked. confirmed/declined/expired invites are already in their terminal state.",
next_tool: "pica_collaborators_invite_status",
},
],
// ADR-238 — agent-mediated counter-proposals
pica_collaborators_counter_propose: [
{
error_code: "invalid_code",
suggestion: "the code must be 8 chars from the 27-char alphabet (A-Z minus I/L/O/U, plus 2/3/4/5/7). re-check what the sender shared.",
},
{
error_code: "not_recipient",
suggestion: "your account's workspace_email doesn't match the invite recipient. log in as the email the invite was sent to.",
},
{
error_code: "invite_no_longer_valid",
suggestion: "the invite was revoked, declined, accepted, or expired — no further action possible. ask the sender for a fresh invite if you still want to negotiate.",
next_tool: "pica_collaborators_incoming_list",
},
{
error_code: "negotiation_limit_reached",
suggestion: "5 counters have already been exchanged on this invite (across both directions). force a final decision by accepting the latest counter, declining the invite outright, or asking the sender to revoke + reissue with new terms.",
next_tool: "pica_collaborators_invite_status",
},
],
pica_collaborators_accept_counter: [
{
error_code: "not_counter_party",
suggestion: "you proposed this counter yourself — you can't accept your own counter. the OTHER party accepts. use pica_collaborators_invite_status({include_history: true}) to see who proposed what.",
next_tool: "pica_collaborators_invite_status",
},
{
error_code: "counter_no_longer_pending",
suggestion: "this counter is already accepted, declined, or superseded by a newer counter. fetch the invite history to see the current state.",
next_tool: "pica_collaborators_invite_status",
},
{
error_code: "invite_no_longer_valid",
suggestion: "the parent invite was revoked or expired — the counter chain is dead.",
next_tool: "pica_collaborators_invite_status",
},
],
pica_collaborators_decline_counter: [
{
error_code: "not_counter_party",
suggestion: "you proposed this counter yourself — you can't decline your own counter. the OTHER party declines. use pica_collaborators_invite_status({include_history: true}) to see who proposed what.",
next_tool: "pica_collaborators_invite_status",
},
{
error_code: "counter_no_longer_pending",
suggestion: "this counter is already accepted, declined, or superseded.",
next_tool: "pica_collaborators_invite_status",
},
{
error_code: "invite_no_longer_valid",
suggestion: "the parent invite was revoked or expired — the counter chain is dead.",
next_tool: "pica_collaborators_invite_status",
},
],
pica_collaborators_counter_back: [
{
error_code: "not_sender",
suggestion: "only the original invite sender can counter-back. if you're the recipient, use pica_collaborators_counter_propose with the invite's claim code instead.",
next_tool: "pica_collaborators_counter_propose",
},
{
error_code: "counter_not_actionable",
suggestion: "this counter is already accepted or has been superseded by a newer counter — there's nothing to counter-back against. fetch the invite history to see the current pending counter (if any).",
next_tool: "pica_collaborators_invite_status",
},
{
error_code: "invite_no_longer_valid",
suggestion: "the parent invite was revoked or expired — the counter chain is dead.",
next_tool: "pica_collaborators_invite_status",
},
{
error_code: "negotiation_limit_reached",
suggestion: "5 counters have already been exchanged on this invite (across both directions). force a final decision by accepting the latest counter or revoking the invite and reissuing with new terms.",
next_tool: "pica_collaborators_invite_status",
},
],
// ── user profile (opt-in public handles + bio + avatar) ──
pica_users_set_handle: [
{
error_code: "HANDLE_TAKEN",
suggestion: "another user already claimed this handle. pick a different one and try again.",
},
{
error_code: "INVALID_HANDLE",
suggestion: "handles must be 3-30 chars, lowercase letters / digits / hyphens / underscores, and start with a letter. punctuation other than - and _ is not allowed.",
},
],
pica_users_clear_handle: [
{
error_code: "NO_HANDLE_SET",
suggestion: "no public handle was set on your profile — nothing to clear. confirm via pica_users_get_my_profile.",
next_tool: "pica_users_get_my_profile",
},
],
pica_users_set_bio: [
{
error_code: "BIO_TOO_LONG",
suggestion: "bio is capped at 500 chars. trim it down and call again, or use pica_users_import_bio_from_person to pull a curated bio from your linked person row.",
next_tool: "pica_users_import_bio_from_person",
},
],
pica_users_set_avatar_url: [
{
error_code: "INVALID_URL",
suggestion: "the avatar URL must be a valid https URL. pass null to clear.",
},
],
pica_users_import_bio_from_person: [
{
error_code: "NO_LINKED_PERSON",
suggestion: "your user_profile is not linked to a person row, so there's no biography to import. write your bio directly via pica_users_set_bio.",
next_tool: "pica_users_set_bio",
},
{
error_code: "would_overwrite",
suggestion: "you already have a bio set. re-call with `overwrite: true` to replace it with the person's biography, or use pica_users_set_bio to edit the current value.",
},
],
// ── agent identity / grants ──
pica_revoke_agent_grant: [
{
error_code: "NOT_FOUND",
suggestion: "list your active agent grants",
next_tool: "pica_list_my_agent_grants",
},
],
// ── groups (member relationships on people, ADR-223) ──
pica_group_add_member: [
{
error_code: "GROUP_NOT_FOUND",
suggestion: "search people for the group person record",
next_tool: "pica_people_query",
},
{
error_code: "ALREADY_MEMBER",
suggestion: "list members to see the existing membership",
next_tool: "pica_group_list_members",
},
],
pica_group_remove_member: [
{
error_code: "NOT_FOUND",
suggestion: "list members of this group to find active rows",
next_tool: "pica_group_list_members",
},
],
// ── ADR-222 sync placements ──
pica_sync_placements_inspect: [
{
error_code: "NOT_FOUND",
suggestion: "placement id may be wrong — list the org's placements",
next_tool: "pica_sync_placements_query",
},
],
pica_sync_placements_create: [
{
error_code: "MISSING_CONTACT",
suggestion: "at least one contact is required — find existing people or supply a person_name to auto-create",
next_tool: "pica_people_query",
},
{
error_code: "INVALID_WORK",
suggestion: "the work_id is invalid or not in this org's catalog",
next_tool: "pica_works_query",
},
],
pica_sync_placements_update: [
{
error_code: "OPERATOR_REQUIRED",
suggestion: "this field is operator-only — for self-service retirement use status:'terminated' instead",
next_tool: "pica_sync_placements_update",
},
{
error_code: "EVIDENCE_REQUIRED",
suggestion: "operator_verified needs at least one document of type sync_contract / cue_sheet / invoice / airing_proof — upload evidence first",
next_tool: "pica_upload_file",
},
{
error_code: "INVALID_TRANSITION",
suggestion: "this status transition is not allowed by the placement state machine — inspect to see the current status and valid next states",
next_tool: "pica_sync_placements_inspect",
},
],
pica_sync_placements_delete: [
{
error_code: "OPERATOR_REQUIRED",
suggestion: "delete is operator-only — for self-service use update with status:'terminated' (soft-delete)",
next_tool: "pica_sync_placements_update",
},
{
error_code: "NOT_FOUND",
suggestion: "placement id may be wrong — list the org's placements",
next_tool: "pica_sync_placements_query",
},
],
pica_sync_placements_cite: [
{
error_code: "NOT_FOUND",
suggestion: "placement id may be wrong — list the org's placements",
next_tool: "pica_sync_placements_query",
},
{
error_code: "VALIDATION_ERROR",
suggestion: "inspect the placement to see the source schema and required fields before retrying",
next_tool: "pica_sync_placements_inspect",
},
],
// ADR-273 — catalog health fix
pica_catalog_health_fix: [
{
error_code: "PLAN_NOT_FOUND",
suggestion: "planToken is missing or expired — run pica_catalog_health_plan again to get a fresh token",
next_tool: "pica_catalog_health_plan",
},
{
error_code: "PLAN_EXPIRED",
suggestion: "the plan token has expired — run pica_catalog_health_plan again to get a fresh plan",
next_tool: "pica_catalog_health_plan",
},
{
error_code: "VALIDATION_ERROR",
suggestion: 'planToken and accept[] are required; accept can be a list of actionIds or the string "all_safe_auto"',
next_tool: "pica_catalog_health_plan",
},
],
// ADR-286 — attributing an unmatched royalty line to a work/recording.
pica_royalties_attribute: [
{
error_code: "NOT_FOUND",
suggestion: "no unmatched royalty line with that id in this org — list the current unmatched lines to get valid ids and their candidate works/recordings",
next_tool: "pica_royalties_unmatched",
},
],
};
/**
* Tools that are intentionally NOT in `RECOVERY_HINTS` despite being
* `mutating` or `destructive`. Read by the lint at
* `scripts/lint-mcp-tools.ts` (ADR-180 Rule 10) — every mutating/
* destructive tool must either appear in `RECOVERY_HINTS` or here.
*
* Three buckets:
*
* `rich_response`: returns a structured response shape that already
* carries the "what next" affordance. The resolver tools populate
* `recovery_hints: string[]` on the response itself; import/export
* tools return a structured download envelope; cascade runners are
* fire-and-forget; the upload pair returns presigned-URL state.
*
* `terminal`: no plausible recovery. One-shot tools, idempotent acks,
* or validation-only writes whose only failure mode is a clear schema
* error the agent can fix from the error message alone.
*
* `dispatcher`: discriminated-branch tool that is a renaming candidate
* per the agreement-types audit. Adding hints to the dispatcher would
* be wasted work if the tools split.
*
* If a tool is added to `RECOVERY_HINTS`, remove it from here. The lint
* emits a stale-skip warning when both are populated for the same tool.
*/
export const INTENTIONALLY_SKIPPED = new Map([
// rich_response
["pica_resolve_work", "rich_response"],
["pica_resolve_person", "rich_response"],
["pica_resolve_recording", "rich_response"],
["pica_resolve_release", "rich_response"],
["pica_run_work_cascade", "rich_response"],
["pica_run_person_cascade", "rich_response"],
// ADR-313 — response already carries matched/unmatched counts, venue
// candidates and contributor proposals; that IS the "what next" affordance.
["pica_log_show", "rich_response"],
["pica_audio_analyze", "rich_response"],
["pica_audio_complete_upload", "rich_response"],
["pica_audio_presigned_upload", "rich_response"],
["pica_documents_analyse", "rich_response"],
["pica_documents_link", "terminal"],
["pica_enrichment_propose", "rich_response"],
["pica_export_ai_consent", "rich_response"],
["pica_export_catalog_asset_report", "rich_response"],
// The four diligence documents, bucketed like every export sibling above:
// each returns a signed-URL envelope ({download_url, expires_at,
// file_size_bytes}) or the structured data inline, so the response itself
// carries the agent's next step and a recovery hint would duplicate it.
["pica_export_diligence_pack", "rich_response"],
["pica_export_industry_ready", "rich_response"],
["pica_export_my_data", "rich_response"],
["pica_export_ownership_record", "rich_response"],
["pica_export_rights_proof", "rich_response"],
["pica_export_snapshot", "rich_response"],
["pica_export_song_dossiers", "rich_response"],
["pica_export_song_registration", "rich_response"],
["pica_ingest_statement", "rich_response"],
["pica_statement_ingest_document", "rich_response"],
// Reports sent/skipped per-signer with a plain-language reason in the
// response text itself — equivalent to a RECOVERY_HINTS table.
["pica_agreements_send_for_signature", "rich_response"],
["pica_report_issue", "rich_response"],
["pica_send_message", "rich_response"],
["pica_send_resend", "rich_response"],
["pica_subscription_manage", "rich_response"],
["pica_upload_complete", "rich_response"],
["pica_upload_file", "rich_response"],
["pica_collaborators_invite_bulk", "rich_response"],
["pica_discoveries_review", "rich_response"],
["pica_multimedia_create", "rich_response"],
["pica_multimedia_import_url", "rich_response"],
// pica_share_send returns structured errors with per-error-code suggestions
// built in via suggestionFor() — equivalent to a RECOVERY_HINTS table.
["pica_share_send", "rich_response"],
["pica_file_deliver", "rich_response"],
["pica_release_notes", "rich_response"],
// terminal
// ADR-319 — its only failure modes are validation (bad predicate clause)
// or a duplicate name, both clear from the error message alone; no id
// exists yet to recover with.
["pica_selections_create", "terminal"],
["pica_acknowledge_onboarding", "terminal"],
["pica_introduce_self", "terminal"],
["pica_outcome_log", "terminal"],
["pica_outcomes_recent", "rich_response"],
["pica_delete_my_account", "terminal"],
["pica_file_revoke_delivery", "terminal"],
["pica_notification_acknowledge", "terminal"],
["pica_notifications_mark_read", "terminal"],
["pica_notify_user", "terminal"],
["pica_notify_user_at", "terminal"],
["pica_notify_user_cancel", "terminal"],
["pica_provenance_certificate", "terminal"],
["pica_provenance_certificate_get", "terminal"],
["pica_sessions_create", "terminal"],
["pica_share_links_create", "terminal"],
["pica_share_links_delete", "terminal"],
["pica_share_links_update", "terminal"],
["pica_split_sheet_send", "terminal"],
["pica_credits_send_for_attestation", "terminal"],
["pica_storage_disconnect", "terminal"],
["pica_submit_feedback", "terminal"],
["pica_submit_license_enquiry", "terminal"],
["pica_telegram_connect", "terminal"],
// ADR-289 Wave C2 — mirrors pica_notify_user / pica_telegram_connect:
// the executor already returns an inline "use pica_telegram_connect
// first" suggestion for the not-connected case, equivalent to a
// RECOVERY_HINTS entry.
["pica_telegram_config", "terminal"],
["pica_update_license_enquiry_status", "terminal"],
["pica_update_my_identity", "terminal"],
["pica_update_organisation_profile", "terminal"],
["pica_update_privacy_settings", "terminal"],
["pica_works_verify", "terminal"],
["pica_works_attest", "terminal"],
["pica_memory_save", "terminal"],
["pica_notes_create", "terminal"],
["pica_create_agent_identity", "terminal"],
["pica_issue_agent_grant", "terminal"],
// dispatcher
["pica_agreement_types_create", "dispatcher"],
["pica_agreement_types_update", "dispatcher"],
["pica_agreement_types_delete", "dispatcher"],
]);
export function getRecoveryHint(toolName, errorCode) {
const hints = RECOVERY_HINTS[toolName];
if (!hints)
return undefined;
return hints.find((h) => h.error_code === errorCode);
}
//# sourceMappingURL=recovery-hints.js.map