UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

1,864 lines • 79.6 kB
// 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