{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "name": "ChangePilot Structured Actions",
  "version": "0.3.5",
  "description": "Permission-aware, auditable actions for ChangePilot — the operational workflow graph for industrial and regulated-change teams. This manifest is intended for LLM agents (Claude, ChatGPT, Gemini, Copilot) to discover and invoke ChangePilot capabilities via the live REST API. The MCP server is live at POST /api/mcp/v1 (JSON-RPC 2.0 over HTTP); obtain a token at /api/mcp/v1/auth. Full Anthropic MCP spec (SSE long-lived stream, resource subscriptions) is on the roadmap. Module-aware (v0.3.0): every action declares a `module` slug so agent integrations can scope to specific domain modules (projects, MOC, vendor, training, incidents, compliance) as they ship.",
  "mcp_endpoint": "/api/mcp/v1",
  "mcp_auth_endpoint": "/api/mcp/v1/auth",
  "mcp_auth_tiers": [
    "anon",
    "user"
  ],
  "mcp_protocol": "json-rpc-2.0",
  "a2a_agent_card": "/.well-known/agent.json",
  "a2a_agent_card_canonical": "/.well-known/agent-card.json",
  "a2a_task_endpoint": "/api/agent/task",
  "a2a_protocol_version": "1.0.0",
  "install_hint": {
    "agent_manifest": "/api/install-mcp",
    "human_page": "/install-mcp",
    "one_line_user_instruction": "Tell your agent: 'Install the ChangePilot MCP server. The manifest is at https://changepilot.deltamarksystems.com/api/install-mcp.' The agent will fetch the manifest, pick the right install command for its client, and (with your permission) execute it. No terminal needed."
  },
  "domain": {
    "name": "industrial / regulated change management",
    "verticals": [
      "chemical",
      "pharmaceutical",
      "energy",
      "food",
      "utility",
      "mining"
    ],
    "not_for": [
      "generic SaaS project management",
      "engineering ticket tracking",
      "consumer todos"
    ]
  },
  "modules": [
    {
      "slug": "projects",
      "displayName": "Projects",
      "status": "live",
      "description": "Capture → plan → share → execute. The v1 acquisition surface."
    },
    {
      "slug": "moc",
      "displayName": "Management of Change",
      "status": "hidden",
      "description": "Multi-stage MOC workflow with department reviews, regulatory citations, PSSR routing. Built but hidden behind NEXT_PUBLIC_V1_HIDE_MODULES."
    },
    {
      "slug": "vendor",
      "displayName": "Vendor RFQs",
      "status": "planned",
      "description": "Vendor RFQ + response tracking. Planned."
    },
    {
      "slug": "training",
      "displayName": "Training",
      "status": "planned",
      "description": "Training session + credential tracking. Planned."
    },
    {
      "slug": "incidents",
      "displayName": "Incidents",
      "status": "planned",
      "description": "Incident investigation workflow. Planned."
    },
    {
      "slug": "compliance",
      "displayName": "Compliance Assessments",
      "status": "planned",
      "description": "Standalone compliance assessments + finding management. Planned."
    },
    {
      "slug": "agent",
      "displayName": "Agent Substrate",
      "status": "live",
      "description": "Cross-cutting substrate: webhook subscriptions, staged proposals, MCP-callable read methods. The rails every other module rides on."
    }
  ],
  "actions": [
    {
      "name": "create_change",
      "category": "lifecycle",
      "description": "Submit a brain dump, captured email, or structured task list. ChangePilot segments into work units (projects or tasks), detects entities (equipment, vendors, regulations, permits), surfaces risks, suggests team roles, and emits a structured plan ready for confirmation. Use this when the user describes new work that does not yet exist in ChangePilot.",
      "input_schema": {
        "type": "object",
        "required": [
          "description"
        ],
        "properties": {
          "description": {
            "type": "string",
            "minLength": 3,
            "maxLength": 10000,
            "description": "Free text describing the work. May be a brain dump, email body, pasted task list, or specification. Industrial vocabulary (PSSR, MOC, hot-work permit, vendor names) helps the segmenter."
          },
          "anchor_action_id": {
            "type": "string",
            "format": "uuid",
            "description": "When set, the new content REFINES an existing project rather than creating new ones. Use for scope additions to an existing change."
          }
        }
      },
      "permission_required": "actions.create",
      "side_effects": [
        "Creates a planning_sessions row in phase=triage_input",
        "May create action / action_item rows on confirmation"
      ],
      "human_confirmation": "required_for_terminal_create",
      "rest_endpoint": "POST /api/screener/planning-session (anonymous) or POST /api/planning-sessions (authenticated)",
      "notes": "Returns segmented units + clarifying questions when input is ambiguous. The user (or agent on user's behalf) must confirm structure before rows are materialized.",
      "requires_tier": "anon",
      "module": "projects",
      "mcp_method": "create_change",
      "mcp_endpoint": "/api/mcp/v1",
      "output_hints": {
        "anonymous_next_step": {
          "description": "When tier=anon, create_change includes a `_next_step` field with kind='claim_to_save' + claim_url + instructions_for_user. Agents should surface this to the user. OMITTED for authed callers.",
          "example": {
            "_next_step": {
              "kind": "claim_to_save",
              "claim_url": "https://changepilot.deltamarksystems.com/signup?import_token=<uuid>&ref=mcp_agent",
              "instructions_for_user": "To save this plan, sign up at the link below. Your draft will migrate automatically."
            }
          }
        },
        "unit_classification": {
          "description": "Each segmented unit carries a `work_type` that CHANGES what the system does with it — the agent should surface the consequence, not just the label (surface-parity classification-choice-clarity@mcp). 'project' = a full plan (tasks grouped into phases, with a timeline, risks, and dependencies); 'task' = a single piece of work that CAN carry a flat checklist of subtasks (up to ~15), but with no phase grouping, timeline, or risk decomposition. Re-segment (different create_change input / answer a clarifying question) if the classification is wrong before confirming — once confirmed it materializes rows.",
          "values": {
            "project": "a full plan: tasks grouped into phases with a timeline + risks",
            "task": "one piece of work with an optional flat checklist of subtasks (no timeline)"
          }
        },
        "planner_signals": {
          "description": "create_change returns the planner's internal grounding signals so an agent can interpret + ground its recommendation (surface-parity internal-signal-display@mcp). These are informational — do not treat them as the user's input.",
          "fields": {
            "intensity": "Auto-classified planning depth tier: 'quick' (light touch), 'standard', or 'deep' (full decomposition). Derived from input richness + org history; the agent does NOT set it (see depth-control note).",
            "intensity_signals": "The cues that drove the intensity tier (e.g. multi-step, regulated, high-criticality) — surface these when explaining WHY the plan is shaped as it is.",
            "confidence": "0–1 segmentation confidence. Low confidence usually pairs with clarifying_questions; prefer answering those over confirming a low-confidence segmentation."
          }
        }
      },
      "examples": [
        {
          "title": "Industrial brain dump → multi-unit segmentation",
          "input": {
            "description": "Replace pump P-205 in Q3 — likely needs LOTO, drain, mechanical replacement, hydro test, and OSHA 1910.119 management-of-change review. Sarah needs her PSM cert renewed in July. Also audit our hot-work permit procedure."
          },
          "expected_output_shape": {
            "session_id": "uuid",
            "phase": "triage_review",
            "units": "[3 units: pump replacement | cert renewal | procedure audit]",
            "detected_entities": "P-205 + Sarah + hot-work permit",
            "regulations": "OSHA 1910.119",
            "clarifying_questions": "array, may be empty"
          },
          "notes": "Multi-unit dumps get V2 segmentation; each unit gets its own module_shape_candidate (training_certification / audit_cycle / null)."
        },
        {
          "title": "Refine existing project (anchor_action_id)",
          "input": {
            "description": "Need to add a pre-startup safety review before commissioning. Also vendor delivery slipped 2 weeks — push downstream dependencies.",
            "anchor_action_id": "550e8400-e29b-41d4-a716-446655440000"
          },
          "expected_output_shape": {
            "session_id": "uuid",
            "phase": "triage_review",
            "refine_target": "existing action_id",
            "proposed_changes": "PSSR subtask add + dependency reschedule"
          },
          "notes": "anchor_action_id mode merges into an existing project rather than creating new ones."
        }
      ],
      "error_codes": [
        {
          "code": "rate_limit_exceeded",
          "http_status": 429,
          "description": "Caller exceeded the per-action rate_limit. Retry after Retry-After header."
        },
        {
          "code": "description_too_long",
          "http_status": 400,
          "description": "description exceeded maxLength=10000. Truncate or split into multiple calls."
        },
        {
          "code": "description_too_short",
          "http_status": 400,
          "description": "description below minLength=3. Provide substantive context."
        },
        {
          "code": "anchor_action_not_found",
          "http_status": 404,
          "description": "anchor_action_id does not exist or caller cannot access it (RLS)."
        },
        {
          "code": "anchor_action_archived",
          "http_status": 409,
          "description": "anchor_action_id refers to an archived project. Reopen or create new instead."
        },
        {
          "code": "tier_quota_exhausted",
          "http_status": 402,
          "description": "Free tier active-project cap reached (3 active). Upgrade to Team or close an existing project."
        },
        {
          "code": "llm_provider_unavailable",
          "http_status": 503,
          "description": "LLM backend timed out or returned error. Safe to retry after a brief delay."
        },
        {
          "code": "validation_failed",
          "http_status": 422,
          "description": "Input passed schema but failed semantic validation (e.g. anchor_action_id type mismatch)."
        }
      ],
      "rate_limit": {
        "anonymous": {
          "requests_per_minute": 6,
          "requests_per_day": 12,
          "scope": "per-ip"
        },
        "authenticated": {
          "requests_per_minute": 20,
          "requests_per_day": 200,
          "scope": "per-user"
        },
        "notes": "Anonymous caps are lower per spec — \"a couple freebies per IP via MCP\" (Steve directive 2026-05-14). Authed callers get per-user scope."
      },
      "idempotency_key_supported": false,
      "idempotency_notes": "create_change is NOT idempotent — repeated calls with the same description create N planning_sessions rows. Use anchor_action_id to refine an existing project instead."
    },
    {
      "name": "search_changes",
      "category": "read",
      "description": "Enumerate the caller's organization's projects and tasks with filters + keyset pagination. Use this to LIST work an agent can act on — including terminal (completed/cancelled) projects and pages beyond the first 25, which get_project_status_summary cannot return. Filter by status, work_type, module_shape, free-text title, assignee, and created-after date; page through with the opaque next_cursor. Returns project metadata only (id, title, status, work_type, module_shape, due_date, started_at, created_at) — NEVER notes or description (privacy boundary). Org-scoped via RLS; no cross-tenant access.",
      "input_schema": {
        "type": "object",
        "properties": {
          "status_filter": {
            "type": "array",
            "minItems": 1,
            "maxItems": 7,
            "items": {
              "type": "string",
              "enum": [
                "open",
                "in_progress",
                "overdue",
                "escalated",
                "pending_verification",
                "completed",
                "cancelled"
              ]
            },
            "description": "Statuses to include. Default: non-terminal only (open/in_progress/overdue/escalated/pending_verification). Pass completed/cancelled to include terminal work."
          },
          "work_type": {
            "type": "string",
            "enum": [
              "task",
              "project"
            ],
            "description": "Restrict to a single work_type. Omit for both."
          },
          "module_shape": {
            "type": "string",
            "maxLength": 64,
            "description": "module_shape discriminator filter (e.g. 'moc', 'training', 'incident')."
          },
          "query": {
            "type": "string",
            "maxLength": 200,
            "description": "Case-insensitive substring match on the title."
          },
          "assigned_to_me": {
            "type": "boolean",
            "description": "When true, only items assigned to the calling user."
          },
          "since_date": {
            "type": "string",
            "format": "date-time",
            "description": "Lower bound on created_at (ISO 8601)."
          },
          "cursor": {
            "type": "string",
            "maxLength": 512,
            "description": "Opaque keyset cursor from a prior page's next_cursor. Omit for the first page."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Page size. Default 25."
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "search_changes",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: search_changes)",
      "module": "projects",
      "status": "live",
      "rate_limit": "200 calls/hour per user",
      "idempotency_key_supported": false,
      "idempotency_notes": "Read-only; safe to retry. Pagination is stable via the keyset cursor (created_at,id), so re-fetching a page with the same cursor returns the same rows.",
      "examples": [
        {
          "title": "First page of all non-terminal projects",
          "input": {
            "work_type": "project",
            "limit": 25
          },
          "expected_output_shape": {
            "results": "[{action_id, title, status, work_type, module_shape, due_date, started_at, created_at, link_back_url}]",
            "count": 25,
            "next_cursor": "opaque base64 string or null"
          },
          "notes": "Omitting status_filter defaults to non-terminal statuses. Page again by passing next_cursor."
        },
        {
          "title": "Find completed MOC work assigned to me, page 2",
          "input": {
            "status_filter": [
              "completed"
            ],
            "module_shape": "moc",
            "assigned_to_me": true,
            "cursor": "eyJjIjoiMjAyNi0wNS0wMVQwMDowMDowMFoiLCJpIjoiLi4uIn0="
          },
          "expected_output_shape": {
            "results": "[...]",
            "next_cursor": "null when last page"
          }
        }
      ],
      "error_codes": [
        {
          "code": "SEARCH_CHANGES_INVALID_ARGS",
          "http_status": 400,
          "description": "An argument failed validation (limit out of 1–100, unknown status, malformed since_date, etc.)."
        },
        {
          "code": "SEARCH_CHANGES_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Anonymous tier — search_changes is user-tier only (no org to enumerate)."
        },
        {
          "code": "SEARCH_CHANGES_BAD_CURSOR",
          "http_status": 400,
          "description": "The cursor could not be decoded. Drop it and start from the first page."
        },
        {
          "code": "SEARCH_CHANGES_READ_FAILED",
          "http_status": 502,
          "description": "Upstream enumeration query failed. Safe to retry."
        }
      ]
    },
    {
      "name": "summarize_project",
      "category": "read",
      "description": "Generate an LLM-readable executive summary of a project: title, status, key entities, risks, regulatory context, current blockers, recent activity, next milestone. Optimized for an assistant to paste into a chat or email.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "permission_required": "actions.read",
      "rest_endpoint": "GET /api/actions/{id}/detail-bundle",
      "status": "planned"
    },
    {
      "name": "create_action_item",
      "category": "lifecycle",
      "description": "Add a subtask under a project or task. Permission-checked against project membership. Used when an external source (email decision, meeting note) generates a discrete next step.",
      "input_schema": {
        "type": "object",
        "required": [
          "parent_action_id",
          "title"
        ],
        "properties": {
          "parent_action_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "due_date": {
            "type": "string",
            "format": "date"
          },
          "estimated_days": {
            "type": "integer",
            "minimum": 0
          },
          "assigned_to": {
            "type": "string",
            "format": "uuid"
          },
          "depends_on": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "permission_required": "actions.modify",
      "rest_endpoint": "POST /api/actions/{parent_action_id}/items",
      "status": "planned"
    },
    {
      "name": "update_project_status",
      "category": "lifecycle",
      "description": "Move a project between statuses. Triggers audit-log entry. Terminal transitions (completed / cancelled) require human confirmation per ChangePilot's AI-safety posture.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "status"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "in_progress",
              "overdue",
              "escalated",
              "pending_verification",
              "completed",
              "cancelled"
            ]
          },
          "comment": {
            "type": "string",
            "description": "Optional comment recorded in audit log"
          }
        }
      },
      "permission_required": "actions.modify",
      "human_confirmation": "required_for_terminal_transitions",
      "rest_endpoint": "PATCH /api/actions/{id}",
      "status": "planned"
    },
    {
      "name": "assign_reviewers",
      "category": "routing",
      "description": "Set or change the reviewer / approver list on a project. Permission-checked. Used when an agent recognizes a project needs cross-functional review.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "reviewers"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "reviewers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "user_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "role": {
                  "type": "string",
                  "description": "e.g., 'EHS lead', 'Mechanical supervisor', 'Operations manager'"
                }
              }
            }
          }
        }
      },
      "permission_required": "actions.modify",
      "rest_endpoint": "POST /api/actions/{id}/assign",
      "status": "planned"
    },
    {
      "name": "submit_for_review",
      "category": "routing",
      "description": "Mark a project ready for downstream approval. Triggers reviewer notifications. Agent stages the submission; human confirms the actual submission.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "comment": {
            "type": "string"
          }
        }
      },
      "permission_required": "actions.modify",
      "human_confirmation": "recommended",
      "rest_endpoint": "POST /api/actions/{id}/share",
      "status": "planned"
    },
    {
      "name": "attach_email_summary",
      "category": "capture",
      "description": "Capture a summary of an external email as a comment + attachment on a project. Used when an external assistant (Claude reading user's Gmail, etc.) extracts operationally-relevant content. Permission-checked.\n\nDO NOT paste raw email contents — only the operationally-relevant facts (decisions, changes, deadlines, named entities). The user owns the privacy boundary.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "summary"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "summary": {
            "type": "string",
            "minLength": 10,
            "maxLength": 4000,
            "description": "What was decided / what changed / what is needed. Plain operational facts, not the original message."
          },
          "source_label": {
            "type": "string",
            "description": "Where this came from: 'Email from procurement@abb.com', 'Slack thread #ops', etc."
          },
          "source_timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "permission_required": "actions.modify",
      "rest_endpoint": "POST /api/actions/{action_id}/comments",
      "status": "planned"
    },
    {
      "name": "assess_risk",
      "category": "analysis",
      "description": "Re-run risk + regulatory detection on a project's current state. Returns updated risk_factors and regulatory_context. Use when project content has changed materially since last assessment.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "permission_required": "actions.modify",
      "rest_endpoint": "POST /api/actions/{id}/risk-insights",
      "requires_tier": "user",
      "module": "projects",
      "mcp_method": "assess_risk",
      "mcp_endpoint": "/api/mcp/v1"
    },
    {
      "name": "get_outcome_history",
      "category": "read",
      "description": "Read the org's completed-project outcome history: per-project planned-vs-actual duration and scope drift, the CAPA effectiveness-verification determination (effectiveness_outcome met | partially_met | not_met — did the CHANGE achieve its STATED OBJECTIVE, measured against the change-specific success criterion, with the observed-result evidence), realized risks (which forecasted risks actually fired), and aggregate calibration (median actual duration, on-time rate, on-scope rate, cancellation rate, median estimation drift). Use when the user (or their agent) wants to ground a recommendation in actual historical performance: 'how long do our pump replacements really take?', 'did the change actually achieve its objective?', 'which risks actually fired this quarter?', 'how accurate are our estimates?'. Data is scoped to the caller's org via RLS — no cross-tenant access. Mirrors the substrate the in-product planner uses to calibrate its own segmentation + intensity tiering, so MCP callers get parity with the planner's internal grounding signals (ADR 0012 Moat-Rec A Path 2).",
      "input_schema": {
        "type": "object",
        "properties": {
          "since_date": {
            "type": "string",
            "format": "date-time",
            "description": "Lower bound on completion/cancellation timestamp. Defaults to 6 months ago. Cannot be more than 12 months in the past."
          },
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "When set, scope the result to a single project's outcome (for 'what happened on THIS project' agent queries)."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "Max rows per list. Default 50."
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "output_hints": {
        "effectiveness_summary": {
          "description": "CAPA effectiveness-RECALL summary, giving the agent READ-PARITY with the in-product planner's effectiveness-recall lane. `effectiveness_summary.by_work_type` is keyed by work_type ('task' | 'project'); each entry gives the org's met-rate over verified determinations (met / sample_size; null below the sample floor) plus the recurring NOT-MET failure modes — the structured effectiveness_failure_mode enum value (`mode`), a human-readable explanation (`label`), and how often it recurred (`count`). A failure mode appears ONLY once it RECURS (a one-off is not a pattern). Ground a new plan of the same shape in these: weigh whether each recurring failure mode applies this time and write the verification/controls that address it up front. PRIVACY: derived only from the structured enum + outcome counts — it NEVER contains the free-text effectiveness_evidence / effectiveness_criterion (those stay audit-only).",
          "example": {
            "effectiveness_summary": {
              "by_work_type": {
                "project": {
                  "sample_size": 8,
                  "met_count": 5,
                  "partially_met_count": 1,
                  "not_met_count": 2,
                  "met_rate": 0.625,
                  "not_met_failure_modes": [
                    {
                      "mode": "insufficient_verification",
                      "label": "closed before effectiveness was verified",
                      "count": 2
                    }
                  ]
                },
                "task": {
                  "sample_size": 0,
                  "met_count": 0,
                  "partially_met_count": 0,
                  "not_met_count": 0,
                  "met_rate": null,
                  "not_met_failure_modes": []
                }
              }
            }
          }
        }
      },
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_outcome_history)",
      "module": "projects",
      "mcp_method": "get_outcome_history",
      "mcp_endpoint": "/api/mcp/v1"
    },
    {
      "name": "get_typical_durations",
      "category": "read",
      "description": "Read the org's per-dimension duration medians: overall + by work_type (task | project) + by size (small | medium | large) + by priority (critical | high | medium | low) for projects, plus by category for tasks. Each bucket includes sample_size, median_days, p25_days, p75_days, mean_days, and on_time_rate. Use when the user (or their agent) wants typical-duration grounding for a recommendation: 'how long do small compliance projects usually take?', 'what's our median task duration for maintenance work?'. Data is scoped to the caller's org via RLS — no cross-tenant access. Aggregates only — never returns individual project rows or any free-text content. Sample-size-aware: numerics are null when fewer than min_sample_size rows match a bucket's filter (ADR 0012 Moat-Rec B Path 2 W2).",
      "input_schema": {
        "type": "object",
        "properties": {
          "since_date": {
            "type": "string",
            "format": "date-time",
            "description": "Lower bound on completion timestamp. Defaults to 12 months ago. Cannot be more than 12 months in the past."
          },
          "work_type": {
            "type": "string",
            "enum": [
              "task",
              "project"
            ],
            "description": "Optional filter — restrict project aggregates to a single work_type."
          },
          "category": {
            "type": "string",
            "maxLength": 100,
            "description": "Optional filter — restrict task aggregates to this category (e.g., 'safety', 'compliance', 'maintenance')."
          },
          "min_sample_size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Per-bucket sample-size gate. When fewer than this many rows match a dimension's filter, all numerics in that bucket return null. Default 3."
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_typical_durations",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_typical_durations)",
      "module": "projects"
    },
    {
      "name": "find_similar_projects",
      "category": "read",
      "description": "Cross-project entity recall WITHIN the caller's organization. Given explicit entity references (e.g., pump tag 'P-101A', vendor 'ABB', regulation 'OSHA 1910.119') or free text the planner is processing, returns previously-encountered projects whose project_entities history overlaps. Use when an agent wants to surface 'have we touched this before?' context BEFORE proposing new work: 'your org touched P-101A on 3 prior projects (2025-Q3 seal replacement, 2025-Q4 vendor RFQ, 2026-Q1 PSSR review) — want to attach those?'. Scoring: exact canonical-label match = 1.0; token-bounded substring match = 0.7; Jaccard token overlap > 0.5 = 0.5..0.9 linear. Project rank = sum-of-relevance / num-query-entities (encourages high coverage AND high relevance). Data is scoped to the caller's org via RLS — no cross-tenant access. NOT cross-org learning: only the caller's own project_entities history surfaces. Privacy: returns project metadata only (id, title, status, created_at, completed_at) — never reads notes / description columns. The matched_entities labels are user-provided business data already visible in their UI (ADR 0012 Moat-Rec C Path 2 Week 1).",
      "input_schema": {
        "type": "object",
        "properties": {
          "entity_query": {
            "type": "array",
            "maxItems": 25,
            "items": {
              "type": "object",
              "required": [
                "label"
              ],
              "properties": {
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 240,
                  "description": "Verbatim text the agent saw — 'P-101A', 'ABB', 'OSHA 1910.119'. Case-insensitive matching."
                },
                "entity_type": {
                  "type": "string",
                  "description": "Optional filter — restrict matching to this entity_type (e.g., 'pump', 'vendor', 'regulatory_standard')."
                }
              }
            },
            "description": "Explicit entity references. Both this AND free_text can be present; query entities merge."
          },
          "free_text": {
            "type": "string",
            "maxLength": 100000,
            "description": "Free text the planner is processing. Tier-A deterministic regex extracts tagged equipment / vendor / regulatory references. No LLM call."
          },
          "since_date": {
            "type": "string",
            "format": "date-time",
            "description": "Lower bound on project created_at. Defaults to 12 months ago."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "description": "Max projects returned. Default 10."
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "find_similar_projects",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: find_similar_projects)",
      "module": "projects"
    },
    {
      "name": "get_audit_export",
      "category": "read",
      "description": "Structured RLS-scoped audit-export of a project's full activity history. Returns a structured JSON envelope an agent can summarize for an auditor, regulator, internal-incident reviewer, or contract-dispute stakeholder. Same data sources as the existing audit PDF export (project header + action_items with status timeline + decisions when MOC-linked + activity_events synthesizing comments/inflections/field-change-with-WHY + ai_actions proposals + risk_realizations + regulatory citations + attachment metadata), plus the CAPA effectiveness_verification determination (met | partially_met | not_met — did the change achieve its stated objective, measured against the change-specific success criterion, with observed-result evidence; null when not yet verified). Privacy posture: NEVER reads binary attachment bytes (filenames + mime + size + uploader only — binary flows via storage download URLs). NEVER includes args_hash / before_state / after_state on ai_actions (already hashed for the privacy contract). Each per-list query is hard-capped at 1000 rows; when any cap is hit, a `warning` field surfaces which lists were truncated. User-tier only; cross-org probes return not_found via RLS. Optional include_attachments / include_ai_actions / include_activity flags let an agent shape a leaner envelope. Optional since_date filter time-bounds every list (ADR 0012 Moat-Rec E Path 2 Week 3 — trust-rails demo).",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "The project to export. Caller must have RLS access; cross-org returns not_found."
          },
          "include_attachments": {
            "type": "boolean",
            "description": "Include attachment metadata (filename + mime + size + uploader). NEVER includes bytes. Default true."
          },
          "include_ai_actions": {
            "type": "boolean",
            "description": "Include AI proposals + confirmations from ai_actions (action_kind + timestamps + actor only). Default true."
          },
          "include_activity": {
            "type": "boolean",
            "description": "Include synthesized activity_events (comments + inflections + field-change audit). Default true."
          },
          "include_inline_feedback": {
            "type": "boolean",
            "description": "Include inline FeedbackWidget submissions (user_feedback) whose surface_action_id matches. Default true. Never includes submitter contact / IP."
          },
          "include_agent_action_log": {
            "type": "boolean",
            "description": "Include the agent-contribution custody chain: which agent (agent_id), acting for which human (acting_user_id), over what transport (internal|http|mcp), in what order (occurred_at), with human_confirmed_at on applied proposals. Deduped by invocation_id. Default FALSE (the deeper multi-agent surface). Never includes args_hash."
          },
          "include_integrity_rows": {
            "type": "boolean",
            "description": "Include the RAW seq-ordered organization_change_log rows inside the integrity block so you can recompute the hash chain OFFLINE via POST /api/audit/verify. Default FALSE: the attestation is org-wide, so by default the block ships the verdict + head_hash + counts + seq_range only (not the whole org change log). When true and the chain exceeds the emit cap, chain_valid/reason reflect only the emitted prefix (reason 'partial_export')."
          },
          "since_date": {
            "type": "string",
            "format": "date-time",
            "description": "Lower bound on event timestamps. When set, every per-list query filters out earlier rows."
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_audit_export",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_audit_export)",
      "module": "projects"
    },
    {
      "name": "get_action_template_from_history",
      "category": "read",
      "description": "Distill a TEMPLATE (common subtasks + risks + regs + aggregate stats) from past completed similar projects in the caller's organization. Different from find_similar_projects (which returns individual matching projects): this action aggregates across MULTIPLE past similar projects and returns the recurring structure as a template — what subtasks consistently appear, which risks actually fired, which regulations get cited, how long similar work typically takes. Use this BEFORE create_change when starting a new plan that looks like past work the org has done: 'last 3 pump-seal-replacement projects in your org averaged 14 days, hit 5 subtasks each (kickoff → permits → vendor → install → PSSR), realized risks 2x (hot-work permit expired, vendor late), cited regs OSHA 1910.119 + 1910.147'. Lets the agent pre-fill structure with ORG-SPECIFIC ACTUALS, not LLM hallucination. Matching: extracts query entities from free_text (Tier-A deterministic regex, no LLM call) OR uses explicit entity_query; finds past completed actions (status=completed, work_type filter, completed_at within since_date) sharing >=1 entity. Aggregation: subtasks at >=40% frequency, risks at >=2 firings (from risk_realizations, not planning-time forecasts), citations at >=2 occurrences. Min-sample-size gate (default 3): template returns null + insufficient_data_reason when fewer matching projects found; source_projects list always populated so callers can audit what data exists. Data is scoped to the caller's org via RLS — no cross-tenant access. Privacy: structural data only (subtask titles, risk labels, citation short_labels, aggregate medians) — never reads notes / descriptions / mitigation_applied / relevance / trigger_phrase / ai_metadata. ADR 0012 Moat-Rec D Path 2 Week 3+ — the 'killer demo' Moat-Rec.",
      "input_schema": {
        "type": "object",
        "properties": {
          "free_text": {
            "type": "string",
            "maxLength": 100000,
            "description": "Free text describing the new work being planned. Tier-A deterministic regex extracts query entities (no LLM call)."
          },
          "entity_query": {
            "type": "array",
            "maxItems": 25,
            "items": {
              "type": "object",
              "required": [
                "label"
              ],
              "properties": {
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 240,
                  "description": "Verbatim entity label — 'P-101A', 'centrifugal pump', 'OSHA 1910.119'."
                },
                "entity_type": {
                  "type": "string",
                  "description": "Optional filter — restrict matching to this entity_type."
                }
              }
            },
            "description": "Explicit entity references. Both this AND free_text can be present; query entities merge."
          },
          "vertical": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional vertical hint (e.g. 'chemical', 'pharmaceutical') to narrow the source set."
          },
          "work_type": {
            "type": "string",
            "enum": [
              "task",
              "project"
            ],
            "description": "Work-type filter on source projects. Default 'project'."
          },
          "since_date": {
            "type": "string",
            "format": "date-time",
            "description": "Lower bound on completed_at. Defaults to 18 months ago. Cannot be more than 18 months in the past."
          },
          "min_sample_size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Minimum source-project count for a template to be returned. When the matched source-set is smaller, template is null + insufficient_data_reason is populated. Default 3."
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_action_template_from_history",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_action_template_from_history)",
      "module": "projects"
    },
    {
      "name": "attach_evidence",
      "category": "lifecycle",
      "description": "Attach an external URL or a previously-uploaded storage path to a project (or task) as evidence. Wraps the unified attachments substrate. Use for SharePoint→ChangePilot doc flows, email-attachment ingest, agent-mediated evidence pipelines.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "display_name"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "action_item_id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "External link (http/https)"
          },
          "file_metadata": {
            "type": "object",
            "required": [
              "storage_path",
              "mime_type"
            ],
            "properties": {
              "storage_path": {
                "type": "string"
              },
              "mime_type": {
                "type": "string"
              },
              "file_size_bytes": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "display_name": {
            "type": "string",
            "maxLength": 255
          },
          "description": {
            "type": "string",
            "maxLength": 1024
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "INSERT action_attachments row"
      ],
      "mcp_method": "attach_evidence",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: attach_evidence)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "extract_asset_from_photo",
      "category": "lifecycle",
      "description": "Submit a structured asset suggestion (manufacturer / model / serial / asset_tag / type) extracted from a nameplate photo by the agent's own vision model. STAGES the asset for human approval as an agent proposal — it is NOT written to project_entities until a human approves. On approval it lands on project_entities + bumps the entity_instances org-rollup so the asset compounds across future projects. Path-2 dual-path partner of the in-product POST extract-nameplate route — zero vision cost to ChangePilot when called via MCP.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "asset"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "Project the asset belongs to."
          },
          "action_item_id": {
            "type": "string",
            "format": "uuid",
            "description": "Optional task scope when the asset is task-scoped."
          },
          "asset": {
            "type": "object",
            "required": [
              "label"
            ],
            "properties": {
              "label": {
                "type": "string",
                "maxLength": 200,
                "description": "Tag / identifier (P-101A, Sundyne LMV-322, etc.)"
              },
              "entity_type": {
                "type": "string",
                "enum": [
                  "pump",
                  "motor",
                  "vessel",
                  "tank",
                  "valve",
                  "instrument",
                  "panel",
                  "system",
                  "equipment",
                  "compressor",
                  "controller",
                  "sensor"
                ],
                "description": "Canonical entity_type. Falls back to 'equipment' if absent."
              },
              "manufacturer": {
                "type": "string",
                "maxLength": 200
              },
              "model": {
                "type": "string",
                "maxLength": 200
              },
              "serial_number": {
                "type": "string",
                "maxLength": 200
              },
              "asset_tag": {
                "type": "string",
                "maxLength": 200,
                "description": "Plant tag overlay sticker if visible (often NOT on the original nameplate)."
              },
              "raw_text": {
                "type": "string",
                "maxLength": 4000,
                "description": "All readable nameplate text — audit trail only."
              },
              "confidence": {
                "type": "string",
                "enum": [
                  "high",
                  "medium",
                  "low"
                ]
              }
            }
          },
          "source_photo_path": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional storage_path of the original photo when the agent uploaded it via attach_evidence first."
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "STAGE agent_proposal (projects.import_entities) for human approval",
        "On approval: INSERT project_entities row + UPSERT entity_instances org-rollup"
      ],
      "mcp_method": "extract_asset_from_photo",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: extract_asset_from_photo)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "import_pid_extracted",
      "category": "lifecycle",
      "description": "Submit a structured asset list extracted from a P&ID drawing by the agent's own parser/vision model. STAGES the asset list for human approval as an agent proposal — it is NOT written to project_entities until a human approves. On approval it lands on project_entities + bumps the entity_instances org-rollup so assets compound across future projects. adjacency_hints from explicit equipment-list table connections are stored as JSONB metadata, forward-compatible with future entity_relationships table. Path-2 dual-path partner of the in-product /import pid_drawing shape extractor — zero LLM cost to ChangePilot when called via MCP.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "assets"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "drawing_number": {
            "type": "string",
            "maxLength": 200,
            "description": "Optional title-block drawing number (e.g. P-001-A)."
          },
          "drawing_title": {
            "type": "string",
            "maxLength": 200
          },
          "revision": {
            "type": "string",
            "maxLength": 50
          },
          "assets": {
            "type": "array",
            "maxItems": 500,
            "items": {
              "type": "object",
              "required": [
                "label"
              ],
              "properties": {
                "label": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Primary identifier (P-101A, V-201, FC-301)."
                },
                "entity_type": {
                  "type": "string",
                  "enum": [
                    "pump",
                    "valve",
                    "compressor",
                    "vessel",
                    "tank",
                    "motor",
                    "instrument",
                    "controller",
                    "sensor",
                    "line",
                    "other"
                  ]
                },
                "description": {
                  "type": "string",
                  "maxLength": 200
                },
                "manufacturer": {
                  "type": "string",
                  "maxLength": 200
                },
                "model": {
                  "type": "string",
                  "maxLength": 200
                },
                "size_or_rating": {
                  "type": "string",
                  "maxLength": 200
                },
                "service": {
                  "type": "string",
                  "maxLength": 200
                },
                "location_zone": {
                  "type": "string",
                  "maxLength": 200
                },
                "adjacent_to": {
                  "type": "array",
                  "maxItems": 20,
                  "items": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "description": "Adjacency hints from EXPLICIT equipment-list table feeds_from/feeds_to columns ONLY. Do NOT infer from prose. Forward-compatible with future entity_relationships table."
                },
                "confidence": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                }
              }
            }
          },
          "line_numbers": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Distinct piping line numbers (e.g. 12-PIP-A1-150). Each becomes an equipment entity with asset_type=line metadata."
          },
          "source_drawing_path": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional storage_path for the original P&ID file when uploaded via attach_evidence first."
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "STAGE agent_proposal (projects.import_entities) for human approval",
        "On approval: INSERT N project_entities rows (1 per asset + 1 per line_number) + UPSERT entity_instances org-rollup for each row"
      ],
      "mcp_method": "import_pid_extracted",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: import_pid_extracted)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_upcoming_deadlines",
      "category": "read",
      "description": "Returns project + task deadlines in a calendar-shaped envelope with stable idempotent event_ids. Built for one-way ChangePilot → Outlook/Google Calendar mirroring by an agent.",
      "input_schema": {
        "type": "object",
        "properties": {
          "since_date": {
            "type": "string",
            "format": "date-time"
          },
          "until_date": {
            "type": "string",
            "format": "date-time"
          },
          "assigned_to_me": {
            "type": "boolean"
          },
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_upcoming_deadlines",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_upcoming_deadlines)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_my_assignments",
      "category": "read",
      "description": "Cross-project view of every project + task assigned to the caller. Powers daily-standup, email-digest, and what-do-I-owe-today agent workflows.",
      "input_schema": {
        "type": "object",
        "properties": {
          "include_completed": {
            "type": "boolean",
            "default": false
          },
          "since_date": {
            "type": "string",
            "format": "date-time"
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_my_assignments",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_my_assignments)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_my_recent_calls",
      "category": "read",
      "description": "Returns the caller's own recent agent_action_log rows for self-audit + dedup. Includes the current graph-walk distinct-project counter so an agent can self-throttle below the per-token cap.",
      "input_schema": {
        "type": "object",
        "properties": {
          "minutes_back": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1440,
            "default": 60
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "default": 50
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_my_recent_calls",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_my_recent_calls)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_agent_capabilities",
      "category": "read",
      "description": "Ask-first posture discovery: returns the calling agent's tier + the propose→human-apply custody model + per proposal-kind whether the change can EVER auto-apply (the irreducible floor) or is always human-gated. Pure read, no DB. Call this BEFORE staging writes to plan around what must be human-confirmed. Per-org/user autonomy settings can only TIGHTEN these ceilings, never loosen them.",
      "input_schema": {
        "type": "object",
        "properties": {}
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_agent_capabilities",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_agent_capabilities)",
      "status": "live",
      "rate_limit": "300 calls/hour per user",
      "examples": [
        {
          "title": "Discover posture before staging a write",
          "input": {},
          "expected_output_shape": {
            "tier": "user",
            "can_propose": true,
            "can_apply_requires_human": true,
            "proposal_kinds": [
              {
                "kind": "projects.assign",
                "can_ever_auto_apply": false
              }
            ]
          },
          "notes": "Call this first; a kind with can_ever_auto_apply=false is always human-applied regardless of org settings."
        }
      ],
      "error_codes": [
        {
          "code": "GET_AGENT_CAPABILITIES_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Anonymous tier has no proposal posture — exchange a session JWT for a user-tier token first."
        }
      ],
      "module": "agent"
    },
    {
      "name": "get_org_graph_stats",
      "category": "read",
      "description": "Compounding-data moat made queryable. Returns total entity counts by type, recent growth, top mentioned entities, and project-reuse rate. Read-only aggregates over project_entities.",
      "input_schema": {
        "type": "object",
        "properties": {
          "since_date": {
            "type": "string",
            "format": "date-time"
          },
          "category_filter": {
            "type": "string"
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_org_graph_stats",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_org_graph_stats)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_org_narrative",
      "category": "read",
      "description": "Returns this org's synthesized operating narrative — a short, grounded summary of what the team repeatedly does well and where it repeatedly gets bitten, distilled from its recurring learned patterns (org_learned_patterns) and project retrospectives. Read this to ground new work in the org's compounding memory. Read-only; returns narrative:null when the org has not been synthesized yet.",
      "input_schema": {
        "type": "object",
        "properties": {}
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "examples": [
        {
          "description": "Read the org narrative before planning new work, to ground it in the team's compounding memory.",
          "request": {
            "method": "get_org_narrative",
            "params": {}
          },
          "response": {
            "organization_id": "…",
            "narrative": "The organization consistently delivers seal replacements quickly but repeatedly underestimates permit lead times, leading to schedule slips on regulated work.",
            "generated_at": "2026-06-08T00:00:00Z",
            "source_pattern_count": 8,
            "_source": "changepilot.org_synthesis"
          }
        }
      ],
      "error_codes": [
        "GET_ORG_NARRATIVE_REQUIRES_AUTH — caller is not a user-tier (authenticated) token",
        "GET_ORG_NARRATIVE_REQUIRES_ORG — caller does not belong to an organization",
        "GET_ORG_NARRATIVE_READ_FAILED — the narrative snapshot read failed upstream"
      ],
      "rate_limit": {
        "per_user_per_hour": 100
      },
      "mcp_method": "get_org_narrative",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_org_narrative)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_stakeholder_list",
      "category": "read",
      "description": "Per-project enumeration of stakeholders (creator, project assignee, task assignees, recent commenters) with role + name only. NO contact info — the agent's email tool resolves contacts from its own directory.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_stakeholder_list",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_stakeholder_list)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_workforce_skills",
      "category": "read",
      "description": "Org-scoped workforce skill rollup observed from governed task history (skill name, observation count, confidence, last-observed date), grouped per person. Answers 'who in this org has done this kind of work?' for agent-driven staffing/assignment. Returns id + full_name + role + skills only — NO contact info (the agent's directory tool resolves contacts). Optionally filter by skill_name.",
      "input_schema": {
        "type": "object",
        "properties": {
          "skill_name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 80,
            "description": "Optional case-insensitive substring filter on skill name."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "Max people to return (default 50)."
          }
        }
      },
      "examples": [
        {
          "summary": "Who can do welding work?",
          "input": {
            "skill_name": "weld",
            "limit": 10
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "people": [
              {
                "user_id": "11111111-1111-1111-1111-111111111111",
                "full_name": "Jordan Lee",
                "role": "user",
                "skills": [
                  {
                    "skill_name": "pipe welding",
                    "observation_count": 12,
                    "confidence": 0.8,
                    "last_observed_at": "2026-05-30T00:00:00.000Z"
                  }
                ]
              }
            ],
            "total_people": 1,
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_WORKFORCE_SKILLS_INVALID_ARGS",
        "GET_WORKFORCE_SKILLS_REQUIRES_AUTH",
        "GET_WORKFORCE_SKILLS_REQUIRES_ORG",
        "GET_WORKFORCE_SKILLS_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 100,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_workforce_skills",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_workforce_skills)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_why_history",
      "category": "read",
      "description": "Org-scoped WHY substrate: the reason-for-change behind governed edits, inflection-point lessons, and retrospective narratives (what went wrong / what we learned). Answers 'WHY did this org do or change what it did, and what did it learn?' so the caller's agent can reason WITH the org's owned decision history instead of re-deriving or fabricating it. All text is sanitized and length-capped. Optionally scope to one action/project (action_id), set a time floor (since_iso), or cap per category (limit).",
      "input_schema": {
        "type": "object",
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "Optional project scope: only WHY entries tied to this action/project."
          },
          "since_iso": {
            "type": "string",
            "format": "date-time",
            "description": "Optional ISO-8601 time floor; defaults to each loader's lookback (6–12 months)."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Per-category cap (default 30)."
          }
        }
      },
      "examples": [
        {
          "summary": "Why did the boiler-feed-pump project change scope, and what was learned?",
          "input": {
            "action_id": "22222222-2222-2222-2222-222222222222",
            "limit": 5
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "reason_changes": [
              {
                "field_name": "due_date",
                "entity_type": "action_item",
                "entity_id": "33333333-3333-3333-3333-333333333333",
                "old_value": "2026-05-01",
                "new_value": "2026-05-15",
                "reason_text": "Seal vendor lead time slipped two weeks; rescheduled to keep the outage window.",
                "reason_source": "user",
                "changed_at": "2026-04-20T00:00:00.000Z"
              }
            ],
            "inflection_lessons": [
              {
                "event_type": "schedule_slip",
                "narrative": "ERP showed parts in stock; the floor found none.",
                "lesson_learned": "Verify physical stock before committing the outage date.",
                "occurred_at": "2026-04-22T00:00:00.000Z",
                "action_id": "22222222-2222-2222-2222-222222222222"
              }
            ],
            "outcome_narratives": [
              {
                "what_went_wrong": "Late seal delivery compressed the test window.",
                "what_we_learned": "Pre-stage long-lead spares for critical pumps.",
                "occurred_at": "2026-05-18T00:00:00.000Z",
                "action_id": "22222222-2222-2222-2222-222222222222"
              }
            ],
            "total_entries": 3,
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_WHY_HISTORY_INVALID_ARGS",
        "GET_WHY_HISTORY_REQUIRES_AUTH",
        "GET_WHY_HISTORY_REQUIRES_ORG",
        "GET_WHY_HISTORY_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 100,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_why_history",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_why_history)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "explain_decision",
      "category": "read",
      "description": "Explain ONE decision of record: given its organization_change_log id, returns the captured reasoning (the principal reason + the decision record's context, options weighed, authority, level, expected outcome) AND each authoritative source that justified it — the why-behind-the-why — with every cited source resolved as live (the record exists), unresolved (it was deleted, is cross-org, or its table is a ghost), or soft (a source kind whose backing table isn't built yet). Answers 'why did this specific change happen, on whose authority, and what justified it?' so the caller's agent can audit the judgment instead of trusting an unverifiable claim. provenance_status names the honest verdict (none / soft_only / unresolved / sourced) — a dangling citation is never dressed as authority. Org-scoped (RLS); read-only.",
      "input_schema": {
        "type": "object",
        "properties": {
          "change_log_id": {
            "type": "string",
            "format": "uuid",
            "description": "The organization_change_log id of the decision of record to explain."
          }
        },
        "required": [
          "change_log_id"
        ]
      },
      "examples": [
        {
          "summary": "Why was this MOC's risk tier raised, and what justified it?",
          "input": {
            "change_log_id": "44444444-4444-4444-4444-444444444444"
          },
          "output": {
            "change_log_id": "44444444-4444-4444-4444-444444444444",
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "decision": {
              "entity_type": "action",
              "entity_id": "22222222-2222-2222-2222-222222222222",
              "change_type": "decision",
              "field_name": "risk_tier",
              "old_value": "medium",
              "new_value": "high",
              "changed_by": "11111111-1111-1111-1111-111111111111",
              "occurred_at": "2026-06-20T14:00:00.000Z"
            },
            "reasoning": {
              "reason_text": "Raised to high after a near-identical line failure last quarter.",
              "reason_source": "user",
              "decision_record": {
                "context": "Pump seal replacement on a sour-service line.",
                "options_considered": [
                  "Keep medium with extra monitoring",
                  "Raise to high and require PSSR"
                ],
                "authority": "Operations Manager",
                "decision_level": "manager",
                "expected_outcome": "Mandatory PSSR closes the recurrence path."
              }
            },
            "justifications": [
              {
                "source_kind": "prior_decision",
                "source_id": "55555555-5555-5555-5555-555555555555",
                "source_entity_type": "organization_change_log",
                "note": "The Q1 line-failure MOC that set the precedent.",
                "resolved": true,
                "status": "live",
                "detail": "cited source resolves to a live record"
              }
            ],
            "justification_summary": {
              "total": 1,
              "live": 1,
              "unresolved": 0,
              "soft": 0
            },
            "provenance_status": "sourced",
            "_source": "changepilot.decision_provenance"
          }
        }
      ],
      "error_codes": [
        "EXPLAIN_DECISION_INVALID_ARGS",
        "EXPLAIN_DECISION_REQUIRES_AUTH",
        "EXPLAIN_DECISION_REQUIRES_ORG",
        "EXPLAIN_DECISION_NOT_FOUND",
        "EXPLAIN_DECISION_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 100,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "explain_decision",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: explain_decision)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "explain_pattern",
      "category": "read",
      "description": "Explain ONE learned pattern: given its org_learned_patterns id, returns the persisted source ids resolved back to the project_outcomes that taught the rule — 'which sources taught us this?' (the why-behind-the-why for learned domain logic). Answers 'this rule keeps firing — which past outcomes created it, and when?' so the caller's agent can audit a learned judgment against its real origins instead of trusting an opaque pattern. A pattern with no traced sources yet resolves to an EMPTY sources list (the honest cold answer 'no traced origins yet'), never a fabricated source. Org-scoped (RLS); read-only.",
      "input_schema": {
        "type": "object",
        "properties": {
          "pattern_id": {
            "type": "string",
            "format": "uuid",
            "description": "The org_learned_patterns id of the learned pattern to explain."
          }
        },
        "required": [
          "pattern_id"
        ]
      },
      "examples": [
        {
          "summary": "This 'require PSSR on sour-service seal work' rule keeps firing — what taught us this?",
          "input": {
            "pattern_id": "77777777-7777-7777-7777-777777777777"
          },
          "output": {
            "pattern_id": "77777777-7777-7777-7777-777777777777",
            "lesson": "require-pssr-sour-service-seal",
            "recurrence_count": 3,
            "source_ids": [
              "88888888-8888-8888-8888-888888888888",
              "99999999-9999-9999-9999-999999999999"
            ],
            "sources": [
              {
                "outcome_id": "88888888-8888-8888-8888-888888888888",
                "lesson_labels": [
                  "sour-service seal failure"
                ],
                "captured_at": "2026-03-15T00:00:00.000Z"
              },
              {
                "outcome_id": "99999999-9999-9999-9999-999999999999",
                "lesson_labels": [
                  "PSSR closed recurrence"
                ],
                "captured_at": "2026-04-22T00:00:00.000Z"
              }
            ]
          }
        }
      ],
      "error_codes": [
        "EXPLAIN_PATTERN_INVALID_ARGS",
        "EXPLAIN_PATTERN_REQUIRES_AUTH",
        "EXPLAIN_PATTERN_REQUIRES_ORG",
        "EXPLAIN_PATTERN_NOT_FOUND",
        "EXPLAIN_PATTERN_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 100,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "explain_pattern",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: explain_pattern)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_regulatory_history",
      "category": "read",
      "description": "Org-scoped regulatory-citation recall: the distinct regulations / codes / standards this org has cited on its work, each with a cited_count and a representative relevance note. Answers 'what compliance citations does this org actually use for work like this?' so the caller's agent grounds compliance reasoning in the org's real history instead of fabricating a citation. All text is sanitized and length-capped. Optionally filter by domain, set a time floor (since_iso), or cap the fetch (limit).",
      "input_schema": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "description": "Optional domain filter (e.g. 'process safety', 'electrical'); letters, numbers, spaces, - / & . only."
          },
          "since_iso": {
            "type": "string",
            "format": "date-time",
            "description": "Optional ISO-8601 time floor; defaults to a 6-month lookback."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Max citation rows to fetch from before dedup (default 50)."
          }
        }
      },
      "examples": [
        {
          "summary": "Which regulations does this org cite for process-safety work?",
          "input": {
            "domain": "process safety",
            "limit": 25
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "regulations": [
              {
                "citation": "29 CFR 1910.119 (PSM)",
                "canonical_id": "osha-1910-119",
                "relevance": "Cited on MOCs that alter covered process chemicals or equipment.",
                "cited_count": 7
              }
            ],
            "total_citations": 1,
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_REGULATORY_HISTORY_INVALID_ARGS",
        "GET_REGULATORY_HISTORY_REQUIRES_AUTH",
        "GET_REGULATORY_HISTORY_REQUIRES_ORG",
        "GET_REGULATORY_HISTORY_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 100,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_regulatory_history",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_regulatory_history)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_agent_activity_log",
      "category": "read",
      "description": "Org-WIDE agent custody dossier: every agent CALL (agent_action_log) plus the propose->approve->resolution lifecycle (agent_proposals) across the whole organization for a time window. The conformance-evidence bundle / verifiable record of what the agents did, on whose authority, and how each governed change resolved (applied/rejected/reverted/expired). The org-wide companion to get_audit_export, which is action-scoped. Aggregates custody fields only (no change payloads) plus a proposals-by-status summary. Optionally bound the window (since_iso/until_iso) or cap each list (limit). Read-only, org-scoped. Each agent action also carries an ATTRIBUTION: when the agent acted under a server-verified credential (PAT), verified_owner resolves the real human owner behind it (pat_id, owner_user_id, owner_name, credential_name, credential_revoked) and attribution='verified'; a self-asserted/decorative agent label cannot be tied to an owner so verified_owner=null and attribution='unverified' (never fabricated). Owner resolution is org-scoped + fail-closed (a credential outside this org never resolves). Also runs a READ-ABUSE monitor: flags any agent whose sensitive-read volume (the governed reads that surface a per-org slice of org exhaust) crosses the threshold in the window — the drip-drip exfiltration watch (read_abuse_flags; empty under normal use).",
      "input_schema": {
        "type": "object",
        "properties": {
          "since_iso": {
            "type": "string",
            "format": "date-time",
            "description": "Optional ISO-8601 lower bound (inclusive). Defaults to 90 days ago."
          },
          "until_iso": {
            "type": "string",
            "format": "date-time",
            "description": "Optional ISO-8601 upper bound (exclusive). Defaults to now."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "Per-list cap (default 100)."
          }
        }
      },
      "examples": [
        {
          "summary": "What have agents done org-wide in the last quarter, and how did proposals resolve?",
          "input": {
            "since_iso": "2026-04-01T00:00:00.000Z",
            "limit": 5
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "since_iso": "2026-04-01T00:00:00.000Z",
            "until_iso": "2026-06-16T00:00:00.000Z",
            "agent_actions": [
              {
                "occurred_at": "2026-05-02T10:00:00.000Z",
                "action_name": "propose_change",
                "transport": "mcp",
                "agent_id": "pat:33333333-3333-3333-3333-333333333333",
                "acting_user_id": "11111111-1111-1111-1111-111111111111",
                "target_action_id": "22222222-2222-2222-2222-222222222222",
                "human_confirmed_at": "2026-05-02T11:00:00.000Z",
                "verified_owner": {
                  "pat_id": "33333333-3333-3333-3333-333333333333",
                  "owner_user_id": "11111111-1111-1111-1111-111111111111",
                  "owner_name": "Jordan Operator",
                  "credential_name": "Claude Desktop (Jordan)",
                  "credential_revoked": false
                },
                "attribution": "verified"
              }
            ],
            "proposals": [
              {
                "proposal_kind": "projects.create_change",
                "status": "applied",
                "rationale": "New seal-replacement scope from the forwarded RFQ.",
                "staged_at": "2026-05-02T10:00:00.000Z",
                "resolved_at": "2026-05-02T11:00:00.000Z",
                "resolved_by_user_id": "11111111-1111-1111-1111-111111111111",
                "proposed_by_agent_id": "claude-desktop",
                "target_action_id": "22222222-2222-2222-2222-222222222222"
              }
            ],
            "summary": {
              "total_actions": 1,
              "total_proposals": 1,
              "proposals_by_status": {
                "applied": 1
              },
              "read_abuse_flag_count": 0
            },
            "_source": "changepilot.org_history",
            "read_abuse_flags": []
          }
        }
      ],
      "error_codes": [
        "GET_AGENT_ACTIVITY_LOG_INVALID_ARGS",
        "GET_AGENT_ACTIVITY_LOG_REQUIRES_AUTH",
        "GET_AGENT_ACTIVITY_LOG_REQUIRES_ORG",
        "GET_AGENT_ACTIVITY_LOG_READ_FAILED",
        "GET_AGENT_ACTIVITY_LOG_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 30,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_agent_activity_log",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_agent_activity_log)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "explain_entity",
      "category": "read",
      "description": "Server-owned ENTITY DOSSIER: given a named entity (e.g. an equipment tag, vendor, or person), one call returns its canonical record (mention count + first/last seen), WHERE it appears across the org’s work (the actions that reference it), and the risks that realized on that work. The fan-out the agent would otherwise assemble from several reads, done server-side and org-scoped. Match is by label (case-insensitive); pass entity_type to disambiguate. For deep vendor/person metrics use get_vendor_performance / get_workforce_skills. Read-only, org-scoped.",
      "input_schema": {
        "type": "object",
        "required": [
          "label"
        ],
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "The entity to explain, e.g. \"P-101A\". Case-insensitive exact match."
          },
          "entity_type": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "description": "Optional filter to disambiguate (e.g. \"equipment\" | \"vendor\" | \"person\")."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Per-list cap for appearances + risks (default 25)."
          }
        }
      },
      "examples": [
        {
          "summary": "What do we know about pump P-101A, and what has gone wrong around it?",
          "input": {
            "label": "P-101A",
            "limit": 5
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "found": true,
            "entity": {
              "label": "P-101A",
              "entity_type": "equipment",
              "mention_count": 12,
              "first_seen_at": "2026-01-10T00:00:00.000Z",
              "last_seen_at": "2026-06-01T00:00:00.000Z"
            },
            "appears_in": [
              {
                "action_id": "22222222-2222-2222-2222-222222222222",
                "action_title": "P-101A seal replacement",
                "category": "equipment"
              }
            ],
            "related_risks": [
              {
                "risk_label": "seal_failure",
                "impact_severity": "high",
                "mitigated": true,
                "realized_at": "2026-03-02T00:00:00.000Z",
                "action_id": "22222222-2222-2222-2222-222222222222"
              }
            ],
            "summary": {
              "appearance_count": 1,
              "related_risk_count": 1,
              "truncated": false
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "EXPLAIN_ENTITY_INVALID_ARGS",
        "EXPLAIN_ENTITY_REQUIRES_AUTH",
        "EXPLAIN_ENTITY_REQUIRES_ORG",
        "EXPLAIN_ENTITY_READ_FAILED",
        "EXPLAIN_ENTITY_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "explain_entity",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: explain_entity)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "diagnose_recurrence",
      "category": "read",
      "description": "Server-owned RECURRENCE dossier: pivots on a recurring entity/theme (org_entity_recurrence) to answer \"this keeps happening — how often, across which work, and what did we learn each time?\". Returns the recurrence record (recurrence_count + first/last seen), the inflection LESSONS from its sample actions (what went wrong + what was learned), and the risks realized on them. Distinct from get_why_history (a flat org-wide recent WHY feed) and explain_entity (appearances + risks, no recurrence count/lessons). Match by label (case-insensitive); pass entity_type to disambiguate. Read-only, org-scoped.",
      "input_schema": {
        "type": "object",
        "required": [
          "label"
        ],
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "The recurring entity/theme, e.g. \"seal failure\" or \"P-101A\". Case-insensitive."
          },
          "entity_type": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "description": "Optional filter to disambiguate."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Per-list cap for lessons + risks (default 25)."
          }
        }
      },
      "examples": [
        {
          "summary": "Why does the seal failure keep recurring, and what have we learned?",
          "input": {
            "label": "seal failure",
            "limit": 5
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "found": true,
            "recurrence": {
              "label": "seal failure",
              "entity_type": "risk",
              "recurrence_count": 4,
              "first_seen_at": "2026-01-10T00:00:00.000Z",
              "last_seen_at": "2026-06-01T00:00:00.000Z",
              "sample_action_count": 4
            },
            "lessons": [
              {
                "action_id": "22222222-2222-2222-2222-222222222222",
                "event_type": "delay",
                "narrative": "Seal arrived late from vendor",
                "lesson_learned": "Pre-order long-lead seals at scoping"
              }
            ],
            "related_risks": [
              {
                "risk_label": "seal_failure",
                "impact_severity": "high",
                "mitigated": true,
                "realized_at": "2026-03-02T00:00:00.000Z",
                "action_id": "22222222-2222-2222-2222-222222222222"
              }
            ],
            "summary": {
              "lesson_count": 1,
              "related_risk_count": 1,
              "truncated": false
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "DIAGNOSE_RECURRENCE_INVALID_ARGS",
        "DIAGNOSE_RECURRENCE_REQUIRES_AUTH",
        "DIAGNOSE_RECURRENCE_REQUIRES_ORG",
        "DIAGNOSE_RECURRENCE_READ_FAILED",
        "DIAGNOSE_RECURRENCE_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "diagnose_recurrence",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: diagnose_recurrence)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_learned_scoping_pack",
      "category": "read",
      "description": "Expose the org’s LEARNED interpretation layer so your agent scopes work in the org’s house style, not generic: the effective house VOCABULARY the planner resolves with (declared + derived terms, e.g. \"the big reactor\"->R-301), the org’s learned INTENSITY bias (whether it tends to escalate/de-escalate work thoroughness, from override history, -2..2 with a plain-language reading), and the industry VERTICAL prior. Parity with what the planner itself reads. Read-only, org-scoped, no input required.",
      "input_schema": {
        "type": "object",
        "properties": {
          "vocabulary_limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "Cap on returned vocabulary terms (default 100)."
          }
        }
      },
      "examples": [
        {
          "summary": "What is this org’s house vocabulary and scoping tendency?",
          "input": {
            "vocabulary_limit": 5
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "vertical": "chemical_manufacturing",
            "effective_vocabulary": [
              "R-301",
              "LOTO",
              "MOC",
              "PSSR",
              "the big reactor"
            ],
            "intensity_bias": {
              "value": 1,
              "interpretation": "This org tends to ESCALATE work intensity vs the default (bias +1) — scope work a notch more thoroughly than the generic baseline.",
              "has_signal": true
            },
            "summary": {
              "vocabulary_count": 5
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_LEARNED_SCOPING_PACK_INVALID_ARGS",
        "GET_LEARNED_SCOPING_PACK_REQUIRES_AUTH",
        "GET_LEARNED_SCOPING_PACK_REQUIRES_ORG",
        "GET_LEARNED_SCOPING_PACK_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_learned_scoping_pack",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_learned_scoping_pack)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_calibration_signals",
      "category": "read",
      "description": "Expose the org’s CALIBRATION exhaust so your agent self-corrects against the org’s history instead of repeating known misses: planner DISAGREEMENTS (the planner staged X, the user overrode to Y), confirm-gate CORRECTIONS (edited / refined values), repeatedly-DISMISSED layers (steps the team keeps skipping — a suggest-don’t-drop signal), and recurrence cadence DRIFT (does the org’s ‘weekly’ work actually run weekly?). Parity with what the planner itself learns from. Optionally bound the window (since_iso) or cap each list (limit). Read-only, org-scoped.",
      "input_schema": {
        "type": "object",
        "properties": {
          "since_iso": {
            "type": "string",
            "format": "date-time",
            "description": "Optional ISO-8601 lower bound. Defaults to each signal’s own window (~6-12mo)."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Per-list cap (default 30)."
          }
        }
      },
      "examples": [
        {
          "summary": "What does this org keep overriding or skipping — so I don’t repeat it?",
          "input": {
            "limit": 3
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "since_iso": null,
            "disagreements": [
              {
                "field": "due_date",
                "before_value": "2026-05-01",
                "after_value": "2026-05-15",
                "was_planner_generated": true,
                "changed_at": "2026-04-20T00:00:00.000Z",
                "action_id": "22222222-2222-2222-2222-222222222222",
                "action_item_id": null
              }
            ],
            "confirmation_corrections": [
              {
                "layer": "risk",
                "change_type": "refined",
                "suggested_summary": "3 generic risks",
                "final_summary": "2 site-specific risks",
                "refine_text": "drop the generic ones",
                "corrected_at": "2026-04-21T00:00:00.000Z"
              }
            ],
            "dismissed_steps": [
              {
                "layer": "regulatory",
                "skip_count": 5,
                "example_summaries": [
                  "OSHA 1910.147 citation"
                ],
                "last_skipped_at": "2026-05-02T00:00:00.000Z"
              }
            ],
            "recurrence_drift": [
              {
                "interval": "weekly",
                "cycle_count": 8,
                "avg_drift_days": 2.5,
                "late_rate": 0.75
              }
            ],
            "summary": {
              "disagreement_count": 1,
              "correction_count": 1,
              "dismissed_layer_count": 1,
              "drift_interval_count": 1
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_CALIBRATION_SIGNALS_INVALID_ARGS",
        "GET_CALIBRATION_SIGNALS_REQUIRES_AUTH",
        "GET_CALIBRATION_SIGNALS_REQUIRES_ORG",
        "GET_CALIBRATION_SIGNALS_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_calibration_signals",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_calibration_signals)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_escalation_policy",
      "category": "read",
      "description": "The org’s ACTIVE escalation rules (rule name, description, and time-based escalation delay in hours). An agent proposing or applying work should respect the org’s escalation policy — e.g. \"this org escalates blocked work after 48h.\" Parity with what the planner reads. Read-only, org-scoped.",
      "input_schema": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 25,
            "description": "Cap on returned rules (default 10)."
          }
        }
      },
      "examples": [
        {
          "summary": "What is this org’s escalation policy?",
          "input": {
            "limit": 3
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "rules": [
              {
                "rule_name": "Blocked > 48h",
                "rule_description": "Escalate to the area manager when a task stays blocked beyond two business days.",
                "delay_hours": 48
              }
            ],
            "summary": {
              "rule_count": 1
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_ESCALATION_POLICY_INVALID_ARGS",
        "GET_ESCALATION_POLICY_REQUIRES_AUTH",
        "GET_ESCALATION_POLICY_REQUIRES_ORG",
        "GET_ESCALATION_POLICY_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_escalation_policy",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_escalation_policy)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_realized_risks",
      "category": "read",
      "description": "The org-wide RANKED register of risks that actually MATERIALIZED (risk label, impact severity, whether mitigated, delay days caused) — the prior an agent reads to scope NEW work risk-aware (\"what tends to actually go wrong here\"). Distinct from explain_entity / diagnose_recurrence, which expose per-entity / per-recurrence slices. Optionally bound the window (since_iso), filter a domain, or cap (limit). Read-only, org-scoped.",
      "input_schema": {
        "type": "object",
        "properties": {
          "since_iso": {
            "type": "string",
            "format": "date-time",
            "description": "Optional ISO-8601 lower bound (default ~6mo)."
          },
          "domain": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "description": "Optional domain filter (e.g. \"cooling\"); untagged rows always included."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "Cap (default 50)."
          }
        }
      },
      "examples": [
        {
          "summary": "What risks actually materialize for this org?",
          "input": {
            "limit": 3
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "since_iso": null,
            "risks": [
              {
                "risk_label": "seal_failure",
                "impact_severity": "high",
                "realized_at": "2026-03-02T00:00:00.000Z",
                "mitigated": true,
                "delay_days_caused": 5
              }
            ],
            "summary": {
              "risk_count": 1
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_REALIZED_RISKS_INVALID_ARGS",
        "GET_REALIZED_RISKS_REQUIRES_AUTH",
        "GET_REALIZED_RISKS_REQUIRES_ORG",
        "GET_REALIZED_RISKS_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_realized_risks",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_realized_risks)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_cascade_suggestions",
      "category": "read",
      "description": "Cross-module cascade SUGGESTIONS for one approved parent change (Q11 inc2, suggest-only). Runs a deterministic engine over the parent's fields and returns the downstream steps in OTHER modules it actually implies — e.g. a permanent process change implies operator re-training (Training) + vendor notification (Vendor RFQ); an equipment change implies a preventive-maintenance-schedule update. Read-only: NO writes, NO auto-staging. Each suggestion is directly usable as a propose_change unit — a HUMAN stages a chosen one through propose_change (carrying cascade_parent_action_id). Org-scoped.",
      "input_schema": {
        "type": "object",
        "properties": {
          "parent_action_id": {
            "type": "string",
            "format": "uuid",
            "description": "The approved parent action to derive cross-module cascade suggestions from."
          }
        },
        "required": [
          "parent_action_id"
        ]
      },
      "examples": [
        {
          "summary": "What downstream changes does this approved permanent process change imply?",
          "input": {
            "parent_action_id": "11111111-1111-1111-1111-111111111111"
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "parent_action_id": "11111111-1111-1111-1111-111111111111",
            "parent_found": true,
            "parent_module_shape": "moc",
            "suggestions": [
              {
                "target_module_shape": "training_certification",
                "suggested_kind": "training_update",
                "suggested_title": "Re-train affected operators on: Reactor feed-rate procedure change",
                "rationale": "Because the parent change \"Reactor feed-rate procedure change\" is a permanent process change, affected operators need to be re-trained on the new way of working before it takes effect.",
                "cascade_parent_action_id": "11111111-1111-1111-1111-111111111111"
              }
            ],
            "summary": {
              "suggestion_count": 1
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_CASCADE_SUGGESTIONS_INVALID_ARGS",
        "GET_CASCADE_SUGGESTIONS_REQUIRES_AUTH",
        "GET_CASCADE_SUGGESTIONS_REQUIRES_ORG",
        "GET_CASCADE_SUGGESTIONS_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_cascade_suggestions",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_cascade_suggestions)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_eligible_approvers",
      "category": "read",
      "description": "The org members who may APPROVE an agent proposal (role grants actions.approve_agent_proposals — reviewer+). An agent can propose but otherwise cannot discover WHO should approve; this lets it route a staged change to a legitimate human (separation of duties: recorder≠actor) instead of dumping it into a flat queue. Returns id + name + role only (no contact PII). Read-only, org-scoped.",
      "input_schema": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "Cap on returned approvers (default 50)."
          }
        }
      },
      "examples": [
        {
          "summary": "Who can approve the change I just staged?",
          "input": {
            "limit": 5
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "approvers": [
              {
                "user_id": "11111111-1111-1111-1111-111111111111",
                "name": "Dana Reyes",
                "role": "reviewer"
              }
            ],
            "summary": {
              "approver_count": 1
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_ELIGIBLE_APPROVERS_INVALID_ARGS",
        "GET_ELIGIBLE_APPROVERS_REQUIRES_AUTH",
        "GET_ELIGIBLE_APPROVERS_REQUIRES_ORG",
        "GET_ELIGIBLE_APPROVERS_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_eligible_approvers",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_eligible_approvers)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_authority_timeline",
      "category": "read",
      "description": "Reconstruct who held APPROVE-authority at a past time. get_eligible_approvers says who can approve NOW; this replays the org role-change history (organization_change_log) into a chronological grant/revoke timeline, flagging when approve-authority (reviewer+) was gained or lost. The temporal/custody companion — permissions are mutable, so proving a past approval was legitimate needs the grant as it stood THEN. Filter to one user_id or the whole org; bound with since_iso. Read-only, org-scoped.",
      "input_schema": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string",
            "maxLength": 64,
            "description": "Restrict to one user's role history; omit for the whole org."
          },
          "since_iso": {
            "type": "string",
            "format": "date-time",
            "description": "Optional ISO-8601 lower bound on events."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "description": "Cap (default 100)."
          }
        }
      },
      "examples": [
        {
          "summary": "When did this user gain/lose approve-authority?",
          "input": {
            "user_id": "11111111-1111-1111-1111-111111111111"
          },
          "output": {
            "organization_id": "00000000-0000-0000-0000-000000000000",
            "user_id": "11111111-1111-1111-1111-111111111111",
            "events": [
              {
                "user_id": "11111111-1111-1111-1111-111111111111",
                "changed_at": "2026-03-01T00:00:00.000Z",
                "changed_by": "00000000-0000-0000-0000-0000000000aa",
                "from_role": "user",
                "to_role": "reviewer",
                "gained_approve_authority": true,
                "lost_approve_authority": false
              }
            ],
            "summary": {
              "event_count": 1,
              "authority_grants": 1,
              "authority_revocations": 0
            },
            "_source": "changepilot.org_history"
          }
        }
      ],
      "error_codes": [
        "GET_AUTHORITY_TIMELINE_INVALID_ARGS",
        "GET_AUTHORITY_TIMELINE_REQUIRES_AUTH",
        "GET_AUTHORITY_TIMELINE_REQUIRES_ORG",
        "GET_AUTHORITY_TIMELINE_READ_FAILED",
        "GET_AUTHORITY_TIMELINE_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "scope": "user"
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_authority_timeline",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_authority_timeline)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_vendor_performance",
      "category": "read",
      "description": "Org-scoped vendor metrics aggregated from project_entities + project_outcomes: engagement count, on-time rate, median lead time, last engagement date. Sample-size-gated. Cross-org vendor data is reserved as Wave 3 moat data.",
      "input_schema": {
        "type": "object",
        "properties": {
          "vendor_label": {
            "type": "string"
          },
          "since_date": {
            "type": "string",
            "format": "date-time"
          },
          "min_sample_size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "default": 2
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 25
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_vendor_performance",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_vendor_performance)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "get_required_procedures",
      "category": "read",
      "description": "Theme-A proactive governance inject: returns THIS org's mandatory procedures that apply to a described piece of work (the org's governed 'we always do X for this kind of change' steps) so an agent includes them BEFORE proposing, instead of being corrected at the gate. Each procedure carries a graduated enforcement — 'flag' (advisory) or 'block' (irreversible: the resulting action MUST be staged through propose_*/apply_proposal for human approval). Read-only; org-scoped.",
      "input_schema": {
        "type": "object",
        "required": [
          "work_description"
        ],
        "properties": {
          "work_description": {
            "type": "string",
            "minLength": 3,
            "maxLength": 2000,
            "description": "A description of the work the agent is about to plan or perform (the action-class signal matched against the org's procedures)."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 20
          }
        }
      },
      "examples": [
        {
          "summary": "Ask what the org requires before a valve replacement",
          "input": {
            "work_description": "Replace control valve CV-22 on the reactor feed line"
          },
          "output": {
            "organization_id": "org_…",
            "required_procedures": [
              {
                "id": "proc_…",
                "action_class": "valve_replacement",
                "requirement_title": "Witnessed pressure test before return to service",
                "requirement_description": "This org always runs a witnessed pressure test after a valve replacement.",
                "enforcement": "block",
                "reversible": false
              }
            ],
            "total": 1,
            "_source": "changepilot.org_governance"
          }
        }
      ],
      "error_codes": [
        "GET_REQUIRED_PROCEDURES_INVALID_ARGS",
        "GET_REQUIRED_PROCEDURES_REQUIRES_AUTH",
        "GET_REQUIRED_PROCEDURES_REQUIRES_ORG",
        "GET_REQUIRED_PROCEDURES_UPSTREAM_FAILED"
      ],
      "rate_limit": {
        "per_user_per_hour": 120
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_required_procedures",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_required_procedures)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "propose_task_status_update",
      "module": "projects",
      "category": "lifecycle",
      "description": "STAGED write per ADR 0012: agents propose, humans apply. Stages a proposal to change a task's status (pending/in_progress/done/blocked/skipped). Returns proposal_id + apply_url + reject_url. The actual mutation only happens when an authorized human POSTs the apply_url. Emits 'agent.proposal.staged' webhook on stage, 'agent.proposal.applied' on apply.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_item_id",
          "new_status"
        ],
        "properties": {
          "action_item_id": {
            "type": "string",
            "format": "uuid"
          },
          "new_status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "done",
              "blocked",
              "skipped"
            ]
          },
          "rationale": {
            "type": "string",
            "maxLength": 2000,
            "description": "Shown to the human reviewer when they decide to apply or reject. REQUIRED for agent callers: this is a high-stakes JUDGMENT proposal, so an agent (token-authenticated) must explain WHY — a missing/blank rationale returns PROPOSE_RATIONALE_REQUIRED. Human-driven callers are exempt."
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 168
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "INSERT agent_proposals row (pending)",
        "EMIT agent.proposal.staged webhook"
      ],
      "mcp_method": "propose_task_status_update",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: propose_task_status_update)",
      "status": "live"
    },
    {
      "name": "propose_due_date_update",
      "module": "projects",
      "category": "lifecycle",
      "description": "STAGED write per ADR 0012. Proposes changing a project's due_date OR a task's end_date. Exactly one of action_id (project) or action_item_id (task) must be provided. Pass new_due_date=null to clear.",
      "input_schema": {
        "type": "object",
        "required": [
          "new_due_date"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "Project-level scope. Exactly one of action_id / action_item_id required."
          },
          "action_item_id": {
            "type": "string",
            "format": "uuid",
            "description": "Task-level scope."
          },
          "new_due_date": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "YYYY-MM-DD or null to clear."
          },
          "rationale": {
            "type": "string",
            "maxLength": 2000
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 168
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "INSERT agent_proposals row (pending)",
        "EMIT agent.proposal.staged webhook"
      ],
      "mcp_method": "propose_due_date_update",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: propose_due_date_update)",
      "status": "live"
    },
    {
      "name": "propose_add_comment",
      "module": "projects",
      "category": "lifecycle",
      "description": "STAGED write per ADR 0012. Proposes adding a comment to a project. Content is XSS-sanitized at stage time; the actual comment_type='agent_proposal' record only lands when the human applies.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "content"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "content": {
            "type": "string",
            "minLength": 3,
            "maxLength": 10000
          },
          "rationale": {
            "type": "string",
            "maxLength": 2000
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 168
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "INSERT agent_proposals row (pending)",
        "EMIT agent.proposal.staged webhook"
      ],
      "mcp_method": "propose_add_comment",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: propose_add_comment)",
      "status": "live"
    },
    {
      "name": "propose_assignment",
      "module": "projects",
      "category": "lifecycle",
      "description": "STAGED write per ADR 0012. Proposes REASSIGNING a task to a teammate (sets action_items.assigned_to). Pass new_assignee_user_id=null to clear the assignee. Reassigning who-owns-work always requires human confirmation — this proposal kind NEVER auto-applies regardless of org autonomy settings (the irreducible safety floor). Returns proposal_id + apply_url + reject_url; the mutation only lands when an authorized human applies it. Emits 'agent.proposal.staged' on stage, 'projects.action_item.assigned' on apply.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_item_id",
          "new_assignee_user_id"
        ],
        "properties": {
          "action_item_id": {
            "type": "string",
            "format": "uuid",
            "description": "The task to reassign."
          },
          "new_assignee_user_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "User to assign the task to, or null to clear the assignee."
          },
          "rationale": {
            "type": "string",
            "maxLength": 2000,
            "description": "Shown to the human reviewer when they decide to apply or reject. REQUIRED for agent callers: this is a high-stakes JUDGMENT proposal, so an agent (token-authenticated) must explain WHY — a missing/blank rationale returns PROPOSE_RATIONALE_REQUIRED. Human-driven callers are exempt."
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 168
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "INSERT agent_proposals row (pending)",
        "EMIT agent.proposal.staged webhook"
      ],
      "mcp_method": "propose_assignment",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: propose_assignment)",
      "status": "live",
      "rate_limit": "60 calls/hour per user",
      "examples": [
        {
          "title": "Propose reassigning a task to a teammate",
          "input": {
            "action_item_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
            "new_assignee_user_id": "22222222-2222-2222-2222-222222222222",
            "rationale": "Sarah owns the PSM cert work; routing this seal-replacement subtask to her."
          },
          "expected_output_shape": {
            "proposal_id": "uuid",
            "status": "pending",
            "apply_url": "/api/agent-proposals/<id>/apply",
            "requires_human_confirmation": true
          },
          "notes": "Always returns status:pending — projects.assign never auto-applies."
        }
      ],
      "error_codes": [
        {
          "code": "PROPOSE_INVALID_ARGS",
          "http_status": 400,
          "description": "action_item_id is not a UUID, new_assignee_user_id is neither a UUID nor null, or rationale exceeds 2000 chars."
        },
        {
          "code": "PROPOSE_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Propose actions are user-tier only."
        },
        {
          "code": "PROPOSE_TARGET_NOT_FOUND",
          "http_status": 404,
          "description": "The action_item does not exist or is not in the caller's organization (RLS)."
        },
        {
          "code": "PROPOSE_QUEUE_DEPTH_EXCEEDED",
          "http_status": 429,
          "description": "The caller already has 25+ pending proposals. Apply or reject some before proposing more."
        },
        {
          "code": "PROPOSE_RATIONALE_REQUIRED",
          "http_status": 400,
          "description": "An agent caller staged this high-stakes JUDGMENT proposal with no (or blank) rationale. Re-call with a rationale explaining WHY, so the human reviewer and the audit log can see the agent's reasoning (meaningful oversight). Human-driven callers are exempt."
        }
      ]
    },
    {
      "name": "propose_add_task",
      "module": "projects",
      "category": "lifecycle",
      "description": "STAGED write per ADR 0012. Proposes ADDING a new task to an existing project (inserts an action_items row under the given action_id). Creating new scope always requires human confirmation — this proposal kind NEVER auto-applies regardless of org autonomy settings (the irreducible safety floor). Returns proposal_id + apply_url + reject_url; the task is only created when an authorized human applies it. Emits 'agent.proposal.staged' on stage, 'projects.action_item.created' on apply.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "title"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "The parent project (actions.id) to add the task under."
          },
          "title": {
            "type": "string",
            "minLength": 3,
            "maxLength": 200,
            "description": "Title of the new task."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000,
            "description": "Optional task description."
          },
          "rationale": {
            "type": "string",
            "maxLength": 2000,
            "description": "Shown to the human reviewer when they decide to apply or reject. REQUIRED for agent callers: this is a high-stakes JUDGMENT proposal, so an agent (token-authenticated) must explain WHY — a missing/blank rationale returns PROPOSE_RATIONALE_REQUIRED. Human-driven callers are exempt."
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 168
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "INSERT agent_proposals row (pending)",
        "EMIT agent.proposal.staged webhook"
      ],
      "mcp_method": "propose_add_task",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: propose_add_task)",
      "status": "live",
      "rate_limit": "60 calls/hour per user",
      "examples": [
        {
          "title": "Propose adding a follow-up task to a project",
          "input": {
            "action_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
            "title": "Schedule post-repair vibration baseline",
            "description": "Capture a vibration baseline 48h after the seal replacement to confirm alignment.",
            "rationale": "Standard post-maintenance verification step missing from the plan."
          },
          "expected_output_shape": {
            "proposal_id": "uuid",
            "status": "pending",
            "apply_url": "/api/agent-proposals/<id>/apply",
            "requires_human_confirmation": true
          },
          "notes": "Always returns status:pending — projects.add_task never auto-applies."
        }
      ],
      "error_codes": [
        {
          "code": "PROPOSE_INVALID_ARGS",
          "http_status": 400,
          "description": "action_id is not a UUID, title is not 3-200 chars, description exceeds 5000 chars, or rationale exceeds 2000 chars."
        },
        {
          "code": "PROPOSE_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Propose actions are user-tier only."
        },
        {
          "code": "PROPOSE_TARGET_NOT_FOUND",
          "http_status": 404,
          "description": "The action (project) does not exist or is not in the caller's organization (RLS)."
        },
        {
          "code": "PROPOSE_QUEUE_DEPTH_EXCEEDED",
          "http_status": 429,
          "description": "The caller already has 25+ pending proposals. Apply or reject some before proposing more."
        },
        {
          "code": "PROPOSE_RATIONALE_REQUIRED",
          "http_status": 400,
          "description": "An agent caller staged this high-stakes JUDGMENT proposal with no (or blank) rationale. Re-call with a rationale explaining WHY, so the human reviewer and the audit log can see the agent's reasoning (meaningful oversight). Human-driven callers are exempt."
        }
      ]
    },
    {
      "name": "propose_change",
      "module": "projects",
      "category": "lifecycle",
      "description": "STAGED write per ADR 0012. Proposes CREATING new work — one or more new projects/tasks — from an inferred change. The Approve destination for the agentic servant: you structure the change into `units`; on human apply, executeWorkPlan materializes them (the same engine the planner uses). Creating new scope NEVER auto-applies regardless of org autonomy settings (the irreducible recorder≠actor floor). Distinct from propose_add_task, which adds ONE task to an EXISTING project. Returns proposal_id + apply_url + reject_url; the work is only created when an authorized human applies it. Emits 'agent.proposal.staged' on stage.",
      "input_schema": {
        "type": "object",
        "required": [
          "units"
        ],
        "properties": {
          "units": {
            "type": "array",
            "minItems": 1,
            "maxItems": 25,
            "description": "The new work units to create.",
            "items": {
              "type": "object",
              "required": [
                "title"
              ],
              "properties": {
                "work_type": {
                  "type": "string",
                  "enum": [
                    "task",
                    "project"
                  ],
                  "description": "'project' (a new project) or 'task' (a standalone task). Defaults 'project'."
                },
                "title": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 200
                },
                "description": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 5000
                },
                "due_date": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "YYYY-MM-DD, or null."
                },
                "category": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 120
                },
                "priority": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 120
                },
                "module_shape_candidate": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "moc",
                    "incident_investigation",
                    "training_certification",
                    "permit_application",
                    "vendor_rfq",
                    "compliance_audit",
                    null
                  ],
                  "description": "Optional: the module shape you infer for this unit. Used as the authoritative module-candidacy answer (so the change lands in the right module lens). null = explicitly no shape; omit to let the server infer."
                },
                "module_shape_confidence": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Your self-assessed confidence (0–1) in module_shape_candidate."
                },
                "cascade_parent_action_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid",
                  "description": "Optional: id of an already-approved parent action this unit CASCADES from, in a different module (e.g. an approved MOC change → a Training change). Records a queryable, audit-exportable cross-module lineage link on the created child. Still human-applied like any create_change — each cascade step is approved on its own."
                }
              }
            }
          },
          "anchor_action_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Optional existing project (actions.id) this change relates to — shown to the reviewer."
          },
          "rationale": {
            "type": "string",
            "maxLength": 2000,
            "description": "Shown to the human reviewer when they decide to apply or reject. REQUIRED for agent callers: this is a high-stakes JUDGMENT proposal, so an agent (token-authenticated) must explain WHY — a missing/blank rationale returns PROPOSE_RATIONALE_REQUIRED. Human-driven callers are exempt."
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 168
          },
          "grounding": {
            "type": "object",
            "description": "Q1 Logic Archive: optional provenance for this proposed change — where it came from. Persisted with the proposal + exported (get_audit_export) so the reasoning outlives the session.",
            "properties": {
              "source_kind": {
                "type": "string",
                "maxLength": 64,
                "description": "What backs the change: e.g. document | email | regulation | prior_outcome | user_statement."
              },
              "source_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Optional id of the source artifact (doc id / action id / citation canonical_id)."
              },
              "quote": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 1000,
                "description": "A short verbatim excerpt from the source that justifies the change."
              }
            },
            "required": [
              "source_kind"
            ]
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Q1: the agent's own confidence in this proposal (0.0-1.0)."
          },
          "model_provenance": {
            "type": "object",
            "description": "Q6: optionally declare which model/provider produced this proposal. Persisted with the proposal (exported via get_audit_export) and lifted into the tamper-evident governance record at apply — the audit trail of WHICH model authored the change (EU AI Act / FTC 'model version' in logs).",
            "properties": {
              "model": {
                "type": "string",
                "maxLength": 120,
                "description": "The model that produced this proposal, e.g. 'claude-opus-4-8'. Required when model_provenance is given."
              },
              "provider": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 120,
                "description": "The provider/vendor, e.g. 'anthropic' | 'openai' | 'google'."
              },
              "version": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 120,
                "description": "Optional finer version/build string."
              }
            },
            "required": [
              "model"
            ]
          },
          "reasoning_path": {
            "type": "array",
            "maxItems": 30,
            "description": "Q1 Logic Archive: the ordered reasoning PATH that led to this proposal — the steps you considered, INCLUDING the paths you abandoned (dead-ends), not just the final grounding. Persisted with the proposal + exported (get_audit_export) so the reasoning that produced it, not just its conclusion, outlives the session.",
            "items": {
              "type": "object",
              "properties": {
                "step": {
                  "type": "string",
                  "maxLength": 500,
                  "description": "What was considered / tried at this step."
                },
                "outcome": {
                  "type": "string",
                  "enum": [
                    "followed",
                    "considered",
                    "abandoned"
                  ],
                  "description": "Whether this path was followed, merely considered, or abandoned (a dead-end). Defaults to 'followed'."
                }
              },
              "required": [
                "step"
              ]
            }
          },
          "decision_reasoning": {
            "type": "object",
            "description": "Optionally supply YOUR OWN decision reasoning for this proposal — the specific principal reason for the change + the alternatives you genuinely weighed. Recorded honestly as an ATTRIBUTED CLAIM: the provenance ('source', and an 'unverified' marker for a self-asserted agent identity) is SERVER-DERIVED from your transport + verified credential and is NEVER taken from any field you send. Stamped onto each created action's audit record at apply and surfaced via get_audit_export, the same as the Planner's own reasoning. ANTI-FABRICATION (ground, don't fabricate): a blank specific_reason drops the whole object; an alternative without a grounded why_rejected is dropped; an empty alternatives list is correct and PREFERRED when you weighed none — do not invent options to look thorough.",
            "properties": {
              "specific_reason": {
                "type": "string",
                "maxLength": 2000,
                "description": "The SPECIFIC, grounded reason for this change, citing something concrete — NOT a generic restatement of the task. Required; a blank value drops the object."
              },
              "alternatives_considered": {
                "type": "array",
                "maxItems": 8,
                "description": "Alternatives you ACTUALLY weighed and ruled out. Empty/omitted is the honest default when you weighed none.",
                "items": {
                  "type": "object",
                  "properties": {
                    "option": {
                      "type": "string",
                      "maxLength": 500,
                      "description": "The alternate approach you considered."
                    },
                    "why_rejected": {
                      "type": "string",
                      "maxLength": 1000,
                      "description": "The concrete, grounded reason it was NOT chosen. An alternative missing this is dropped (a vague rejection is a confabulation)."
                    }
                  },
                  "required": [
                    "option",
                    "why_rejected"
                  ]
                }
              }
            },
            "required": [
              "specific_reason"
            ]
          }
        }
      },
      "permission_required": "actions.create",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "INSERT agent_proposals row (pending)",
        "EMIT agent.proposal.staged webhook"
      ],
      "mcp_method": "propose_change",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: propose_change)",
      "status": "live",
      "rate_limit": "30 calls/hour per user",
      "examples": [
        {
          "title": "Propose a new project inferred from a forwarded email",
          "input": {
            "units": [
              {
                "work_type": "project",
                "title": "Replace failed agitator seal on R-301",
                "description": "Vendor flagged seal weep during the last inspection; plan the shutdown, parts, and PSSR.",
                "priority": "high"
              }
            ],
            "rationale": "Inferred from a maintenance email; staged for the supervisor to approve."
          },
          "expected_output_shape": {
            "proposal_id": "uuid",
            "status": "pending",
            "apply_url": "/api/agent-proposals/<id>/apply",
            "requires_human_confirmation": true
          },
          "notes": "Always returns status:pending — projects.create_change never auto-applies."
        }
      ],
      "error_codes": [
        {
          "code": "PROPOSE_INVALID_ARGS",
          "http_status": 400,
          "description": "units is empty/over 25, a unit.title is not 3-200 chars, a field exceeds its cap, anchor_action_id is not a UUID, or rationale exceeds 2000 chars."
        },
        {
          "code": "PROPOSE_REQUIRES_AUTH",
          "http_status": 401,
          "description": "propose_change is user-tier only."
        },
        {
          "code": "PROPOSE_TARGET_NOT_FOUND",
          "http_status": 404,
          "description": "The anchor action does not exist or is not in the caller's organization (RLS)."
        },
        {
          "code": "PROPOSE_QUEUE_DEPTH_EXCEEDED",
          "http_status": 429,
          "description": "The caller already has 25+ pending proposals. Apply or reject some before proposing more."
        },
        {
          "code": "PROPOSE_RATIONALE_REQUIRED",
          "http_status": 400,
          "description": "An agent caller staged this high-stakes JUDGMENT proposal with no (or blank) rationale. Re-call with a rationale explaining WHY, so the human reviewer and the audit log can see the agent's reasoning (meaningful oversight). Human-driven callers are exempt."
        }
      ]
    },
    {
      "name": "propose_batch",
      "module": "projects",
      "category": "lifecycle",
      "description": "STAGED write per ADR 0012. Stages MULTIPLE task-status changes as one bundle (shared batch_id) so the human reviews/resolves them as a set — e.g. 'mark these 5 tasks done'. MVP supports kind='update_task_status' only. NOT a database transaction: applying happens per-proposal (side effects like notifications can't roll back), and if staging fails partway the already-staged rows keep the shared batch_id so the human still sees the partial set. Each row inherits projects.update_task_status traits, so terminal statuses still route through human confirmation — the batch grants no new authority. Returns batch_id + one proposal_id/apply_url/reject_url per item.",
      "input_schema": {
        "type": "object",
        "required": [
          "kind",
          "items"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "update_task_status"
            ],
            "description": "Batch kind. Only 'update_task_status' is supported in the MVP."
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 25,
            "description": "1-25 task-status changes to stage as a bundle.",
            "items": {
              "type": "object",
              "required": [
                "action_item_id",
                "new_status"
              ],
              "properties": {
                "action_item_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "The task (action_items.id) to change."
                },
                "new_status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "in_progress",
                    "done",
                    "blocked",
                    "skipped"
                  ],
                  "description": "The proposed new status for this task."
                }
              }
            }
          },
          "rationale": {
            "type": "string",
            "maxLength": 2000,
            "description": "Shown to the human reviewer for the whole batch when they decide to apply or reject."
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 168
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": true,
      "side_effects": [
        "INSERT 1-25 agent_proposals rows (pending), sharing one batch_id",
        "EMIT agent.proposal.staged webhook per row"
      ],
      "mcp_method": "propose_batch",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: propose_batch)",
      "status": "live",
      "rate_limit": "20 calls/hour per user",
      "examples": [
        {
          "title": "Propose marking three completed tasks done in one batch",
          "input": {
            "kind": "update_task_status",
            "items": [
              {
                "action_item_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
                "new_status": "done"
              },
              {
                "action_item_id": "9a4d3c21-2b3c-5d4e-9f0a-1b2c3d4e5f60",
                "new_status": "done"
              },
              {
                "action_item_id": "0b5e4d32-3c4d-6e5f-0a1b-2c3d4e5f6071",
                "new_status": "in_progress"
              }
            ],
            "rationale": "Post-outage walkdown: first two steps verified complete, third now underway."
          },
          "expected_output_shape": {
            "batch_id": "uuid",
            "proposal_kind": "projects.update_task_status",
            "count": 3,
            "status": "pending",
            "proposals": [
              {
                "proposal_id": "uuid",
                "apply_url": "/api/agent-proposals/<id>/apply"
              }
            ],
            "requires_human_confirmation": true
          },
          "notes": "Always returns status:pending — each row is applied individually by a human; the batch_id just groups them."
        }
      ],
      "error_codes": [
        {
          "code": "PROPOSE_BATCH_UNSUPPORTED_KIND",
          "http_status": 400,
          "description": "kind is not 'update_task_status' (the only kind supported in the MVP)."
        },
        {
          "code": "PROPOSE_INVALID_ARGS",
          "http_status": 400,
          "description": "items is empty or >25, an action_item_id is not a UUID, a new_status is not a valid status, or rationale exceeds 2000 chars."
        },
        {
          "code": "PROPOSE_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Propose actions are user-tier only."
        },
        {
          "code": "PROPOSE_TARGET_NOT_FOUND",
          "http_status": 404,
          "description": "One or more action_items do not exist or are not in the caller's organization (RLS); the batch is not staged."
        },
        {
          "code": "PROPOSE_QUEUE_DEPTH_EXCEEDED",
          "http_status": 429,
          "description": "Staging would exceed the caller's 25 pending-proposal limit. Already-staged rows in the batch keep the shared batch_id."
        }
      ]
    },
    {
      "name": "list_pending_proposals",
      "module": "agent",
      "category": "read",
      "description": "Returns the org's staged agent_proposals so an agent (or the in-app pending-proposals UI) can show the user what's awaiting confirmation. Each entry includes apply_url + reject_url (machine REST endpoints) and review_url — the HUMAN-facing deep-link (/notifications?proposal=<id>) you hand the person so they approve it in the app, where the high-stakes identity step-up runs. Prefer surfacing review_url to a human; apply_url is for programmatic resolution only.",
      "input_schema": {
        "type": "object",
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "Scope to a single project."
          },
          "include_resolved": {
            "type": "boolean",
            "default": false
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "default": 50
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "list_pending_proposals",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: list_pending_proposals)",
      "status": "live"
    },
    {
      "name": "get_proposal_status",
      "module": "agent",
      "category": "read",
      "description": "Fate-tracking for a single staged proposal by id. After you propose a change, poll this to learn whether a human applied, rejected, reverted, or it expired — distinct from list_pending_proposals (pending only).",
      "input_schema": {
        "type": "object",
        "properties": {
          "proposal_id": {
            "type": "string",
            "format": "uuid",
            "description": "The proposal id returned by a propose_* call."
          }
        },
        "required": [
          "proposal_id"
        ]
      },
      "requires_tier": "user",
      "permission_required": "actions.edit",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_proposal_status",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_proposal_status)",
      "rate_limit": "300 calls/hour per user",
      "examples": [
        {
          "description": "Poll the fate of a proposal you staged.",
          "request": {
            "method": "get_proposal_status",
            "params": {
              "proposal_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f"
            }
          },
          "response": {
            "proposal_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
            "status": "applied",
            "proposal_kind": "projects.update_task_status",
            "resolved_at": "2026-05-31T12:00:00Z"
          }
        }
      ],
      "error_codes": [
        "GET_PROPOSAL_STATUS_INVALID_ARGS",
        "GET_PROPOSAL_STATUS_REQUIRES_AUTH",
        "GET_PROPOSAL_STATUS_NOT_FOUND",
        "GET_PROPOSAL_STATUS_READ_FAILED",
        "GET_PROPOSAL_STATUS_UPSTREAM_FAILED"
      ],
      "status": "live"
    },
    {
      "name": "apply_proposal",
      "module": "agent",
      "category": "lifecycle",
      "description": "Ask to apply a pending agent_proposal, closing the propose→apply loop. Per ADR 0012 this does NOT blanket-apply: it routes through your org's autonomy policy. Only when the org has opted into auto-apply for this change kind does it execute; otherwise the proposal stays STAGED for human approval and the response tells you so (applied=false, status='pending'). Default org posture is human-approval.",
      "input_schema": {
        "type": "object",
        "properties": {
          "proposal_id": {
            "type": "string",
            "format": "uuid",
            "description": "The pending proposal id returned by a propose_* call."
          },
          "note": {
            "type": "string",
            "description": "Optional note recorded on the resolution when auto-applied."
          }
        },
        "required": [
          "proposal_id"
        ]
      },
      "requires_tier": "user",
      "permission_required": "actions.edit",
      "human_must_confirm": false,
      "side_effects": [
        "When the org autonomy policy resolves to 'auto': applies the underlying change (task status / due date / comment) + fires the same audit-log, notifications, telemetry, webhook, and immutable-trail side effects as the human-apply path.",
        "Otherwise: no mutation — the proposal remains pending for human approval."
      ],
      "mcp_method": "apply_proposal",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: apply_proposal)",
      "rate_limit": "60 calls/hour per user",
      "examples": [
        {
          "description": "Try to apply a staged proposal; safe-default org returns it as still-pending.",
          "request": {
            "method": "apply_proposal",
            "params": {
              "proposal_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f"
            }
          },
          "response": {
            "proposal_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
            "applied": false,
            "posture": "prompt",
            "status": "pending",
            "message": "Staged for human approval (org has not enabled agent auto-apply). A human can apply it from the agent-activity inbox at /notifications."
          }
        }
      ],
      "error_codes": [
        "APPLY_PROPOSAL_INVALID_ARGS",
        "APPLY_PROPOSAL_REQUIRES_AUTH",
        "APPLY_PROPOSAL_NO_ORG",
        "APPLY_PROPOSAL_NOT_FOUND",
        "APPLY_PROPOSAL_WRONG_STATUS",
        "APPLY_PROPOSAL_EXPIRED",
        "APPLY_PROPOSAL_LOAD_FAILED",
        "APPLY_PROPOSAL_EXECUTE_FAILED"
      ],
      "status": "live"
    },
    {
      "name": "subscribe_to_events",
      "module": "agent",
      "category": "lifecycle",
      "description": "Register an outbound webhook subscription. Returns a one-time signing secret the caller MUST persist — every event POST is HMAC-SHA256-signed with this secret in the X-Changepilot-Signature header. Event types are module-namespaced (e.g. 'projects.action.created', 'moc.stage.advanced'); supply an empty event_types array to subscribe to ALL events the org has access to. Use module_filter to restrict to specific modules. Foundation for Slack/Teams real-time digests, Linear sync, CMMS triggers, calendar invalidation.",
      "input_schema": {
        "type": "object",
        "required": [
          "callback_url"
        ],
        "properties": {
          "callback_url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS endpoint that will receive event POSTs. http:// rejected."
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Specific event types to subscribe to. Empty array = subscribe to ALL events the org has access to."
          },
          "module_filter": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Module slugs to restrict to (e.g. ['moc']). Empty = all available modules."
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "INSERT webhook_subscriptions row"
      ],
      "mcp_method": "subscribe_to_events",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: subscribe_to_events)",
      "status": "live"
    },
    {
      "name": "unsubscribe_from_events",
      "module": "agent",
      "category": "lifecycle",
      "description": "Soft-revoke a webhook subscription. Sets revoked_at; preserves delivery history for audit.",
      "input_schema": {
        "type": "object",
        "required": [
          "subscription_id"
        ],
        "properties": {
          "subscription_id": {
            "type": "string",
            "format": "uuid"
          },
          "reason": {
            "type": "string"
          }
        }
      },
      "permission_required": "actions.modify",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "UPDATE webhook_subscriptions row (revoked_at)"
      ],
      "mcp_method": "unsubscribe_from_events",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: unsubscribe_from_events)",
      "status": "live"
    },
    {
      "name": "list_subscriptions",
      "module": "agent",
      "category": "read",
      "description": "List the org's webhook subscriptions WITHOUT the signing secret (returned only on create). Includes delivery health fields (last attempt timestamp, last_delivery_ok, consecutive_failures) so an agent can diagnose its own endpoint failures.",
      "input_schema": {
        "type": "object",
        "properties": {
          "include_revoked": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "list_subscriptions",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: list_subscriptions)",
      "status": "live"
    },
    {
      "name": "get_project_status_summary",
      "category": "read",
      "description": "Deterministic distilled status digest across the caller's active projects (or a single project). Status label, what's progressing, what's blocked, next milestone date — derived from structured fields + task counts. NO comment text / PII read.",
      "input_schema": {
        "type": "object",
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 25,
            "default": 10
          }
        }
      },
      "permission_required": "actions.read",
      "requires_tier": "user",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_project_status_summary",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_project_status_summary)",
      "status": "live",
      "module": "projects"
    },
    {
      "name": "generate_pssr_checklist",
      "category": "compliance",
      "description": "For projects tagged with PSSR scope, emit a Pre-Startup Safety Review checklist bound to the project's entities (equipment, permits, regulatory citations). Returns structured checklist rows for the user to review.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "permission_required": "actions.modify",
      "human_confirmation": "required_before_signoff",
      "rest_endpoint": "POST /api/actions/{id}/pssr-checklist (planned)",
      "status": "planned"
    },
    {
      "name": "route_review",
      "category": "routing",
      "description": "Send a project (or a specific stage) to a named reviewer with a comment. Lighter-weight than assign_reviewers — adds a single reviewer to the current routing chain.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "reviewer_user_id"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid"
          },
          "reviewer_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "comment": {
            "type": "string"
          }
        }
      },
      "permission_required": "actions.modify",
      "rest_endpoint": "POST /api/actions/{id}/share (with reviewer_user_id)",
      "status": "planned"
    },
    {
      "name": "refine_project",
      "category": "lifecycle",
      "requires_tier": "user",
      "module": "projects",
      "description": "Refine an existing planning session by submitting follow-up text. ChangePilot re-runs the planner against the merged context (existing plan + new input) and emits an updated structured plan ready for confirmation.",
      "input_schema": {
        "type": "object",
        "required": [
          "session_id",
          "text"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "format": "uuid",
            "description": "Planning session to refine."
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000,
            "description": "Additional context, corrections, or scope additions."
          }
        }
      },
      "permission_required": "actions.edit",
      "side_effects": [
        "Updates planning_sessions.model_payload",
        "May add/remove action / action_item rows on confirmation"
      ],
      "human_confirmation": "required_for_terminal_apply",
      "mcp_method": "refine_project",
      "mcp_endpoint": "/api/mcp/v1"
    },
    {
      "name": "capture_retrospective",
      "category": "capture",
      "requires_tier": "anon",
      "module": "projects",
      "description": "Capture lessons-learned, outcome notes, and reusable signals from a completed project. Writes project_outcomes + extracts entity/regulation references so future similar projects benefit from the history.",
      "input_schema": {
        "type": "object",
        "required": [
          "action_id",
          "what_went_well",
          "what_went_wrong"
        ],
        "properties": {
          "action_id": {
            "type": "string",
            "format": "uuid",
            "description": "Project to attach the retrospective to."
          },
          "what_went_well": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000
          },
          "what_went_wrong": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000
          },
          "what_we_learned": {
            "type": "string",
            "maxLength": 4000
          },
          "lesson_labels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 12
          }
        }
      },
      "permission_required": "actions.edit",
      "side_effects": [
        "Inserts project_outcomes row",
        "May upsert entity_instances + vendor_mentions"
      ],
      "human_confirmation": "none",
      "mcp_method": "capture_retrospective",
      "mcp_endpoint": "/api/mcp/v1"
    },
    {
      "name": "detect_entities",
      "category": "analysis",
      "requires_tier": "anon",
      "module": "projects",
      "description": "Detect industrial entities (equipment, vendors, regulations, permits, departments) in free text. Returns a structured list with category + confidence per match. Useful as a preview before create_change or as a standalone extraction call.",
      "input_schema": {
        "type": "object",
        "required": [
          "text"
        ],
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000,
            "description": "Free text to scan. May be a brain dump, email body, or document excerpt."
          }
        }
      },
      "permission_required": null,
      "side_effects": [
        "None — read-only extraction"
      ],
      "human_confirmation": "none",
      "mcp_method": "detect_entities",
      "mcp_endpoint": "/api/mcp/v1"
    },
    {
      "name": "answer_clarification",
      "category": "lifecycle",
      "requires_tier": "anon",
      "module": "projects",
      "description": "Answer a clarifying question emitted by the planner during V2 segmentation by picking one of the presented option ids. Continues the planning session past an `awaiting_clarification` checkpoint to phase `triage_review`. Anonymous callers must pass `anonymous_token` (the planning-session resume token returned by an anonymous create_change). Path A = pick a presented option. Path B = `option_id='other'` + `free_text` re-segments the original input with your clarification (AUTHED-only; anonymous callers re-run create_change with the clarification appended).",
      "input_schema": {
        "type": "object",
        "required": [
          "session_id",
          "option_id"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "format": "uuid",
            "description": "Planning session id returned by create_change when phase=awaiting_clarification."
          },
          "option_id": {
            "type": "string",
            "description": "One of clarifying_question.options[].id, OR 'other' to re-segment from your own free text (Path B). Path B is AUTHED-only and requires `free_text`."
          },
          "free_text": {
            "type": "string",
            "description": "Required when option_id='other' (Path B, authed only): describe how you want the work split. The planner re-segments the original input with this as guidance and accepts the result (resolution_mode=other_resegmented when it becomes confident, else other_accepted). ≤ 4000 chars."
          },
          "anonymous_token": {
            "type": "string",
            "format": "uuid",
            "description": "Planning-session resume token (data.anonymous_token from an anonymous create_change). Required for anonymous-tier callers; ignored for authed callers. Note: Path B (option_id='other') is not available to anonymous callers — re-run create_change with the clarification appended instead."
          },
          "idempotency_key": {
            "type": "string"
          }
        }
      },
      "permission_required": "actions.create",
      "side_effects": [
        "Updates planning_sessions.phase to triage_review + model_payload.units"
      ],
      "human_confirmation": "none",
      "examples": [
        {
          "summary": "Anonymous agent answers a clarifying question",
          "args": {
            "session_id": "11111111-1111-4111-8111-111111111111",
            "option_id": "opt_two_projects",
            "anonymous_token": "22222222-2222-4222-8222-222222222222"
          }
        }
      ],
      "error_codes": [
        "ANON_TOKEN_REQUIRED",
        "INVALID_SESSION_ID",
        "INVALID_OPTION_ID",
        "PATH_B_NOT_SUPPORTED",
        "WRONG_PHASE",
        "NO_CLARIFYING_QUESTION",
        "UNKNOWN_OPTION_ID",
        "SESSION_NOT_FOUND"
      ],
      "rate_limit": {
        "per_user_per_hour": 60,
        "per_anon_per_hour": 30
      },
      "mcp_method": "answer_clarification",
      "mcp_endpoint": "/api/mcp/v1"
    },
    {
      "name": "import_documents",
      "category": "capture",
      "requires_tier": "user",
      "module": "projects",
      "description": "Ingest one or more documents into ChangePilot. Classifier picks a shape per file (project_lifecycle / equipment_registry / vendor_registry / etc.); per-shape extractor parses entities, vendors, regulations, outcomes. Staged for approval before canonical writes — never auto-applies.",
      "input_schema": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "mime_type",
                "storage_path"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "mime_type": {
                  "type": "string"
                },
                "storage_path": {
                  "type": "string"
                }
              }
            },
            "minItems": 1,
            "maxItems": 50
          },
          "source_kind": {
            "type": "string",
            "enum": [
              "manual_upload",
              "cloud_sync",
              "mcp_import",
              "email_in"
            ],
            "default": "mcp_import"
          },
          "re_extract_existing": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "permission_required": "actions.edit",
      "side_effects": [
        "Inserts document_ingest_jobs + document_ingest_items rows",
        "Staged — no canonical writes until user approval"
      ],
      "human_confirmation": "required_for_terminal_apply",
      "mcp_method": "import_documents",
      "mcp_endpoint": "/api/mcp/v1"
    },
    {
      "name": "get_import_status",
      "module": "projects",
      "category": "read",
      "description": "Poll the fate of a document-import job by the ingest_job_id that import_documents returned. After you trigger an import, poll this until is_terminal is true to learn how many files extracted vs failed, what each was classified as, and whether a human still needs to approve the staged import. Read-only; RLS-scoped to your org.",
      "input_schema": {
        "type": "object",
        "properties": {
          "ingest_job_id": {
            "type": "string",
            "format": "uuid",
            "description": "The ingest_job_id returned in the data of a prior import_documents call."
          }
        },
        "required": [
          "ingest_job_id"
        ]
      },
      "requires_tier": "user",
      "permission_required": "actions.view_own",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "mcp_method": "get_import_status",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_import_status)",
      "rate_limit": "300 calls/hour per user",
      "examples": [
        {
          "description": "Poll an import job until it finishes, then surface per-file results.",
          "request": {
            "method": "get_import_status",
            "params": {
              "ingest_job_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f"
            }
          },
          "response": {
            "ingest_job_id": "8f3c2b10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
            "status": "completed",
            "total_items": 3,
            "completed_items": 2,
            "failed_items": 1,
            "pending_items": 0,
            "percent_complete": 100,
            "is_terminal": true,
            "requires_human_approval": true,
            "items": [
              {
                "item_id": "…",
                "file_name": "outage-report.pdf",
                "status": "completed",
                "classified_shape": "project_lifecycle",
                "target_module": "projects",
                "error_detail": null
              }
            ]
          }
        }
      ],
      "error_codes": [
        "GET_IMPORT_STATUS_INVALID_ARGS",
        "GET_IMPORT_STATUS_REQUIRES_AUTH",
        "GET_IMPORT_STATUS_NOT_FOUND",
        "GET_IMPORT_STATUS_READ_FAILED"
      ],
      "status": "live"
    },
    {
      "name": "get_claim_url",
      "category": "lifecycle",
      "requires_tier": "anon",
      "module": "projects",
      "description": "Build a deep-link URL that an anonymous user can click to claim ownership of their planning sessions. Use this AFTER an anonymous create_change so the user can sign up and have their orphan plan migrate automatically. The agent should display the returned claim_url + instructions_for_user verbatim to the human.",
      "input_schema": {
        "type": "object",
        "properties": {
          "anonymous_token": {
            "type": "string",
            "format": "uuid",
            "description": "The anonymous_token returned in the `data.anonymous_token` field of a prior create_change response. Required for both anon and authed callers — the Bearer JWT's session id is separate from the planning_sessions resume token. Most agents should follow the `_next_step.claim_url` field on the create_change response instead of calling this action directly."
          },
          "ref": {
            "type": "string",
            "maxLength": 48,
            "description": "Optional source tag (e.g. 'claude_chat', 'cursor', 'chatgpt_gpt') for conversion attribution."
          }
        }
      },
      "permission_required": null,
      "side_effects": [
        "None - URL builder only, no rows written"
      ],
      "human_confirmation": "none",
      "mcp_method": "get_claim_url",
      "mcp_endpoint": "/api/mcp/v1",
      "notes": "URL includes the anonymous_token as ?import_token=<uuid>. /signup forwards import_token to /login (signup mode), which stashes it in sessionStorage. On signup the existing /api/screener/planning-session/link route migrates orphan planning_sessions onto the new user."
    },
    {
      "name": "get_pending_events",
      "category": "read",
      "requires_tier": "user",
      "module": "projects",
      "description": "Poll for notifications/events targeted at the caller's user. Use this when you need to surface async updates from ChangePilot in your chat thread (mentions, due-soon, overdue, status changes, comments). Returns events newer than the optional cursor; pass the returned next_cursor on the next call. Bounded by limit (default 25, max 100). Source is user_notifications (RLS-scoped).",
      "input_schema": {
        "type": "object",
        "properties": {
          "cursor": {
            "type": "string",
            "format": "date-time",
            "description": "ISO timestamp from a previous next_cursor. Omit on first call."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 25
          }
        }
      },
      "permission_required": "actions.view_own",
      "side_effects": [
        "None - read-only poll"
      ],
      "human_confirmation": "none",
      "mcp_method": "get_pending_events",
      "mcp_endpoint": "/api/mcp/v1",
      "notes": "Pairs with the four-layer notification plan (Layer D polling). Future: MCP SSE notifications will push these events without polling once Anthropic ships inbound MCP notifications in Claude.ai."
    },
    {
      "name": "get_my_attention_queue",
      "category": "read",
      "requires_tier": "user",
      "module": "projects",
      "description": "Returns a single proactive-loop digest of everything that needs the caller since they last synced — composing pending approvals, upcoming deadlines, the caller's own assignments, recent events, and the TRIAGE backlog (captured-but-unscoped drafts awaiting scoping, the agent-facing view of Triage Mode) into one read. Call this on wake (e.g. a daily scheduled task) to learn what changed and what needs you; every item carries its source read's why-it-matters fields (reason, risk tier, what it blocks, due date, triage_url), never a bare count. Triage items are read-only here — help the human clear them via the governed plan path / the triage_url, never a destructive write.",
      "input_schema": {
        "type": "object",
        "properties": {
          "since": {
            "type": "string",
            "format": "date-time",
            "description": "ISO timestamp of your last sync (pass back the prior response's generated_at). Filters recent_events to events strictly after it. Other sections are current-state and not time-filtered."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 20,
            "description": "Max items surfaced per section."
          }
        }
      },
      "permission_required": "actions.view_own",
      "requires_tier_notes": "User-tier only. Composes four user-tier, org-scoped reads; inherits the strictest gate.",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "rate_limit": "300 calls/hour per user",
      "idempotency_key_supported": false,
      "idempotency_notes": "Read-only and idempotent — safe to retry on every agent wake. Pure composition over four existing reads; performs no writes.",
      "examples": [
        {
          "title": "First sync of the day (no prior cursor)",
          "input": {
            "limit": 20
          },
          "expected_output_shape": {
            "summary": "3 approvals waiting (1 high-risk), 2 deadlines this week, 5 assignments on your plate (1 overdue).",
            "generated_at": "ISO timestamp — pass back as `since` next time",
            "since": null,
            "sections": "{pending_approvals[], upcoming_deadlines[], my_assignments[], recent_events[], triage_queue[]}",
            "section_errors": "[] (per-section fail-soft notes; empty when all reads succeeded)"
          },
          "notes": "Save generated_at and pass it as `since` on the next wake to get only what's new."
        },
        {
          "title": "Incremental sync since the last wake",
          "input": {
            "since": "2026-06-19T08:00:00.000Z",
            "limit": 50
          },
          "expected_output_shape": {
            "summary": "1 approval waiting, 4 unread updates.",
            "sections": "recent_events filtered to events after `since`; other sections current-state"
          }
        }
      ],
      "error_codes": [
        {
          "code": "GET_MY_ATTENTION_QUEUE_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Anonymous tier — get_my_attention_queue is user-tier only (no org or assignments to digest)."
        },
        {
          "code": "GET_MY_ATTENTION_QUEUE_INVALID_SINCE",
          "http_status": 400,
          "description": "`since` was not a parseable ISO timestamp. Pass back a prior response's generated_at, or omit it for a full digest."
        }
      ],
      "mcp_method": "get_my_attention_queue",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_my_attention_queue)",
      "status": "live",
      "notes": "Q106 proactive-loop primitive (2026-06-19). Pure composition over list_pending_proposals + get_upcoming_deadlines + get_my_assignments + get_pending_events + get_triage_queue — no new query, no migration, no writes. Fail-soft per section: a failed underlying read yields an empty section + a section_errors note, not a whole-digest failure. Reference recipe: a Claude Code Scheduled Task calls this each morning, then proposes updates."
    },
    {
      "name": "get_triage_queue",
      "category": "read",
      "requires_tier": "user",
      "module": "projects",
      "description": "Returns the TRIAGE backlog — the captured-but-unscoped work awaiting scoping, the same drafts the in-app Triage Mode (/projects/triage) walks one card at a time. This is the agent-facing 'triage function': call it to surface 'you have N captured items to triage' and help the human clear the backlog. Each item carries a draft_id, a human title (source title or a brain-dump snippet), when it was captured, where it came from, and a triage_url deep-link to the governed scoping surface. READ-ONLY: scoping/snoozing/discarding a draft happens via the human in Triage Mode (triage_url) or an agent's propose_change — never a destructive write from this read.",
      "input_schema": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 20,
            "description": "Max triage items to return (newest captures first)."
          }
        }
      },
      "permission_required": "actions.view_own",
      "requires_tier_notes": "User-tier only. Org-scoped + owner-scoped to the caller's own captured drafts (the same filter the Drafts/Triage UI uses).",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "rate_limit": "300 calls/hour per user",
      "idempotency_key_supported": false,
      "idempotency_notes": "Read-only and idempotent — safe to retry on every agent wake. Reuses the shared session-draft filter; performs no writes.",
      "examples": [
        {
          "title": "Surface the triage backlog on wake",
          "input": {
            "limit": 20
          },
          "expected_output_shape": {
            "items": "[{ draft_id, title, created_at, source_app, triage_url }]",
            "total": "count of items returned",
            "_source": "changepilot.org_history"
          },
          "notes": "Tell the human 'you have N captured items to triage' and hand them triage_url; or scope one yourself via propose_change. Never silently apply."
        }
      ],
      "error_codes": [
        {
          "code": "GET_TRIAGE_QUEUE_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Anonymous tier — get_triage_queue is user-tier only (no org backlog to triage)."
        },
        {
          "code": "GET_TRIAGE_QUEUE_REQUIRES_ORG",
          "http_status": 403,
          "description": "The caller's token has no organization, so there is no org triage backlog to read."
        }
      ],
      "mcp_method": "get_triage_queue",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_triage_queue)",
      "status": "live",
      "notes": "Q111 (2026-06-20) — the agent-facing triage function. A LENS over existing planning_session drafts via the shared applySessionDraftFilters (no query drift from the Drafts/Triage UI), not a new object model. Also composed into get_my_attention_queue's triage_queue section so the wake-digest includes it."
    },
    {
      "name": "get_suggested_setup",
      "category": "read",
      "requires_tier": "user",
      "module": "projects",
      "description": "Returns a RANKED, GROUNDED list of the highest-value next setup moves for THIS org, so the Agent can proactively offer them instead of waiting to be asked. Composes existing org-state reads (org graph stats, project history, workforce) to derive how cold or mature the operation is, then suggests only what its REAL state warrants: a cold or thin org is told to import its existing project docs (a full document dump) FIRST — the single highest-value first step — while a warm org gets few or no suggestions (anti-nag). Each suggestion carries why_now (grounded in the org's state) and the mcp_method to call; queued capabilities (e.g. a calendar ICS feed) are returned with a null mcp_method and status 'coming' so no non-existent method is ever named.",
      "input_schema": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 5,
            "description": "Max number of suggestions to return."
          }
        }
      },
      "permission_required": "actions.view_own",
      "requires_tier_notes": "User-tier only. Composes user-tier, org-scoped reads; inherits the strictest gate.",
      "human_must_confirm": false,
      "side_effects": [
        "None — read-only"
      ],
      "rate_limit": "300 calls/hour per user",
      "idempotency_key_supported": false,
      "idempotency_notes": "Read-only and idempotent — safe to retry. Pure composition over existing reads; performs no writes.",
      "examples": [
        {
          "title": "Cold org — proactively suggest seeding history",
          "input": {
            "limit": 5
          },
          "expected_output_shape": {
            "summary": "This operation is just getting started — top suggestion: Import your existing project docs (a full document dump).",
            "org_maturity": "cold",
            "suggestions": "[{id, action, why_now, mcp_method:'import_documents', priority:1, status:'available'}, ...]",
            "section_errors": "[] (per-section fail-soft notes; empty when all composed reads succeeded)"
          },
          "notes": "When the org has little/no history the full document dump ranks first. Offer it to the user, then call import_documents."
        },
        {
          "title": "Warm org — anti-nag, few or no suggestions",
          "input": {},
          "expected_output_shape": {
            "summary": "This operation already has a healthy history in ChangePilot — no setup suggestions right now.",
            "org_maturity": "warm",
            "suggestions": "[] or a short list"
          }
        }
      ],
      "error_codes": [
        {
          "code": "GET_SUGGESTED_SETUP_REQUIRES_AUTH",
          "http_status": 401,
          "description": "Anonymous tier — get_suggested_setup is user-tier only (no org state to ground suggestions in)."
        }
      ],
      "mcp_method": "get_suggested_setup",
      "mcp_endpoint": "/api/mcp/v1",
      "rest_endpoint": "POST /api/mcp/v1 (JSON-RPC method: get_suggested_setup)",
      "status": "live",
      "notes": "Q110 proactive setup suggester (2026-06-19). Pure composition over get_org_graph_stats + search_changes + get_workforce_skills — no new query, no migration, no writes. Ground-don't-fabricate: suggests the full document dump only when the org is actually cold/thin; returns few/none when warm (anti-nag); never names an mcp_method that doesn't exist (the calendar ICS feed is queued → status 'coming', mcp_method null)."
    }
  ],
  "human_confirmation_policy": {
    "description": "ChangePilot enforces a draft-vs-commit boundary for AI-invoked actions. Agents stage proposals via proposeAiActionsBatch; humans apply or revert.",
    "agent_may": [
      "draft",
      "summarize",
      "stage_change",
      "suggest_reviewer",
      "extract_entity",
      "search",
      "summarize_project"
    ],
    "human_must_confirm": [
      "approve_project",
      "close_project",
      "cancel_project",
      "submit_regulatory_signoff",
      "delete_data",
      "change_org_membership"
    ]
  },
  "auth": {
    "current": "Three options: (1) Anonymous tier: POST /api/mcp/v1/auth with body {} returns mcp_anon_* (IP-bound, ~1h ttl). (2) User tier (short-lived): POST /api/mcp/v1/auth with body {\"tier\": \"user\"} while a Supabase session cookie is present; returns mcp_user_* (12h ttl). (3) Personal access token (long-lived, recommended for agent installs): user generates mcp_pat_* at /account/settings/mcp-tokens and pastes the raw token into the agent's MCP config. Never expires unless user sets expires_in_days or revokes.",
    "planned": "OAuth2 + MCP-server-native auth for connector ecosystem (Anthropic / OpenAI / Cursor)",
    "anonymous_lifecycle": {
      "story": "Anonymous agent calls create_change -> ChangePilot returns a session_id + structured plan. The plan is real but unclaimed; without action it would expire. Agent then calls get_claim_url to receive a signup deep-link, shows the link + instructions_for_user to the human, and the human's click+signup automatically migrates the orphan plan into their new account.",
      "steps": [
        "POST /api/mcp/v1/auth with body {} -> mcp_anon_* Bearer token",
        "tools/call create_change with the user's brain dump -> session_id + structured plan",
        "tools/call get_claim_url -> claim_url + instructions_for_user",
        "Agent: relay claim_url + instructions verbatim to the human user",
        "Human: clicks -> signs up -> orphan planning_sessions migrate via /api/screener/planning-session/link"
      ],
      "agent_responsibilities": [
        "Tell the user when their state is anonymous (plans are unclaimed)",
        "Show the claim_url + instructions_for_user from get_claim_url verbatim - the copy is calibrated for user trust",
        "After a brain dump, prompt the user whether they want to save it"
      ],
      "expires_after_days": 7,
      "after_expiration": "User can still sign up; planning_sessions migrate only if the link route still finds matching anonymous_token rows. After 7+ days plans persist in DB but may be considered stale by cleanup cron."
    }
  },
  "rate_limits": {
    "anonymous": "1 LLM call / minute per IP; 10 / hour per IP global cap",
    "authenticated": "10 LLM calls / minute per user; 100 / hour per org",
    "agent_connector": "Inherits the authenticated user's session limits"
  },
  "contact": {
    "support": "https://changepilot.deltamarksystems.com/contact",
    "security": "https://changepilot.deltamarksystems.com/.well-known/security.txt",
    "company": "Deltamark Systems"
  },
  "conformance_pack": {
    "available": true,
    "surfaced_via": "get_agent_capabilities",
    "description": "The governance controls ChangePilot enforces on agents (the Agent Conformance Pack). Each control is backed by an enforcing mechanism + an evidence read action. Call get_agent_capabilities for the full control→mechanism→evidence map; get_audit_export (per-project) + get_agent_activity_log (org-wide) produce the evidence.",
    "controls": [
      "meaningful_human_oversight",
      "separation_of_duties",
      "required_agent_rationale",
      "governance_decision_replay",
      "authority_basis",
      "tamper_evident_audit_trail",
      "complete_lifecycle_record",
      "stale_context_protection",
      "inlet_integrity"
    ],
    "evidence_actions": [
      "get_agent_capabilities",
      "get_audit_export",
      "get_agent_activity_log"
    ]
  }
}
