Agentforce Pipeline Management Configuration Configure Agentforce Pipeline Management end-to-end in a Salesforce org. This skill handles both greenfield orgs (zero configuration) and partially configured orgs that need repair or completion. Scope - In scope : Enabling Pipeline Management, configuring the Sales Management agent, activating/customizing flows (Process Field Update Suggestions, Get Opportunity Grounding Data, Get AI Recommendations from Call Transcripts), prompt template configuration, permission set assignment, data source setup, opportunity stage descriptions, autonomous field…

|| echo \"\")\nif [[ -n \"$SF_MAJOR\" && \"$SF_MAJOR\" -lt 2 ]]; then\n echo \"ERROR: Salesforce CLI v2.x or newer is required (found major version ${SF_MAJOR}).\"\n echo \" Agent Access (SetupEntityAccess for BotDefinition) needs API v64.0+, which ships with sf CLI v2.x.\"\n exit 1\nfi\n\n# Configurable identifiers\nACCESS_PS_NAME=\"Sales_Agent_Access\"\nACCESS_PS_LABEL=\"Sales Agent Access\"\nUSER_PSG_NAME=\"SalesManagementUserPsg\"\nAGENT_PSG_NAME=\"SalesManagementAgentUserPsg\"\n\necho \"=== Define Agent Access for Pipeline Management Sales Agent ===\"\necho \"\"\n\n# ------------------------------------------------------------\n# Step 0: The agent must exist first (BotDefinition Id is runtime-only).\n# Detect it by AgentTemplate, matching the scheduled flow's own lookup — not by\n# a hardcoded DeveloperName. Uses a TWO-STEP query: first find the BotDefinition\n# by AgentTemplate (no subquery), then separately confirm it has an Active\n# BotVersion. The single-query `Id IN (SELECT ... FROM BotVersion WHERE Status =\n# 'Active')` form was removed in de77d520 because that correlated subquery hangs\n# 60+ seconds on some orgs.\n# ------------------------------------------------------------\nBOT_DEF=$(sf data query \\\n --query \"SELECT Id, DeveloperName FROM BotDefinition WHERE AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) ORDER BY LastModifiedDate DESC LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0] // empty' 2>/dev/null || echo \"\")\n\nBOT_DEF_ID=$(echo \"$BOT_DEF\" | jq -r '.Id // empty' 2>/dev/null || echo \"\")\nBOT_DEF_NAME=$(echo \"$BOT_DEF\" | jq -r '.DeveloperName // empty' 2>/dev/null || echo \"\")\n\n# BOT_DEF_ID is interpolated into later SOQL; reject anything that is not a\n# valid Salesforce record Id (15 or 18 char alphanumeric).\nif [[ -n \"$BOT_DEF_ID\" && ! \"$BOT_DEF_ID\" =~ ^[a-zA-Z0-9]{15,18}$ ]]; then\n log_fail \"Invalid BotDefinition ID format: $BOT_DEF_ID\"\n exit 1\nfi\n\nif [[ -z \"$BOT_DEF_ID\" ]]; then\n log_warn \"No Pipeline Management BotDefinition found — cannot define Agent Access yet.\"\n echo \" Agent Access can only be defined after the agent exists and has an Active version.\"\n echo \" Create the agent first (./create-agent.sh $ORG_ALIAS), then re-run this script.\"\n exit 0\nfi\n\n# Separately confirm the bot has an Active BotVersion (flat query on BotVersion,\n# no correlated subquery). Agent Access on an inactive bot has no runtime effect.\nACTIVE_VERSION_COUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM BotVersion WHERE BotDefinitionId = '${BOT_DEF_ID}' AND Status = 'Active'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\nACTIVE_VERSION_COUNT=\"${ACTIVE_VERSION_COUNT:-0}\"\n\nif [[ \"$ACTIVE_VERSION_COUNT\" -lt 1 ]]; then\n log_warn \"Agent BotDefinition '${BOT_DEF_NAME:-\u003cunknown>}' has no Active BotVersion — cannot define Agent Access yet.\"\n echo \" Activate the agent version first, then re-run this script.\"\n exit 0\nfi\nlog_pass \"Found active agent BotDefinition: ${BOT_DEF_NAME:-\u003cunknown>} ($BOT_DEF_ID)\"\n\n# ------------------------------------------------------------\n# Step 1: Create the custom permission set (idempotent)\n# PermissionSet is createable via the STANDARD Data API (NOT Tooling).\n# Omitting License leaves LicenseId null (= \"None\"), which is required so the\n# permset can be added as a component of the managed PSG.\n# ------------------------------------------------------------\nPS_ID=$(sf data query \\\n --query \"SELECT Id FROM PermissionSet WHERE Name = '${ACCESS_PS_NAME}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\nif [[ -n \"$PS_ID\" ]]; then\n log_pass \"Custom permission set '${ACCESS_PS_NAME}' already exists ($PS_ID)\"\nelse\n log_try \"Creating custom permission set '${ACCESS_PS_NAME}' (License=None)...\"\n CREATE_PS=$(sf data create record \\\n --sobject PermissionSet \\\n --values \"Name='${ACCESS_PS_NAME}' Label='${ACCESS_PS_LABEL}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null || echo '{\"status\":1}')\n\n PS_ID=$(echo \"$CREATE_PS\" | jq -r '.result.id // empty' 2>/dev/null || echo \"\")\n if [[ -z \"$PS_ID\" ]]; then\n # Re-query in case of a race / partial output. A concurrent create or a\n # not-yet-visible write can make the row lag briefly, so retry with a short\n # backoff rather than giving up after a single lookup.\n for _ in $(seq 1 5); do\n PS_ID=$(sf data query \\\n --query \"SELECT Id FROM PermissionSet WHERE Name = '${ACCESS_PS_NAME}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n [[ -n \"$PS_ID\" ]] && break\n sleep 2\n done\n fi\n\n if [[ -z \"$PS_ID\" ]]; then\n log_fail \"Could not create permission set '${ACCESS_PS_NAME}'\"\n echo \"$CREATE_PS\" | jq -r '.message // .result.message // empty' 2>/dev/null || true\n exit 1\n fi\n log_pass \"Created custom permission set '${ACCESS_PS_NAME}' ($PS_ID)\"\nfi\n\n# ------------------------------------------------------------\n# Step 2: Add the agent to Agent Access on that permission set\n# SetupEntityAccess: Create/Delete/Query only (NO Update). SetupEntityType is\n# derived from SetupEntityId — do NOT set it on insert. BotDefinition is a valid\n# SetupEntityType only at API v64.0+ (sf CLI default is well above this).\n# ------------------------------------------------------------\nEXISTING_ACCESS=$(sf data query \\\n --query \"SELECT Id FROM SetupEntityAccess WHERE ParentId = '${PS_ID}' AND SetupEntityId = '${BOT_DEF_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\nif [[ -n \"$EXISTING_ACCESS\" ]]; then\n log_pass \"Agent Access already granted to '${ACCESS_PS_NAME}' ($EXISTING_ACCESS)\"\nelse\n log_try \"Granting Agent Access (SetupEntityAccess) to '${ACCESS_PS_NAME}'...\"\n CREATE_ACCESS=$(sf data create record \\\n --sobject SetupEntityAccess \\\n --values \"ParentId='${PS_ID}' SetupEntityId='${BOT_DEF_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null || echo '{\"status\":1}')\n\n # The query-then-create above is non-atomic: a concurrent run can insert the\n # same grant between our SELECT and INSERT, so tolerate DUPLICATE_VALUE — the\n # grant exists either way, which is the outcome we want (idempotent).\n if echo \"$CREATE_ACCESS\" | jq -e '.result.id // empty' >/dev/null 2>&1; then\n log_pass \"Agent Access granted (agent added to '${ACCESS_PS_NAME}')\"\n elif echo \"$CREATE_ACCESS\" | jq -r '.message // .result.message // \"\"' 2>/dev/null | grep -qi \"DUPLICATE_VALUE\"; then\n log_pass \"Agent Access already granted (duplicate insert ignored)\"\n else\n ACCESS_ERR=$(echo \"$CREATE_ACCESS\" | jq -r '.message // .result.message // \"unknown\"' 2>/dev/null || echo \"unknown\")\n log_fail \"Could not grant Agent Access: $ACCESS_ERR\"\n echo \" SetupEntityAccess requires a CUSTOM permission set and API v64.0+.\"\n exit 1\n fi\nfi\n\n# ------------------------------------------------------------\n# Step 3: Add the custom permset as a component of the managed PSG\n# PermissionSetGroupComponent is a TOOLING API object — must use --use-tooling-api.\n# ------------------------------------------------------------\nPSG_ID=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetGroup WHERE DeveloperName = '${USER_PSG_NAME}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\nCOMPONENT_LINKED=0\nif [[ -z \"$PSG_ID\" ]]; then\n log_warn \"PSG '${USER_PSG_NAME}' not found — skipping component link + recalc.\"\n echo \" The custom permission set '${ACCESS_PS_NAME}' still carries Agent Access;\"\n echo \" it can be assigned directly to users as a fallback (see below).\"\nelse\n EXISTING_COMPONENT=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetGroupComponent WHERE PermissionSetGroupId = '${PSG_ID}' AND PermissionSetId = '${PS_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\n if [[ -n \"$EXISTING_COMPONENT\" ]]; then\n log_pass \"'${ACCESS_PS_NAME}' is already a component of '${USER_PSG_NAME}'\"\n COMPONENT_LINKED=1\n else\n log_try \"Adding '${ACCESS_PS_NAME}' as a component of '${USER_PSG_NAME}'...\"\n CREATE_COMPONENT=$(sf data create record \\\n --sobject PermissionSetGroupComponent \\\n --values \"PermissionSetGroupId='${PSG_ID}' PermissionSetId='${PS_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null || echo '{\"status\":1}')\n\n if echo \"$CREATE_COMPONENT\" | jq -e '.result.id // empty' >/dev/null 2>&1; then\n log_pass \"Linked '${ACCESS_PS_NAME}' into '${USER_PSG_NAME}'\"\n COMPONENT_LINKED=1\n elif echo \"$CREATE_COMPONENT\" | jq -r '.message // .result.message // \"\"' 2>/dev/null | grep -qi \"DUPLICATE_VALUE\"; then\n log_pass \"'${ACCESS_PS_NAME}' already a component of '${USER_PSG_NAME}' (duplicate insert ignored)\"\n COMPONENT_LINKED=1\n else\n COMPONENT_ERR=$(echo \"$CREATE_COMPONENT\" | jq -r '.message // .result.message // \"unknown\"' 2>/dev/null || echo \"unknown\")\n log_warn \"Could not link custom permset into managed PSG: $COMPONENT_ERR\"\n echo \" Falling back to direct assignment of '${ACCESS_PS_NAME}' to PSG users.\"\n fi\n fi\nfi\n\n# ------------------------------------------------------------\n# Step 3b: Add the custom permset as a component of the agent-user PSG too.\n# The autonomous agent user runs the suggestion flow AS the agent, so it also\n# needs Agent Access — wire the same custom permset into SalesManagementAgentUserPsg.\n# Mirrors Step 3 (Tooling API object; idempotent query-then-skip). No direct-\n# assignment fallback: the agent user is a single system user, so the component\n# link is the right (and only) path.\n# ------------------------------------------------------------\nAGENT_COMPONENT_LINKED=0\nAGENT_PSG_ID=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetGroup WHERE DeveloperName = '${AGENT_PSG_NAME}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\nif [[ -z \"$AGENT_PSG_ID\" ]]; then\n log_warn \"PSG '${AGENT_PSG_NAME}' not found — skipping agent component link + recalc.\"\nelse\n EXISTING_AGENT_COMPONENT=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetGroupComponent WHERE PermissionSetGroupId = '${AGENT_PSG_ID}' AND PermissionSetId = '${PS_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\n if [[ -n \"$EXISTING_AGENT_COMPONENT\" ]]; then\n log_pass \"'${ACCESS_PS_NAME}' is already a component of '${AGENT_PSG_NAME}'\"\n AGENT_COMPONENT_LINKED=1\n else\n log_try \"Adding '${ACCESS_PS_NAME}' as a component of '${AGENT_PSG_NAME}'...\"\n CREATE_AGENT_COMPONENT=$(sf data create record \\\n --sobject PermissionSetGroupComponent \\\n --values \"PermissionSetGroupId='${AGENT_PSG_ID}' PermissionSetId='${PS_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null || echo '{\"status\":1}')\n\n if echo \"$CREATE_AGENT_COMPONENT\" | jq -e '.result.id // empty' >/dev/null 2>&1; then\n log_pass \"Linked '${ACCESS_PS_NAME}' into '${AGENT_PSG_NAME}'\"\n AGENT_COMPONENT_LINKED=1\n elif echo \"$CREATE_AGENT_COMPONENT\" | jq -r '.message // .result.message // \"\"' 2>/dev/null | grep -qi \"DUPLICATE_VALUE\"; then\n log_pass \"'${ACCESS_PS_NAME}' already a component of '${AGENT_PSG_NAME}' (duplicate insert ignored)\"\n AGENT_COMPONENT_LINKED=1\n else\n AGENT_COMPONENT_ERR=$(echo \"$CREATE_AGENT_COMPONENT\" | jq -r '.message // .result.message // \"unknown\"' 2>/dev/null || echo \"unknown\")\n log_warn \"Could not link custom permset into agent PSG: $AGENT_COMPONENT_ERR\"\n fi\n fi\nfi\n\n# ------------------------------------------------------------\n# Step 4: Confirm the PSG recalculates so the added component takes effect.\n# The PermissionSetGroupComponent insert in Step 3 is what actually TRIGGERS the\n# async recalculation — PermissionSetGroup.Status is a system-computed field\n# (Updated/Updating/Outdated/Failed), NOT client-writable. There is no CLI recalc\n# verb, so we simply POLL Status until it returns to 'Updated'. A Failed status\n# means Agent Access is NOT live and must be surfaced as a hard error.\n# ------------------------------------------------------------\n# The PermissionSetGroupComponent insert in Step 3 IS the recalc trigger — the\n# async recalculation is already under way and the org converges on its own; we\n# deliberately do NOT block waiting for it (recalc completion only matters when a\n# user later launches the agent). We read Status once, for reporting.\nif [[ \"$COMPONENT_LINKED\" -eq 1 && -n \"$PSG_ID\" ]]; then\n log_try \"Recalculation triggered for '${USER_PSG_NAME}' (component linked).\"\n PSG_STATUS=$(psg_status \"$ORG_ALIAS\" \"$USER_PSG_NAME\")\n case \"$PSG_STATUS\" in\n Updated)\n log_pass \"PSG '${USER_PSG_NAME}' Status: Updated\" ;;\n Failed)\n log_fail \"PSG '${USER_PSG_NAME}' recalculation Status: Failed — Agent Access not active\"\n exit 1 ;;\n *)\n log_info \"PSG '${USER_PSG_NAME}' Status: ${PSG_STATUS} — recalculation is async and will complete on its own.\"\n echo \" Agent Access takes effect once Status reaches 'Updated' (verify-all.sh reports it).\" ;;\n esac\nfi\n\n# ------------------------------------------------------------\n# Step 4b: Report the agent-user PSG recalc, mirroring Step 4 (read Status once,\n# never sleep-poll — the Step 3b component insert already triggered the async\n# recalculation).\n# ------------------------------------------------------------\nif [[ \"$AGENT_COMPONENT_LINKED\" -eq 1 && -n \"$AGENT_PSG_ID\" ]]; then\n log_try \"Recalculation triggered for '${AGENT_PSG_NAME}' (component linked).\"\n AGENT_PSG_STATUS=$(psg_status \"$ORG_ALIAS\" \"$AGENT_PSG_NAME\")\n case \"$AGENT_PSG_STATUS\" in\n Updated)\n log_pass \"PSG '${AGENT_PSG_NAME}' Status: Updated\" ;;\n Failed)\n log_fail \"PSG '${AGENT_PSG_NAME}' recalculation Status: Failed — Agent Access not active\"\n exit 1 ;;\n *)\n log_info \"PSG '${AGENT_PSG_NAME}' Status: ${AGENT_PSG_STATUS} — recalculation is async and will complete on its own.\"\n echo \" Agent Access takes effect once Status reaches 'Updated' (verify-all.sh reports it).\" ;;\n esac\nfi\n\n# ------------------------------------------------------------\n# Fallback: if the component link failed, assign the custom permset directly to\n# the users who hold the managed PSG so they still get Agent Access.\n# ------------------------------------------------------------\nif [[ \"$COMPONENT_LINKED\" -eq 0 && -n \"$PSG_ID\" ]]; then\n log_info \"Fallback: assigning '${ACCESS_PS_NAME}' directly to users of '${USER_PSG_NAME}'...\"\n # LIMIT 200 caps the fan-out: each user below costs one sequential API call, so\n # an unbounded query on a large org could issue hundreds of calls. The linked\n # component (Step 3) is the scalable path; this direct-assignment fallback is a\n # best-effort stopgap. If the PSG has >200 users, prefer fixing the component\n # link so recalculation reaches everyone at once.\n ASSIGNEE_IDS=$(sf data query \\\n --query \"SELECT AssigneeId FROM PermissionSetAssignment WHERE PermissionSetGroup.DeveloperName = '${USER_PSG_NAME}' LIMIT 200\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[].AssigneeId' 2>/dev/null || echo \"\")\n\n if [[ -z \"$ASSIGNEE_IDS\" ]]; then\n log_warn \"No users currently hold '${USER_PSG_NAME}' — nothing to assign.\"\n else\n # NOTE: do not name this loop var UID — UID is a readonly special var in bash.\n while IFS= read -r ASSIGNEE_ID; do\n [[ -z \"$ASSIGNEE_ID\" ]] && continue\n CREATE_RESULT=$(sf data create record \\\n --sobject PermissionSetAssignment \\\n --values \"AssigneeId='${ASSIGNEE_ID}' PermissionSetId='${PS_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null || echo '{\"status\":1}')\n\n # Distinguish \"already assigned\" (benign) from real failures like a\n # deactivated user or license conflict — don't paper over the latter.\n RESULT_MSG=$(echo \"$CREATE_RESULT\" | jq -r '.message // .result.message // \"\"' 2>/dev/null || echo \"\")\n if echo \"$CREATE_RESULT\" | jq -e '.result.id // empty' >/dev/null 2>&1; then\n log_pass \"Assigned '${ACCESS_PS_NAME}' to user $ASSIGNEE_ID\"\n elif echo \"$RESULT_MSG\" | grep -qi \"DUPLICATE_VALUE\"; then\n log_info \"Already assigned: $ASSIGNEE_ID\"\n elif echo \"$RESULT_MSG\" | grep -qi \"FIELD_INTEGRITY_EXCEPTION\"; then\n log_warn \"License conflict for $ASSIGNEE_ID (FIELD_INTEGRITY_EXCEPTION) — user may need a license upgrade\"\n elif echo \"$RESULT_MSG\" | grep -qi \"INVALID_CROSS_REFERENCE_KEY\"; then\n log_warn \"Assignment for $ASSIGNEE_ID rejected (INVALID_CROSS_REFERENCE_KEY) — the PSG has not finished recalculating (Status not yet 'Updated'). Re-run after recalculation completes.\"\n else\n ASSIGN_ERR=\"${RESULT_MSG:-unknown}\"\n log_warn \"Failed to assign to $ASSIGNEE_ID: $ASSIGN_ERR\"\n fi\n done \u003c\u003c\u003c \"$ASSIGNEE_IDS\"\n fi\nfi\n\necho \"\"\necho \"=== Agent Access definition complete ===\"\necho \"\"\necho \" Custom permission set : ${ACCESS_PS_NAME} ($PS_ID)\"\necho \" Agent : ${BOT_DEF_NAME:-\u003cunknown>} ($BOT_DEF_ID)\"\necho \" Target PSG : ${USER_PSG_NAME}\"\necho \" Agent PSG : ${AGENT_PSG_NAME}\"\necho \"\"\necho \" Users holding '${USER_PSG_NAME}' can launch the Sales Agent once the PSG\"\necho \" recalculation completes (Status 'Updated'). The recalc runs asynchronously.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":21679,"content_sha256":"707d5d5a10e6e9cfaf11d8579c0e7ef91d2b0491ff0f70f737cbd6f844981dc2"},{"filename":"scripts/deploy-settings.sh","content":"#!/bin/bash\n\n# Deploy Pipeline Management settings from local metadata files\n# Usage: ./deploy-settings.sh \u003corg-alias>\n\nset -euo pipefail\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\nORG_ALIAS=\"${1:-}\"\n\nif [[ -z \"$ORG_ALIAS\" ]]; then\n echo \"❌ Error: Missing org alias\"\n echo \"Usage: $0 \u003corg-alias>\"\n exit 1\nfi\n\nif [[ ! -f \"sfdx-project.json\" ]]; then\n echo \"❌ Error: sfdx-project.json not found in current directory\"\n echo \"Copy from ../assets/sfdx-project.json first\"\n exit 1\nfi\n\nSETTINGS_DIR=\"force-app/main/default/settings\"\n\nif [[ ! -d \"$SETTINGS_DIR\" ]]; then\n echo \"❌ Error: Settings directory not found: $SETTINGS_DIR\"\n echo \"Run ./retrieve-settings.sh first to retrieve settings\"\n exit 1\nfi\n\necho \"📤 Deploying Pipeline Management settings to org: $ORG_ALIAS\"\necho \"\"\n\n# Count available settings files\nSETTINGS_COUNT=$(find \"$SETTINGS_DIR\" -name \"*.settings-meta.xml\" -type f | wc -l | tr -d ' ')\n\nif [[ \"$SETTINGS_COUNT\" -eq 0 ]]; then\n echo \"❌ No settings files found in $SETTINGS_DIR\"\n exit 1\nfi\n\necho \"Found $SETTINGS_COUNT settings file(s) to deploy\"\necho \"\"\n\n# Deploy all settings\necho \"Deploying settings...\"\n\nif sf project deploy start \\\n --source-dir \"$SETTINGS_DIR\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -e '.status == 0' >/dev/null 2>&1; then\n echo \"✅ Settings deployed successfully\"\nelse\n echo \"❌ Deployment failed\"\n echo \"\"\n echo \"Retrying with verbose output...\"\n sf project deploy start \\\n --source-dir \"$SETTINGS_DIR\" \\\n --target-org \"$ORG_ALIAS\"\n exit 1\nfi\n\necho \"\"\n\n# List deployed files\necho \"Deployed settings:\"\nfind \"$SETTINGS_DIR\" -name \"*.settings-meta.xml\" -type f | while read -r FILE; do\n BASENAME=$(basename \"$FILE\" .settings-meta.xml)\n echo \" ✅ $BASENAME\"\ndone\n\necho \"\"\necho \"✅ Deployment complete\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1960,"content_sha256":"2f76d1945cd0bb453897279dbec8283916b25c15ab98b436f92f6524a3a3bf55"},{"filename":"scripts/enable-deal-agent.sh","content":"#!/bin/bash\n\n# Enable Pipeline Management (SalesDealAgentSettings) via SOAP API\n# Usage: ./enable-deal-agent.sh \u003corg-alias>\n\nset -euo pipefail\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\nORG_ALIAS=\"${1:-}\"\n\nif [[ -z \"$ORG_ALIAS\" ]]; then\n echo \"❌ Error: Missing org alias\"\n echo \"Usage: $0 \u003corg-alias>\"\n exit 1\nfi\n\necho \"🚀 Enabling Pipeline Management for org: $ORG_ALIAS\"\necho \"\"\n\n# Get org details\nORG_INFO=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\nORG_ID=$(echo \"$ORG_INFO\" | jq -r '.result.id')\nINSTANCE_URL=$(echo \"$ORG_INFO\" | jq -r '.result.instanceUrl')\nACCESS_TOKEN=$(echo \"$ORG_INFO\" | jq -r '.result.accessToken')\n\n# Newer CLI versions redact accessToken — use show-access-token fallback\nif [[ -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" || \"$ACCESS_TOKEN\" == *\"REDACTED\"* ]]; then\n ACCESS_TOKEN=$(echo \"y\" | sf org auth show-access-token --target-org \"$ORG_ALIAS\" --no-prompt --json 2>/dev/null | jq -r '.result.accessToken // empty')\nfi\n\nif [[ -z \"$ORG_ID\" || -z \"$INSTANCE_URL\" || -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" ]]; then\n echo \"Error: Could not retrieve org details. Is the org authenticated?\"\n exit 1\nfi\n\necho \"Org ID: $ORG_ID\"\necho \"Instance URL: $INSTANCE_URL\"\necho \"\"\n\n# Pre-flight checks\necho \"🔍 Running pre-flight checks...\"\necho \"\"\n\nPREFLIGHT_FAILED=false\n\n# Check Einstein GPT is enabled (critical) — via SOAP readMetadata\nGENAI_CHECK=$(curl -s \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>EinsteinGptSettings\u003c/met:type>\u003cmet:fullNames>EinsteinGpt\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null | grep -o \"\u003cenableEinsteinGptPlatform>[^\u003c]*\" | grep -o \"true\\|false\" || echo \"\")\n\nif [[ \"$GENAI_CHECK\" == \"true\" ]]; then\n echo \" ✅ Einstein GPT enabled\"\nelse\n echo \" ❌ Einstein GPT NOT enabled (required)\"\n PREFLIGHT_FAILED=true\nfi\n\n# Check Agentforce Agent is enabled (required) — via SOAP readMetadata\nCOPILOT_CHECK=$(curl -s \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>EinsteinCopilotSettings\u003c/met:type>\u003cmet:fullNames>EinsteinCopilot\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null | grep -o \"\u003cenableEinsteinGptCopilot>[^\u003c]*\" | grep -o \"true\\|false\" || echo \"\")\n\nif [[ \"$COPILOT_CHECK\" == \"true\" ]]; then\n echo \" ✅ Agentforce Agent enabled\"\nelse\n echo \" ❌ Agentforce Agent NOT enabled (required)\"\n PREFLIGHT_FAILED=true\nfi\n\n# Check Agentforce Studio / Agent Platform is enabled (required) — via SOAP readMetadata.\n# AgentPlatformSettings.enableAgentPlatform (OrgPreference AgentPlatformEnabled) gates\n# SalesDealAgentSettings: core's DealAgentEnabledOrgPreference validates the Agentforce\n# Platform preference and rejects Deal Agent enablement if it is off, so this must pass\n# before we attempt the SalesDealAgent update below.\nAGENT_PLATFORM_CHECK=$(curl -s \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>AgentPlatformSettings\u003c/met:type>\u003cmet:fullNames>AgentPlatform\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null | grep -o \"\u003cenableAgentPlatform>[^\u003c]*\" | grep -o \"true\\|false\" || echo \"\")\n\nif [[ \"$AGENT_PLATFORM_CHECK\" == \"true\" ]]; then\n echo \" ✅ Agentforce Studio (Agent Platform) enabled\"\nelse\n echo \" ❌ Agentforce Studio (Agent Platform) NOT enabled (required — gates Deal Agent)\"\n PREFLIGHT_FAILED=true\nfi\n\n# Check Enhanced Notes (required dependency) — via SOAP readMetadata\nNOTES_CHECK=$(curl -s \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>EnhancedNotesSettings\u003c/met:type>\u003cmet:fullNames>EnhancedNotes\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null | grep -o \"\u003cenableEnhancedNotes>[^\u003c]*\" | grep -o \"true\\|false\" || echo \"\")\n\nif [[ \"$NOTES_CHECK\" == \"true\" ]]; then\n echo \" ✅ Enhanced Notes enabled\"\nelse\n echo \" ❌ Enhanced Notes NOT enabled (required)\"\n PREFLIGHT_FAILED=true\nfi\n\n# Check org edition compatibility\nORG_EDITION=$(sf data query --query \"SELECT OrganizationType FROM Organization\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].OrganizationType')\necho \" ℹ️ Org Edition: $ORG_EDITION\"\n\nif [[ \"$ORG_EDITION\" == \"Developer Edition\" ]]; then\n echo \" ⚠️ Warning: Pipeline Management may have limited functionality in Developer Edition\"\nfi\n\necho \"\"\n\nif [[ \"$PREFLIGHT_FAILED\" == \"true\" ]]; then\n echo \"❌ Pre-flight checks failed. Run ./enable-prerequisites.sh first.\"\n exit 1\nfi\n\necho \"✅ Pre-flight checks passed\"\necho \"\"\n\n# Check if already enabled (via SOAP readMetadata — SalesDealAgentSettings is NOT queryable via SOQL)\necho -n \"Checking current SalesDealAgent status... \"\nCURRENT_STATUS=$(curl -s \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>SalesDealAgentSettings\u003c/met:type>\u003cmet:fullNames>SalesDealAgent\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null | grep -o \"\u003cenableDealAgent>[^\u003c]*\" | grep -o \"true\\|false\" || echo \"\")\n\nif [[ \"$CURRENT_STATUS\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\n echo \"\"\n echo \"✅ Pipeline Management is already enabled\"\n exit 0\nfi\n\necho \"⏳ (not enabled)\"\necho \"\"\n\n# Enable via SOAP API v64.0\necho \"Enabling Pipeline Management via SOAP API v64.0...\"\necho \"\"\n\nSOAP_REQUEST=$(cat \u003c\u003c'EOF'\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?>\n\u003csoapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:met=\"http://soap.sforce.com/2006/04/metadata\">\n \u003csoapenv:Header>\n \u003cmet:SessionHeader>\n \u003cmet:sessionId>{{ACCESS_TOKEN}}\u003c/met:sessionId>\n \u003c/met:SessionHeader>\n \u003c/soapenv:Header>\n \u003csoapenv:Body>\n \u003cmet:updateMetadata>\n \u003cmet:metadata xsi:type=\"met:SalesDealAgentSettings\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n \u003cmet:fullName>SalesDealAgent\u003c/met:fullName>\n \u003cmet:enableDealAgent>true\u003c/met:enableDealAgent>\n \u003cmet:enableDealAgentAutoApproveAllTasks>false\u003c/met:enableDealAgentAutoApproveAllTasks>\n \u003c/met:metadata>\n \u003c/met:updateMetadata>\n \u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\nEOF\n)\n\n# Substitute access token\nSOAP_REQUEST=\"${SOAP_REQUEST//\\{\\{ACCESS_TOKEN\\}\\}/$ACCESS_TOKEN}\"\n\n# Send SOAP request\nRESPONSE=$(curl -s -X POST \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=utf-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"$SOAP_REQUEST\")\n\n# Check response\nif echo \"$RESPONSE\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n echo \"✅ SOAP API call succeeded\"\nelse\n echo \"❌ SOAP API call failed\"\n echo \"\"\n echo \"Response:\"\n echo \"$RESPONSE\"\n exit 1\nfi\n\necho \"\"\n\n# Verify enablement (via SOAP readMetadata)\necho \"🔍 Verifying Pipeline Management is enabled...\"\nsleep 2 # Brief wait for propagation\n\nVERIFY_RESULT=$(curl -s \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>SalesDealAgentSettings\u003c/met:type>\u003cmet:fullNames>SalesDealAgent\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\n\nVERIFY_DEAL_AGENT=$(echo \"$VERIFY_RESULT\" | grep -o \"\u003cenableDealAgent>[^\u003c]*\" | grep -o \"true\\|false\" || echo \"\")\n\nif [[ \"$VERIFY_DEAL_AGENT\" == \"true\" ]]; then\n echo \" ✅ enableDealAgent: true\"\n\n AUTO_APPROVE=$(echo \"$VERIFY_RESULT\" | grep -o \"\u003cenableDealAgentAutoApproveAllTasks>[^\u003c]*\" | grep -o \"true\\|false\" || echo \"false\")\n echo \" ℹ️ enableDealAgentAutoApproveAllTasks: $AUTO_APPROVE (default: false)\"\n\n echo \"\"\n echo \"✅ Pipeline Management enabled successfully\"\n echo \"\"\n echo \"Next steps:\"\n echo \" 1. Run ./create-flow.sh $ORG_ALIAS to create the suggestion processing flow\"\n echo \" 2. Run ./create-agent.sh $ORG_ALIAS \u003cbundle-path> to create and activate the agent\"\nelse\n echo \" ❌ Verification failed\"\n echo \"\"\n echo \"Query result:\"\n echo \"$VERIFY_RESULT\"\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":10283,"content_sha256":"fd929367a5de9ebbea706281912c4716e3c83a84fb2bdd089e9817ed5ba5c339"},{"filename":"scripts/enable-prerequisites.sh","content":"#!/bin/bash\n\n# Enable all prerequisite settings for Pipeline Management\n# Usage: ./enable-prerequisites.sh \u003corg-alias>\n\nset -euo pipefail\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\n# Source shared SOAP parsing library\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"$SCRIPT_DIR/shared/soap.sh\"\n\nORG_ALIAS=\"${1:-}\"\n\nif [[ -z \"$ORG_ALIAS\" ]]; then\n echo \"❌ Error: Missing org alias\"\n echo \"Usage: $0 \u003corg-alias>\"\n exit 1\nfi\n\nif [[ ! -f \"sfdx-project.json\" ]]; then\n echo \"❌ Error: sfdx-project.json not found in current directory\"\n echo \"Copy from ../assets/sfdx-project.json first\"\n exit 1\nfi\n\necho \"🚀 Enabling Pipeline Management prerequisites for org: $ORG_ALIAS\"\necho \"\"\n\n# Get org details\nORG_INFO=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\nORG_ID=$(echo \"$ORG_INFO\" | jq -r '.result.id')\nINSTANCE_URL=$(echo \"$ORG_INFO\" | jq -r '.result.instanceUrl')\nACCESS_TOKEN=$(echo \"$ORG_INFO\" | jq -r '.result.accessToken')\n\n# Newer CLI versions redact accessToken — use show-access-token fallback\nif [[ -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" || \"$ACCESS_TOKEN\" == *\"REDACTED\"* ]]; then\n ACCESS_TOKEN=$(echo \"y\" | sf org auth show-access-token --target-org \"$ORG_ALIAS\" --no-prompt --json 2>/dev/null | jq -r '.result.accessToken // empty')\nfi\n\nif [[ -z \"$ORG_ID\" || -z \"$INSTANCE_URL\" || -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" ]]; then\n echo \"Error: Could not retrieve org details. Is the org authenticated?\"\n exit 1\nfi\n\necho \"Org ID: $ORG_ID\"\necho \"Instance URL: $INSTANCE_URL\"\necho \"\"\n\n# Function to enable setting via SOAP API\nenable_via_soap() {\n local SETTING_NAME=\"$1\"\n local DISPLAY_NAME=\"$2\"\n local FIELD_NAME=\"$3\"\n local API_VERSION=\"${4:-62.0}\"\n\n echo -n \"Enabling ${DISPLAY_NAME}... \"\n\n # Check if already enabled\n local QUERY=\"SELECT ${FIELD_NAME} FROM ${SETTING_NAME}\"\n local CURRENT_VALUE\n CURRENT_VALUE=$(sf data query --query \"$QUERY\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r \".result.records[0].${FIELD_NAME}\")\n\n if [[ \"$CURRENT_VALUE\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\n return 0\n fi\n\n # Build SOAP request\n local SOAP_REQUEST\n SOAP_REQUEST=$(cat \u003c\u003cEOF\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?>\n\u003csoapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:met=\"http://soap.sforce.com/2006/04/metadata\">\n \u003csoapenv:Header>\n \u003cmet:SessionHeader>\n \u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\n \u003c/met:SessionHeader>\n \u003c/soapenv:Header>\n \u003csoapenv:Body>\n \u003cmet:updateMetadata>\n \u003cmet:metadata xsi:type=\"met:${SETTING_NAME}\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n \u003cmet:fullName>${SETTING_NAME}\u003c/met:fullName>\n \u003cmet:${FIELD_NAME}>true\u003c/met:${FIELD_NAME}>\n \u003c/met:metadata>\n \u003c/met:updateMetadata>\n \u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\nEOF\n)\n\n # Send SOAP request\n local RESPONSE\n RESPONSE=$(curl -s -X POST \"${INSTANCE_URL}/services/Soap/m/${API_VERSION}\" \\\n -H \"Content-Type: text/xml; charset=utf-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"$SOAP_REQUEST\")\n\n if echo \"$RESPONSE\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n echo \"✅\"\n return 0\n else\n echo \"❌\"\n echo \"SOAP Response: $RESPONSE\"\n return 1\n fi\n}\n\n# Function to enable setting via CLI retrieve+modify+deploy\nenable_via_cli() {\n local SETTING_NAME=\"$1\"\n local DISPLAY_NAME=\"$2\"\n local FIELD_NAME=\"$3\"\n local FIELD_VALUE=\"${4:-true}\"\n\n echo -n \"Enabling ${DISPLAY_NAME}... \"\n\n # Check if already enabled\n local QUERY=\"SELECT ${FIELD_NAME} FROM ${SETTING_NAME}\"\n local CURRENT_VALUE\n CURRENT_VALUE=$(sf data query --query \"$QUERY\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r \".result.records[0].${FIELD_NAME}\" 2>/dev/null || echo \"null\")\n\n if [[ \"$CURRENT_VALUE\" == \"$FIELD_VALUE\" || \"$CURRENT_VALUE\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\n return 0\n fi\n\n # Retrieve current settings\n local SETTINGS_DIR=\"force-app/main/default/settings\"\n mkdir -p \"$SETTINGS_DIR\"\n\n if ! sf project retrieve start --metadata \"Settings:${SETTING_NAME}\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -e '.status == 0' >/dev/null 2>&1; then\n echo \"❌ (retrieve failed)\"\n return 1\n fi\n\n local SETTINGS_FILE=\"${SETTINGS_DIR}/${SETTING_NAME}.settings-meta.xml\"\n\n if [[ ! -f \"$SETTINGS_FILE\" ]]; then\n echo \"❌ (file not found after retrieve)\"\n return 1\n fi\n\n # Modify setting using sed (non-greedy)\n if grep -q \"\u003c${FIELD_NAME}>\" \"$SETTINGS_FILE\"; then\n # Update existing field\n sed -i.bak \"0,/\u003c${FIELD_NAME}>[^\u003c]*\u003c\\/${FIELD_NAME}>/s|\u003c${FIELD_NAME}>[^\u003c]*\u003c/${FIELD_NAME}>|\u003c${FIELD_NAME}>${FIELD_VALUE}\u003c/${FIELD_NAME}>|\" \"$SETTINGS_FILE\"\n else\n # Insert field before closing tag\n sed -i.bak \"/\u003c\\/.*Settings>/i\\\\\n \u003c${FIELD_NAME}>${FIELD_VALUE}\u003c/${FIELD_NAME}>\" \"$SETTINGS_FILE\"\n fi\n\n # Deploy\n if sf project deploy start --source-dir \"$SETTINGS_DIR\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -e '.status == 0' >/dev/null 2>&1; then\n echo \"✅\"\n rm -f \"${SETTINGS_FILE}.bak\"\n return 0\n else\n echo \"❌ (deploy failed)\"\n mv \"${SETTINGS_FILE}.bak\" \"$SETTINGS_FILE\" 2>/dev/null || true\n return 1\n fi\n}\n\n# Read a settings boolean via SOAP readMetadata and classify the response using\n# the shared shared/soap.sh parser. Distinguishes a genuine \"false\" from an\n# auth/SOAP fault or a network/service error instead of collapsing all three to\n# empty (the W-23215519 bug). Captures the HTTP status so 5xx/HTML error pages\n# are not mistaken for a disabled setting.\n# Args: $1 = metadata type, $2 = fullName, $3 = field name\n# Echoes: \"true\" | \"false\" | \"AUTH_ERROR:...\" | \"TYPE_UNAVAILABLE:...\" | \"NETWORK_ERROR\"\nread_setting_status() {\n local type=\"$1\"\n local full_name=\"$2\"\n local field=\"$3\"\n local response http_status xml\n response=$(curl -s -w \"\\n%{http_code}\" \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>${type}\u003c/met:type>\u003cmet:fullNames>${full_name}\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null || echo \"\")\n http_status=$(echo \"$response\" | tail -1)\n xml=$(echo \"$response\" | sed '$d')\n parse_soap_response \"$xml\" \"$field\" \"$http_status\"\n}\n\n# Read OpportunitySettings.enableOpportunityTeam via SOAP readMetadata.\n# OpportunitySettings is NOT queryable via SOQL, so SOAP is the only option\n# (the fullName for OpportunitySettings is \"Opportunity\", not the type name).\n# Echoes a classification string (see read_setting_status).\nget_opp_team_status() {\n read_setting_status \"OpportunitySettings\" \"Opportunity\" \"enableOpportunityTeam\"\n}\n\n# Step 1: Enable EinsteinGptSettings (MUST succeed for Copilot)\n# NOTE: The critical field for Pipeline Management is enableEinsteinGptPlatform.\n# We enable it via SOAP with the correct fullName pattern (fullName = type name for settings).\necho \"Step 1: Enabling Einstein Generative AI\"\necho -n \" Checking EinsteinGptSettings... \"\n\nGENAI_CURRENT=$(read_setting_status \"EinsteinGptSettings\" \"EinsteinGpt\" \"enableEinsteinGptPlatform\")\n\nif [[ \"$GENAI_CURRENT\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\nelse\n echo \"enabling...\"\n RESULT=$(curl -s -X POST \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=utf-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:updateMetadata>\n \u003cmet:metadata xsi:type='met:EinsteinGptSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\n \u003cmet:fullName>EinsteinGpt\u003c/met:fullName>\n \u003cmet:enableEinsteinGptPlatform>true\u003c/met:enableEinsteinGptPlatform>\n \u003c/met:metadata>\n \u003c/met:updateMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\")\n\n if echo \"$RESULT\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n echo \" ✅ Einstein Generative AI enabled (SOAP)\"\n else\n echo \" ⚠️ SOAP failed — trying CLI deploy fallback...\"\n enable_via_cli \"EinsteinGptSettings\" \"Einstein Generative AI (CLI)\" \"enableEinsteinGptPlatform\"\n GENAI_VERIFY=$(read_setting_status \"EinsteinGptSettings\" \"EinsteinGpt\" \"enableEinsteinGptPlatform\")\n if [[ \"$GENAI_VERIFY\" != \"true\" ]]; then\n echo \" ❌ Critical failure: Cannot enable Einstein Generative AI (all approaches failed)\"\n echo \"$RESULT\" | grep -o '\u003cmessage>[^\u003c]*\u003c/message>' | sed 's/\u003c[^>]*>//g' | head -1 || true\n exit 1\n fi\n echo \" ✅ Einstein Generative AI enabled (CLI fallback)\"\n fi\n sleep 3\nfi\necho \"\"\n\n# Step 2: Enable EinsteinCopilotSettings (depends on GPT)\necho \"Step 2: Enabling Agentforce Agent (Einstein Copilot)\"\necho -n \" Checking EinsteinCopilotSettings... \"\n\nCOPILOT_CURRENT=$(read_setting_status \"EinsteinCopilotSettings\" \"EinsteinCopilot\" \"enableEinsteinGptCopilot\")\n\nif [[ \"$COPILOT_CURRENT\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\nelse\n echo \"enabling...\"\n RESULT=$(curl -s -X POST \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=utf-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:updateMetadata>\n \u003cmet:metadata xsi:type='met:EinsteinCopilotSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\n \u003cmet:fullName>EinsteinCopilot\u003c/met:fullName>\n \u003cmet:enableEinsteinGptCopilot>true\u003c/met:enableEinsteinGptCopilot>\n \u003c/met:metadata>\n \u003c/met:updateMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\")\n\n if echo \"$RESULT\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n echo \" ✅ Agentforce Agent enabled (SOAP)\"\n else\n echo \" ⚠️ SOAP failed — trying CLI deploy fallback...\"\n enable_via_cli \"EinsteinCopilotSettings\" \"Agentforce Agent (CLI)\" \"enableEinsteinGptCopilot\"\n COPILOT_VERIFY=$(read_setting_status \"EinsteinCopilotSettings\" \"EinsteinCopilot\" \"enableEinsteinGptCopilot\")\n if [[ \"$COPILOT_VERIFY\" != \"true\" ]]; then\n echo \" ⚠️ Warning: Agentforce Agent enable failed (all approaches — continuing)\"\n echo \"$RESULT\" | grep -o '\u003cmessage>[^\u003c]*\u003c/message>' | sed 's/\u003c[^>]*>//g' | head -1 || true\n else\n echo \" ✅ Agentforce Agent enabled (CLI fallback)\"\n fi\n fi\n sleep 3\nfi\necho \"\"\n\n# Step 2b: Enable AgentPlatformSettings (Agentforce Studio — gates Deal Agent)\n# AgentPlatformSettings.enableAgentPlatform (OrgPreference AgentPlatformEnabled)\n# must be ON before SalesDealAgentSettings can be enabled; core rejects Deal Agent\n# enablement otherwise. Depends on Einstein GPT + Copilot (Steps 1–2).\necho \"Step 2b: Enabling Agentforce Studio (Agent Platform)\"\necho -n \" Checking AgentPlatformSettings... \"\n\nAGENT_PLATFORM_CURRENT=$(read_setting_status \"AgentPlatformSettings\" \"AgentPlatform\" \"enableAgentPlatform\")\n\nif [[ \"$AGENT_PLATFORM_CURRENT\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\nelse\n echo \"enabling...\"\n RESULT=$(curl -s -X POST \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=utf-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:updateMetadata>\n \u003cmet:metadata xsi:type='met:AgentPlatformSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\n \u003cmet:fullName>AgentPlatform\u003c/met:fullName>\n \u003cmet:enableAgentPlatform>true\u003c/met:enableAgentPlatform>\n \u003c/met:metadata>\n \u003c/met:updateMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\")\n\n if echo \"$RESULT\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n echo \" ✅ Agentforce Studio (Agent Platform) enabled (SOAP)\"\n else\n echo \" ⚠️ SOAP failed — trying CLI deploy fallback...\"\n enable_via_cli \"AgentPlatformSettings\" \"Agentforce Studio (CLI)\" \"enableAgentPlatform\"\n AGENT_PLATFORM_VERIFY=$(read_setting_status \"AgentPlatformSettings\" \"AgentPlatform\" \"enableAgentPlatform\")\n if [[ \"$AGENT_PLATFORM_VERIFY\" != \"true\" ]]; then\n echo \" ⚠️ Warning: Agentforce Studio enable failed (all approaches — continuing)\"\n echo \"$RESULT\" | grep -o '\u003cmessage>[^\u003c]*\u003c/message>' | sed 's/\u003c[^>]*>//g' | head -1 || true\n else\n echo \" ✅ Agentforce Studio (Agent Platform) enabled (CLI fallback)\"\n fi\n fi\n sleep 3\nfi\necho \"\"\n\n# Step 3: Enable Enhanced Notes (required for Pipeline Management data sources)\necho \"Step 3: Enabling Enhanced Notes\"\necho -n \" Checking EnhancedNotesSettings... \"\n\nNOTES_CURRENT=$(read_setting_status \"EnhancedNotesSettings\" \"EnhancedNotes\" \"enableEnhancedNotes\")\n\nif [[ \"$NOTES_CURRENT\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\nelse\n echo \"enabling...\"\n RESULT=$(curl -s -X POST \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=utf-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:updateMetadata>\n \u003cmet:metadata xsi:type='met:EnhancedNotesSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\n \u003cmet:fullName>EnhancedNotes\u003c/met:fullName>\n \u003cmet:enableEnhancedNotes>true\u003c/met:enableEnhancedNotes>\n \u003c/met:metadata>\n \u003c/met:updateMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\")\n\n if echo \"$RESULT\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n echo \" ✅ Enhanced Notes enabled\"\n else\n echo \" ⚠️ SOAP failed — trying CLI deploy fallback...\"\n enable_via_cli \"EnhancedNotesSettings\" \"Enhanced Notes\" \"enableEnhancedNotes\" || echo \" ❌ Enhanced Notes failed (Pipeline Management may not activate)\"\n fi\n sleep 2\nfi\necho \"\"\n\n# Step 4: Enable Email Administration (CLI is OK, handle compliance field removal)\necho \"Step 4: Enabling Email Administration\"\nif ! enable_via_cli \"EmailAdministrationSettings\" \"Email Administration\" \"enableInternalNotesInEmailSnippet\"; then\n echo \"⚠️ Retrying without enableEmailSenderIdCompliance...\"\n\n # Retrieve again\n sf project retrieve start --metadata \"Settings:EmailAdministrationSettings\" --target-org \"$ORG_ALIAS\" >/dev/null 2>&1 || true\n\n SETTINGS_FILE=\"force-app/main/default/settings/EmailAdministrationSettings.settings-meta.xml\"\n\n if [[ -f \"$SETTINGS_FILE\" ]]; then\n # Remove problematic field if present\n sed -i.bak '/\u003cenableEmailSenderIdCompliance>/d' \"$SETTINGS_FILE\"\n\n # Ensure enableInternalNotesInEmailSnippet is true\n if grep -q \"\u003cenableInternalNotesInEmailSnippet>\" \"$SETTINGS_FILE\"; then\n sed -i.bak2 \"0,/\u003cenableInternalNotesInEmailSnippet>[^\u003c]*\u003c\\/enableInternalNotesInEmailSnippet>/s|\u003cenableInternalNotesInEmailSnippet>[^\u003c]*\u003c/enableInternalNotesInEmailSnippet>|\u003cenableInternalNotesInEmailSnippet>true\u003c/enableInternalNotesInEmailSnippet>|\" \"$SETTINGS_FILE\"\n fi\n\n sf project deploy start --source-dir \"force-app/main/default/settings\" --target-org \"$ORG_ALIAS\" >/dev/null 2>&1 || true\n rm -f \"${SETTINGS_FILE}.bak\" \"${SETTINGS_FILE}.bak2\" 2>/dev/null || true\n fi\nfi\necho \"\"\n\n# Step 5: Enable Opportunity Team (CRITICAL — flow uses OpportunityTeamMember)\n# Without this, the Pipeline Management flow deploy fails with an OppTeamMemRec error.\n# OpportunitySettings is NOT queryable via SOQL, so we use SOAP readMetadata/update\n# (matching Steps 1-3 and setup-all.sh). The fullName for OpportunitySettings is\n# \"Opportunity\", not the type name.\necho \"Step 5: Enabling Opportunity Team\"\necho -n \" Checking OpportunitySettings.enableOpportunityTeam... \"\n\nOPP_TEAM_CURRENT=$(get_opp_team_status)\n\nif [[ \"$OPP_TEAM_CURRENT\" == \"true\" ]]; then\n echo \"✅ (already enabled)\"\nelse\n echo \"enabling...\"\n RESULT=$(curl -s -X POST \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=utf-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:updateMetadata>\n \u003cmet:metadata xsi:type='met:OpportunitySettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\n \u003cmet:fullName>Opportunity\u003c/met:fullName>\n \u003cmet:enableOpportunityTeam>true\u003c/met:enableOpportunityTeam>\n \u003c/met:metadata>\n \u003c/met:updateMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\n\n if echo \"$RESULT\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n echo \" ✅ Opportunity Team enabled\"\n else\n echo \" ❌ Opportunity Team enable failed — verifying below...\"\n echo \"$RESULT\" | grep -o '\u003cmessage>[^\u003c]*\u003c/message>' | sed 's/\u003c[^>]*>//g' | head -1 || true\n echo \" Note: cannot coexist with Opportunity Splits — disable Splits first if enabled.\"\n fi\n sleep 2\nfi\necho \"\"\n\n# Step 6: Enable Opportunity settings (Find Similar Opportunities — optional)\necho \"Step 6: Enabling Opportunity Settings\"\nenable_via_cli \"OpportunitySettings\" \"Opportunity Settings\" \"enableFindSimilarOpportunities\" || echo \"⚠️ Warning: Opportunity Settings failed but continuing...\"\necho \"\"\n\n# Verification — use SOAP readMetadata (matches enable-deal-agent.sh pre-flight checks)\necho \"🔍 Verifying prerequisites via SOAP readMetadata...\"\necho \"\"\n\nVERIFICATION_FAILED=false\n\n# Report a verification status classified by shared/soap.sh. Distinguishes a\n# genuine disabled setting from an auth fault or a network/service error so the\n# operator knows whether to re-auth, check connectivity, or actually enable the\n# setting (the W-23215519 fix). Args: $1=label, $2=status string, $3=severity\n# severity \"critical\" → sets VERIFICATION_FAILED on a real \"false\"\n# severity \"warn\" → reports but never blocks\nreport_status() {\n local label=\"$1\"\n local status=\"$2\"\n local severity=\"$3\"\n case \"$status\" in\n true)\n echo \" ✅ ${label}: enabled\"\n ;;\n false)\n if [[ \"$severity\" == \"critical\" ]]; then\n echo \" ❌ ${label}: NOT enabled (CRITICAL)\"\n VERIFICATION_FAILED=true\n else\n echo \" ⚠️ ${label}: NOT enabled\"\n fi\n ;;\n AUTH_ERROR:*)\n echo \" ❌ ${label}: cannot verify — authentication/SOAP fault (${status#AUTH_ERROR:})\"\n echo \" Re-authenticate: sf org login web --instance-url $INSTANCE_URL --alias $ORG_ALIAS\"\n # if/then/fi (not `[[ ]] && ...`): a bare `&&` would return 1 when severity\n # is \"warn\", and as the case arm's last statement that makes report_status\n # return 1 — aborting the script at the bare warn call sites under set -e.\n if [[ \"$severity\" == \"critical\" ]]; then VERIFICATION_FAILED=true; fi\n ;;\n TYPE_UNAVAILABLE:*)\n echo \" ❌ ${label}: not available in this org edition (${status#TYPE_UNAVAILABLE:})\"\n if [[ \"$severity\" == \"critical\" ]]; then VERIFICATION_FAILED=true; fi\n ;;\n NETWORK_ERROR)\n echo \" ❌ ${label}: cannot verify — network/service error (check instance URL and connectivity)\"\n if [[ \"$severity\" == \"critical\" ]]; then VERIFICATION_FAILED=true; fi\n ;;\n *)\n echo \" ⚠️ ${label}: unknown status ($status)\"\n ;;\n esac\n}\n\n# Verify Einstein GPT (critical — checked by enable-deal-agent.sh pre-flight)\nVERIFY_GENAI=$(read_setting_status \"EinsteinGptSettings\" \"EinsteinGpt\" \"enableEinsteinGptPlatform\")\nreport_status \"Einstein Generative AI (enableEinsteinGptPlatform)\" \"$VERIFY_GENAI\" \"critical\"\n\n# Verify Einstein Copilot (checked by enable-deal-agent.sh pre-flight)\nVERIFY_COPILOT=$(read_setting_status \"EinsteinCopilotSettings\" \"EinsteinCopilot\" \"enableEinsteinGptCopilot\")\nreport_status \"Agentforce Agent (enableEinsteinGptCopilot)\" \"$VERIFY_COPILOT\" \"warn\"\n\n# Verify Agentforce Studio / Agent Platform (gates Deal Agent; enable-deal-agent.sh\n# pre-flight hard-blocks on this — a disabled state here explains a downstream\n# Deal Agent activation failure).\nVERIFY_AGENT_PLATFORM=$(read_setting_status \"AgentPlatformSettings\" \"AgentPlatform\" \"enableAgentPlatform\")\nreport_status \"Agentforce Studio / Agent Platform (enableAgentPlatform)\" \"$VERIFY_AGENT_PLATFORM\" \"warn\"\n\n# Verify Enhanced Notes (checked by enable-deal-agent.sh pre-flight)\nVERIFY_NOTES=$(read_setting_status \"EnhancedNotesSettings\" \"EnhancedNotes\" \"enableEnhancedNotes\")\nreport_status \"Enhanced Notes (enableEnhancedNotes)\" \"$VERIFY_NOTES\" \"warn\"\n\n# Verify Opportunity Team (CRITICAL — PM flow deploy fails with OppTeamMemRec error\n# without it). OpportunitySettings is not queryable via SOQL — use SOAP readMetadata.\nVERIFY_OPP_TEAM=$(get_opp_team_status)\nif [[ \"$VERIFY_OPP_TEAM\" != \"true\" ]]; then\n report_status \"Opportunity Team (enableOpportunityTeam)\" \"$VERIFY_OPP_TEAM\" \"critical\"\n echo \" The Pipeline Management flow requires OpportunityTeamMember access;\"\n echo \" without it, flow deployment fails with an OppTeamMemRec error.\"\n echo \" Note: cannot coexist with Opportunity Splits — disable Splits first if enabled.\"\nelse\n report_status \"Opportunity Team (enableOpportunityTeam)\" \"$VERIFY_OPP_TEAM\" \"critical\"\nfi\n\n# Verify Email Administration (best-effort, not blocking)\nVERIFY_EMAIL=$(sf data query --query \"SELECT enableEnhancedEmailEnabled FROM EmailAdministrationSettings\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].enableEnhancedEmailEnabled // \"unknown\"' 2>/dev/null || echo \"unknown\")\nif [[ \"$VERIFY_EMAIL\" == \"true\" ]]; then\n echo \" ✅ Enhanced Email: enabled\"\nelse\n echo \" ⚠️ Enhanced Email: not enabled (optional — email body indexing)\"\nfi\n\n# Verify Opportunity Settings (best-effort, not blocking)\nVERIFY_OPP=$(sf data query --query \"SELECT enableFindSimilarOpportunities FROM OpportunitySettings\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].enableFindSimilarOpportunities // \"unknown\"' 2>/dev/null || echo \"unknown\")\nif [[ \"$VERIFY_OPP\" == \"true\" ]]; then\n echo \" ✅ Opportunity Settings: enabled\"\nelse\n echo \" ⚠️ Opportunity Settings: not enabled (optional)\"\nfi\n\necho \"\"\n\nif [[ \"$VERIFICATION_FAILED\" == \"true\" ]]; then\n echo \"❌ Critical prerequisites failed. Cannot proceed to Deal Agent setup.\"\n exit 1\nfi\n\necho \"✅ All prerequisites enabled successfully\"\necho \"\"\necho \"Next step: Run ./enable-deal-agent.sh $ORG_ALIAS\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":23435,"content_sha256":"425a2f998daaa5ee7acf15edcfb2159991109b7be827dc23d48dcfc5529026e4"},{"filename":"scripts/flow-debug-and-verify.sh","content":"#!/usr/bin/env bash\n#\n# flow-debug-and-verify.sh — Verify Pipeline Management flow is working correctly\n#\n# Usage:\n# bash flow-debug-and-verify.sh \u003corg-alias> [opportunity-id] # flow-debug-start: pick opp + print Debug URL\n# bash flow-debug-and-verify.sh \u003corg-alias> \u003copportunity-id> verify # verify: after the debug run\n#\n# ADMIN VERIFICATION TOOL: Verifies that Pipeline Management is configured\n# correctly and generating field suggestions.\n#\n# The suggestion flow (Process_Field_Update_Suggestions) is a SCHEDULED\n# record-triggered flow. Its $Record global is bound ONLY when the flow runs —\n# by the platform scheduler at cron time, or by Flow Builder's Debug runner\n# (which asks you to pick the record). Starting the interview manually from Apex\n# leaves $Record null and the flow silently no-ops, so this tool drives it as a\n# guided MANUAL step split into two phases:\n#\n# FLOW DEBUG START (default) — picks an eligible opportunity and prints the\n# flow's Debug URL. You then run Debug against that\n# opportunity in the browser (leave rollback mode\n# UNCHECKED).\n# VERIFY (\"verify\") — run after the debug run finishes; confirms the flow\n# reached its action (agent user on the Opportunity Team)\n# and polls for the generated AiGenActionItem suggestions.\n#\n# The wait between phases happens in the DRIVER (the human or the agent invoking\n# this), not inside the script — so an agent can relay the URL, wait in the\n# conversation for you to finish the debug run, then invoke the verify phase.\n# When run in an interactive terminal, FLOW DEBUG START additionally offers to\n# pause and chain straight into VERIFY for convenience.\n#\nset -euo pipefail\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\n# ============================================================\n# Arguments\n# ============================================================\nORG_ALIAS=\"${1:-}\"\nOPP_ID=\"${2:-}\"\nMODE=\"${3:-flow-debug-start}\" # flow-debug-start (default) | verify\nBASELINE_ARG=\"${4:-}\" # verify: suggestion count captured BEFORE the debug run\nFLOW_API_NAME=\"Process_Field_Update_Suggestions\"\n\nif [[ -z \"$ORG_ALIAS\" ]]; then\n cat >&2 \u003c\u003cEOF\nUsage:\n $0 \u003corg-alias> [opportunity-id] # FLOW DEBUG START: pick opp + print the Debug URL\n $0 \u003corg-alias> \u003copportunity-id> verify # VERIFY: run after the debug finishes\n\nADMIN VERIFICATION: Verifies the Pipeline Management suggestion flow works by\nguiding you through a manual Flow Builder Debug run (the only on-demand path\nthat binds a real \\$Record on a scheduled record-triggered flow), then checks\nthe resulting Opportunity Team membership and AiGenActionItem suggestions.\n\nArguments:\n org-alias Salesforce org alias or username\n opportunity-id (Optional in flow-debug-start) Specific opportunity to test.\n If omitted, a test opportunity owned by the current user is\n created (and a grounding Note seeded); otherwise falls back to\n a user-owned open opportunity.\n REQUIRED in verify (use the ID that flow-debug-start reported).\n\nTwo-phase flow:\n 1. FLOW DEBUG START prints the flow's Debug URL and the opportunity to select.\n 2. You open the URL, click Debug, pick that opportunity as the trigger\n record, leave 'rollback mode' UNCHECKED, and Run.\n 3. VERIFY confirms the agent user joined the Opportunity Team and polls for\n the generated suggestion(s).\n\nExample:\n $0 connected-seller-home\n # ...run the debug in the browser against the reported opportunity...\n $0 connected-seller-home 006SB00000J73XjYAJ verify\nEOF\n exit 1\nfi\n\n# ============================================================\n# Helper Functions\n# ============================================================\nlog_info() { echo \" [....] $*\"; }\nlog_pass() { echo \" [PASS] $*\"; }\nlog_fail() { echo \" [FAIL] $*\" >&2; }\nlog_warn() { echo \" [WARN] $*\"; }\nlog_try() { echo \" [TRY ] $*\"; }\n\ndie() {\n echo \"Error: $*\" >&2\n exit 1\n}\n\n# Shared test-opp + grounding-note resolution (kept identical to\n# verify-prompt-generation.sh via this one sourced shared library). Defines\n# resolve_test_opportunity, which sets OPP_ID and seeds/refreshes the note.\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"$SCRIPT_DIR/shared/test-opp.sh\"\n\n# ============================================================\n# VERIFY phase — check the flow reached its action + polled suggestions\n# ============================================================\nrun_verify() {\n local opp_id=\"$1\"\n local opp_name=\"$2\"\n local note_count=\"$3\"\n local baseline_count=\"$4\"\n\n echo \"\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \" VERIFY — checking the debug run's results\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \"\"\n\n # The flow adds the agent user to the Opportunity Team BEFORE calling the\n # suggestion action, within the same synchronous debug run. So once the debug\n # run has finished, that membership must exist — its absence means the flow\n # no-op'd (ineligible opp, missing agent user, inactive Bot, or rollback mode\n # left on) rather than a timing issue.\n log_info \"Confirming the agent user joined the Opportunity Team...\"\n\n local agent_user_id\n agent_user_id=$(sf data query -q \"SELECT Id FROM User WHERE Username LIKE '%salesmanagementagentuser%' LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty')\n\n if [[ -z \"$agent_user_id\" ]]; then\n die \"Agent user (salesmanagementagentuser) not found — Pipeline Management may not be fully provisioned.\"\n fi\n\n local agent_otm\n agent_otm=$(sf data query -q \"SELECT TeamMemberRole, OpportunityAccessLevel FROM OpportunityTeamMember WHERE OpportunityId='$opp_id' AND UserId='$agent_user_id' LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0] // empty')\n\n echo \"\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \" Results\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \"\"\n\n if [[ -z \"$agent_otm\" ]]; then\n log_fail \"Agent user is NOT on the Opportunity Team for $opp_id.\"\n echo \"\"\n echo \" The debug run did not reach the suggestion action. Rule out real gaps:\"\n echo \" - Did you select this opportunity ($opp_id) as the trigger record?\"\n echo \" - Was 'rollback mode' left UNCHECKED? (rollback discards the DML)\"\n echo \" - Opportunity eligible? (IsClosed=false, CloseDate within 90 days)\"\n echo \" - Agent user holds SalesManagementAgentUserPsg?\"\n echo \" - Active BotDefinition/BotVersion for the Sales agent?\"\n echo \" - Bot AgentTemplate matches the flow's SalesManagementAgentTemplate\"\n echo \" (SalesMgmt__SalesAgent vs __NGASalesAgent) — a mismatch makes the\"\n echo \" flow exit BEFORE the Opportunity Team step.\"\n echo \"\"\n die \"Flow did not queue opportunity $opp_id for suggestion generation.\"\n fi\n\n local otm_role otm_access\n otm_role=$(echo \"$agent_otm\" | jq -r '.TeamMemberRole // \"?\"')\n otm_access=$(echo \"$agent_otm\" | jq -r '.OpportunityAccessLevel // \"?\"')\n log_pass \"Agent user is on the Opportunity Team (role: $otm_role, access: $otm_access)\"\n log_pass \"Flow reached its suggestion action for opportunity $opp_id.\"\n echo \"\"\n\n # The action writes AiGenActionItem asynchronously. On-org timing: 0 rows at\n # 90s, suggestions landing ~165s — so poll to ~4 min before concluding failure\n # (a short poll is the #1 false negative). Compare this opportunity's\n # FIELD_UPDATE count against the baseline captured BEFORE the debug run.\n log_info \"Polling for generated suggestion(s) (up to ~4 min)...\"\n\n local suggestion_found=0 current_count\n for _ in $(seq 1 16); do\n sleep 15\n # `|| echo` keeps a transient sf/network blip (ECONNRESET is a known flake on\n # this org) from aborting the whole verify under `set -o pipefail`.\n current_count=$(sf data query -q \"SELECT COUNT() FROM AiGenActionItem WHERE ParentId='$opp_id' AND Type='FIELD_UPDATE'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.totalSize // 0' || echo \"$baseline_count\")\n if [[ \"$current_count\" -gt \"$baseline_count\" ]]; then\n local delta=$((current_count - baseline_count))\n log_pass \"Generated $delta new suggestion(s) for $opp_id (AiGenActionItem).\"\n suggestion_found=1\n break\n fi\n done\n\n if [[ \"$suggestion_found\" -eq 0 ]]; then\n if [[ \"$note_count\" -gt 0 ]]; then\n log_warn \"No new suggestions after ~4 min, though this opp has $note_count note(s).\"\n log_warn \"Generation can lag; check Pipeline Inspection on $opp_name shortly. If it\"\n log_warn \"stays empty, verify the field prompt templates are active and grounded.\"\n else\n log_info \"No suggestions after ~4 min. Expected here — this opp has no notes to\"\n log_info \"ground on. Link a ContentNote and re-run to see grounded suggestions.\"\n fi\n fi\n\n echo \"\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \" Verification Complete\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \"\"\n echo \"Test Opportunity: $opp_id ($opp_name)\"\n}\n\n# ============================================================\n# Setup\n# ============================================================\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" Pipeline Management Flow Debug Verification\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\necho \"Org: $ORG_ALIAS\"\necho \"Flow: $FLOW_API_NAME\"\necho \"Mode: $MODE\"\necho \"\"\n\nif [[ \"$MODE\" != \"flow-debug-start\" && \"$MODE\" != \"verify\" ]]; then\n die \"Unknown mode '$MODE'. Use 'flow-debug-start' (default) or 'verify'.\"\nfi\n\nif [[ \"$MODE\" == \"verify\" && -z \"$OPP_ID\" ]]; then\n die \"verify mode requires an opportunity id: $0 \u003corg-alias> \u003copportunity-id> verify [baseline]\"\nfi\n\n# ============================================================\n# Resolve / validate the target opportunity\n# ============================================================\nif [[ -z \"$OPP_ID\" ]]; then\n # Pipeline Inspection is owner-scoped, so a suggestion on an opp owned by someone\n # else is invisible to the admin verifying. resolve_test_opportunity (shared lib)\n # reuses/creates the stable [PM-TEST] opp owned by the current user, falls back to\n # a user-owned open opp, and seeds/refreshes the grounding note on whichever\n # resolved — the SAME opp + note the prompt-verification path uses.\n log_info \"No opportunity given — resolving a user-owned test opportunity...\"\n\n resolve_test_opportunity\n\n if [[ -z \"$OPP_ID\" ]]; then\n log_warn \"No eligible opportunity found for the current user.\"\n echo \" To verify: create an open Opportunity owned by you with CloseDate within 90 days,\"\n echo \" then re-run: $0 $ORG_ALIAS \u003copportunity-id>\"\n echo \"\"\n log_info \"Skipping output verification — no opportunity available\"\n exit 0\n fi\nelse\n log_info \"Using opportunity: $OPP_ID\"\n\n # In flow-debug-start mode, warn if it doesn't meet flow criteria (verify mode\n # trusts the id flow-debug-start already vetted).\n if [[ \"$MODE\" != \"verify\" ]]; then\n IS_ELIGIBLE=$(sf data query -q \"SELECT Id FROM Opportunity WHERE Id='$OPP_ID' AND IsClosed=false AND CloseDate>=TODAY AND CloseDate\u003c=NEXT_N_DAYS:90\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty')\n\n if [[ -z \"$IS_ELIGIBLE\" ]]; then\n log_warn \"Opportunity $OPP_ID may not meet flow criteria (must be open with CloseDate within 90 days)\"\n fi\n fi\nfi\n\n# Opportunity details (name for messaging; note count feeds the suggestion check)\nOPP_DETAILS=$(sf data query -q \"SELECT Name, StageName, CloseDate FROM Opportunity WHERE Id='$OPP_ID'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0] // empty')\nif [[ -z \"$OPP_DETAILS\" || \"$OPP_DETAILS\" == \"null\" ]]; then\n die \"Opportunity '$OPP_ID' not found in org '$ORG_ALIAS'. Check the id (a missing opp would otherwise look like a flow no-op).\"\nfi\nOPP_NAME=$(echo \"$OPP_DETAILS\" | jq -r '.Name')\nOPP_STAGE=$(echo \"$OPP_DETAILS\" | jq -r '.StageName')\nOPP_CLOSE=$(echo \"$OPP_DETAILS\" | jq -r '.CloseDate')\n\nNOTE_COUNT=$(sf data query -q \"SELECT COUNT() FROM ContentDocumentLink WHERE LinkedEntityId='$OPP_ID'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.totalSize // 0')\n\n# The baseline must be the suggestion count BEFORE the debug run. flow-debug-start\n# captures it here (pre-run) and prints it into the verify command; verify reuses\n# that value. Re-querying in verify would fold the run's own output into the\n# baseline, so the delta poll could never detect it — a guaranteed false negative.\n# Scoped to THIS opp + FIELD_UPDATE so a concurrent run elsewhere can't false-positive.\nif [[ \"$MODE\" == \"verify\" ]]; then\n if [[ -n \"$BASELINE_ARG\" ]]; then\n BASELINE_COUNT=\"$BASELINE_ARG\"\n else\n # Standalone verify with no baseline: fall back to absolute detection (any\n # FIELD_UPDATE item counts). Weaker than the delta, but never a false negative.\n log_warn \"No baseline passed; using absolute count (any FIELD_UPDATE suggestion = pass).\"\n BASELINE_COUNT=0\n fi\n run_verify \"$OPP_ID\" \"$OPP_NAME\" \"$NOTE_COUNT\" \"$BASELINE_COUNT\"\n exit 0\nfi\n\nBASELINE_COUNT=$(sf data query -q \"SELECT COUNT() FROM AiGenActionItem WHERE ParentId='$OPP_ID' AND Type='FIELD_UPDATE'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.totalSize // 0')\n\n# ============================================================\n# FLOW DEBUG START mode: resolve the flow's Debug URL and hand off\n# ============================================================\necho \"\"\necho \"Test Opportunity:\"\necho \" ID: $OPP_ID\"\necho \" Name: $OPP_NAME\"\necho \" Stage: $OPP_STAGE\"\necho \" Close Date: $OPP_CLOSE\"\necho \" Notes: $NOTE_COUNT\"\necho \"\"\n\nif [[ \"$NOTE_COUNT\" -eq 0 ]]; then\n log_warn \"No ContentNotes linked to this opportunity - suggestions may be empty\"\nfi\n\n# Instance URL for the Debug link.\nINSTANCE_URL=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.instanceUrl // empty')\n[[ -z \"$INSTANCE_URL\" ]] && die \"Could not resolve instance URL for org '$ORG_ALIAS' — is it authenticated?\"\nINSTANCE_URL=\"${INSTANCE_URL%/}\"\n\nlog_info \"Locating flow '$FLOW_API_NAME'...\"\n\n# FlowDefinitionView has no 'ActiveVersion' relationship — use the scalar\n# ActiveVersionId, then read VersionNumber from the Flow tooling object.\nFLOW_DEF=$(sf data query -q \"SELECT Id, ActiveVersionId, ProcessType, TriggerType FROM FlowDefinitionView WHERE ApiName='$FLOW_API_NAME' AND IsActive=true\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0] // empty')\n\nif [[ -z \"$FLOW_DEF\" || \"$FLOW_DEF\" == \"null\" ]]; then\n die \"Flow '$FLOW_API_NAME' not found or not active\"\nfi\n\nFLOW_VERSION_ID=$(echo \"$FLOW_DEF\" | jq -r '.ActiveVersionId')\nFLOW_PROCESS_TYPE=$(echo \"$FLOW_DEF\" | jq -r '.ProcessType')\nFLOW_TRIGGER_TYPE=$(echo \"$FLOW_DEF\" | jq -r '.TriggerType // \"None\"')\n\nFLOW_VERSION_NUM=$(sf data query -q \"SELECT VersionNumber FROM Flow WHERE Id='$FLOW_VERSION_ID'\" \\\n --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null | jq -r '.result.records[0].VersionNumber // \"?\"')\n\nlog_pass \"Flow found (version $FLOW_VERSION_NUM, type: $FLOW_PROCESS_TYPE, trigger: $FLOW_TRIGGER_TYPE)\"\n\n# Flow Builder opens on the active version by its FlowVersion (301) Id. From\n# there the Debug button lets you pick the record to bind as $Record.\nDEBUG_URL=\"${INSTANCE_URL}/builder_platform_interaction/flowBuilder.app?flowId=${FLOW_VERSION_ID}\"\n\necho \"\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" MANUAL STEP — Debug the flow in your browser\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\necho \" This is a scheduled record-triggered flow, so \\$Record is only bound\"\necho \" when you run it. Flow Builder's Debug runner lets you pick the record.\"\necho \"\"\necho \" 1. Open the flow (active version $FLOW_VERSION_NUM):\"\necho \"\"\necho \" $DEBUG_URL\"\necho \"\"\necho \" 2. Click the Debug button (top-right in Flow Builder).\"\necho \" 3. In 'Debug the flow' → for the record that triggers the flow, select:\"\necho \"\"\necho \" $OPP_NAME\"\necho \" ($OPP_ID)\"\necho \"\"\necho \" 4. Leave 'Run the Flow in rollback mode' UNCHECKED so the DML persists,\"\necho \" then click Run.\"\necho \" 5. Wait for the debug panel to show the run finished.\"\necho \"\"\necho \" When the debug run is finished, verify the results with (the trailing\"\necho \" $BASELINE_COUNT is the pre-run suggestion baseline — keep it so the delta is exact):\"\necho \"\"\necho \" bash $0 $ORG_ALIAS $OPP_ID verify $BASELINE_COUNT\"\necho \"\"\n\n# Best-effort: open the URL in the default browser for convenience. Requires you\n# to already be logged into this org in that browser, else it lands on login.\nif command -v open >/dev/null 2>&1; then\n open \"$DEBUG_URL\" >/dev/null 2>&1 || true\nfi\n\n# Convenience for a human at an interactive terminal: offer to pause here and\n# chain into VERIFY. Gate on stdin being a real TTY (`-t 0`), NOT merely on\n# /dev/tty existing — an agent driving this via a tool call has no interactive\n# stdin, so it skips this and calls the verify command above once the user\n# confirms in the conversation (no hang).\nif [[ -t 0 ]]; then\n printf ' Press ENTER once the debug run has finished to verify now (or Ctrl-C to verify later): '\n if read -r _; then\n run_verify \"$OPP_ID\" \"$OPP_NAME\" \"$NOTE_COUNT\" \"$BASELINE_COUNT\"\n fi\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":18794,"content_sha256":"f29f8770b44eacf0fbb99658ff775cfd17dac3066a462d7876b079cec721ad7b"},{"filename":"scripts/README.md","content":"# Scripts for Pipeline Management Configuration\n\nThis directory contains automation scripts for enabling Agentforce Pipeline Management. All scripts require an `sfdx-project.json` in your working directory.\n\n## Prerequisites\n\n- Salesforce CLI (`sf`) installed\n- Authenticated org with alias\n- `sfdx-project.json` in working directory (see `../assets/sfdx-project.json`)\n\n## Usage Table\n\n| Script | Purpose | Usage | Dependencies |\n|--------|---------|-------|--------------|\n| `setup-all.sh` | **Master orchestration** — end-to-end setup, enables all prerequisites + PM inline | `./setup-all.sh \u003corg-alias> --fields \"NextStep,Risk__c\"` | None (auth only) |\n| `retrieve-settings.sh` | Retrieve all settings for inspection | `./retrieve-settings.sh \u003corg-alias>` | None |\n| `enable-prerequisites.sh` | Enable prerequisite settings only (no fields/flow/agent) | `./enable-prerequisites.sh \u003corg-alias>` | None |\n| `enable-deal-agent.sh` | Enable SalesDealAgent settings (standalone helper) | `./enable-deal-agent.sh \u003corg-alias>` | Prerequisites must already be enabled |\n| `create-flow.sh` | Detect/verify/activate suggestion flow | `./create-flow.sh \u003corg-alias>` | Deal agent enabled + manual UI clone |\n| `create-agent.sh` | Verify/create/activate agent | `./create-agent.sh \u003corg-alias>` | Deal agent enabled |\n| `deploy-settings.sh` | Deploy settings from local files | `./deploy-settings.sh \u003corg-alias>` | Local metadata files |\n| `verify-all.sh` | Comprehensive status check | `./verify-all.sh \u003corg-alias>` | None (read-only) |\n\n## Execution Order\n\nFor end-to-end setup, drive **`setup-all.sh`** — it enables all prerequisites (Einstein GenAI, Agentforce, Enhanced Notes, Opportunity Team), enables Pipeline Management, deploys prompt templates, builds/activates the flow, provisions the agent + PSGs, and verifies, in dependency order. The individual scripts below are for isolated inspection or repair of a single component:\n\n1. **retrieve-settings.sh** — Inspect current state\n2. **enable-prerequisites.sh** — Enable prerequisite settings only (Einstein, Notes, Opportunity Team)\n3. **enable-deal-agent.sh** — Enable Pipeline Management in isolation (prerequisites must already be on)\n4. **create-flow.sh** — Detect/verify/activate flow (requires manual Setup UI clone first)\n5. **create-agent.sh** — Verify agent user and PSGs (auto-provisioned on enablement)\n6. **verify-all.sh** — Comprehensive status check (read-only)\n\n## Important Notes\n\n- All scripts are idempotent (safe to re-run)\n- Scripts use SOAP API v62.0-64.0 for settings that fail silently via CLI\n- Individual setting deployment isolates failures\n- Each script validates before making changes\n- All `sf` commands use `--json` with error suppression for clean parsing\n\n## sfdx-project.json Requirement\n\nAll scripts expect an `sfdx-project.json` in your working directory. Copy from `../assets/sfdx-project.json`:\n\n```bash\ncp ../assets/sfdx-project.json ./sfdx-project.json\n```\n\n## Error Handling\n\n- Scripts exit with non-zero code on critical failures\n- Warnings are logged but don't stop execution\n- SOAP API errors are captured and reported\n- Missing metadata is handled gracefully\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3169,"content_sha256":"13465229c73ec41b9ec80537b274bcf5c0befbd96524319417a261405a01ce24"},{"filename":"scripts/retrieve-settings.sh","content":"#!/bin/bash\n\n# Retrieve and display Pipeline Management settings status\n# Uses SOAP readMetadata (reliable) instead of CLI retrieve (empty results for many settings)\n# Usage: ./retrieve-settings.sh \u003corg-alias>\n\nset -euo pipefail\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\nORG_ALIAS=\"${1:-}\"\n\nif [[ -z \"$ORG_ALIAS\" ]]; then\n echo \"Error: Missing org alias\"\n echo \"Usage: $0 \u003corg-alias>\"\n exit 1\nfi\n\necho \"Retrieving Pipeline Management settings from org: $ORG_ALIAS\"\necho \"\"\n\n# Get credentials\nINSTANCE_URL=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.instanceUrl // empty' 2>/dev/null || echo \"\")\nACCESS_TOKEN=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.accessToken // empty' 2>/dev/null || echo \"\")\n\nif [[ -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" || \"$ACCESS_TOKEN\" == *\"REDACTED\"* ]]; then\n ACCESS_TOKEN=$(echo \"y\" | sf org auth show-access-token --target-org \"$ORG_ALIAS\" --no-prompt --json 2>/dev/null | jq -r '.result.accessToken // empty' 2>/dev/null || echo \"\")\nfi\n\nif [[ -z \"$INSTANCE_URL\" || -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" ]]; then\n echo \"Error: Cannot connect to org. Run: sf org login web --alias $ORG_ALIAS\"\n exit 1\nfi\n\n# SOAP readMetadata helper\nsoap_read() {\n local TYPE=\"$1\" NAME=\"$2\"\n curl -s \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>${TYPE}\u003c/met:type>\u003cmet:fullNames>${NAME}\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null\n}\n\ncheck_setting() {\n local LABEL=\"$1\" TYPE=\"$2\" NAME=\"$3\" FIELD=\"$4\"\n printf \" %-35s\" \"$LABEL:\"\n local RESPONSE\n RESPONSE=$(soap_read \"$TYPE\" \"$NAME\")\n local VALUE\n VALUE=$(echo \"$RESPONSE\" | grep -o \"\u003c${FIELD}>[^\u003c]*\" | sed \"s/\u003c${FIELD}>//\" | head -1 || echo \"\")\n\n if [[ \"$VALUE\" == \"true\" ]]; then\n echo \"ENABLED\"\n elif [[ \"$VALUE\" == \"false\" ]]; then\n echo \"DISABLED\"\n elif [[ -z \"$VALUE\" ]]; then\n echo \"NOT FOUND (type may not exist in this org)\"\n else\n echo \"$VALUE\"\n fi\n}\n\necho \"=== Settings Status (via SOAP readMetadata) ===\"\necho \"\"\n\ncheck_setting \"Einstein Generative AI\" \"EinsteinGptSettings\" \"EinsteinGpt\" \"enableEinsteinGptPlatform\"\ncheck_setting \"Agentforce Agent (Copilot)\" \"EinsteinCopilotSettings\" \"EinsteinCopilot\" \"enableEinsteinGptCopilot\"\ncheck_setting \"Pipeline Management\" \"SalesDealAgentSettings\" \"SalesDealAgent\" \"enableDealAgent\"\ncheck_setting \"Auto-Approve All Tasks\" \"SalesDealAgentSettings\" \"SalesDealAgent\" \"enableDealAgentAutoApproveAllTasks\"\ncheck_setting \"Pipeline Inspection\" \"OpportunitySettings\" \"Opportunity\" \"enablePipelineInspection\"\ncheck_setting \"Enhanced Notes\" \"EnhancedNotesSettings\" \"EnhancedNotes\" \"enableEnhancedNotes\"\ncheck_setting \"Enhanced Email\" \"EmailAdministrationSettings\" \"EmailAdministration\" \"enableEnhancedEmailEnabled\"\ncheck_setting \"Opportunity Team Selling\" \"OpportunitySettings\" \"Opportunity\" \"enableOpportunityTeam\"\n\necho \"\"\necho \"=== Auto-Provisioned Components ===\"\necho \"\"\n\n# PSGs\nPSG_RESULT=$(sf data query -q \"SELECT DeveloperName FROM PermissionSetGroup WHERE DeveloperName IN ('SalesManagementUserPsg','SalesManagementAgentUserPsg')\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null | jq -r '.result.records[].DeveloperName' 2>/dev/null || echo \"\")\nprintf \" %-35s\" \"Permission Set Groups:\"\nif [[ -n \"$PSG_RESULT\" ]]; then\n echo \"$PSG_RESULT\" | tr '\\n' ',' | sed 's/,$/\\n/'\nelse\n echo \"NOT FOUND\"\nfi\n\n# Agent user\nAGENT_USER=$(sf data query -q \"SELECT Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo \"\")\nprintf \" %-35s\" \"Agent User:\"\nif [[ -n \"$AGENT_USER\" ]]; then\n echo \"$AGENT_USER\"\nelse\n echo \"NOT FOUND\"\nfi\n\n# Flow — check by SourceTemplateId, then by naming convention, then by exact name\nFLOW_QUERY=$(sf data query -q \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\nFLOW_NAME=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0] | \"\\(.ApiName) (Active: \\(.IsActive))\"' 2>/dev/null | head -1 || echo \"\")\n\nif [[ -z \"$FLOW_NAME\" || \"$FLOW_NAME\" == \"null (Active: null)\" ]]; then\n FLOW_QUERY=$(sf data query -q \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\n FLOW_NAME=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0] | \"\\(.ApiName) (Active: \\(.IsActive))\"' 2>/dev/null | head -1 || echo \"\")\nfi\n\nprintf \" %-35s\" \"Schedule-Triggered Flow:\"\nif [[ -n \"$FLOW_NAME\" && \"$FLOW_NAME\" != \"null (Active: null)\" ]]; then\n echo \"$FLOW_NAME\"\nelse\n echo \"NOT FOUND\"\nfi\n\n# Stage descriptions\nDESC_RESULT=$(sf data query -q \"SELECT Id FROM OpptStageDescription\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null || echo '{\"status\":1}')\nprintf \" %-35s\" \"Stage Descriptions:\"\nif echo \"$DESC_RESULT\" | grep -qi \"INVALID_TYPE\\|NOT_FOUND\\|sObject type.*not supported\"; then\n echo \"N/A (PM not fully enabled)\"\nelse\n DESC_COUNT=$(echo \"$DESC_RESULT\" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9')\n echo \"${DESC_COUNT:-0}\"\nfi\n\necho \"\"\necho \"Done.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5834,"content_sha256":"ccb86c9090fd45072ce70c8a45ee0e3a75bbf5e8b084a612629336b7b58f2cfa"},{"filename":"scripts/setup-all.sh","content":"#!/bin/bash\n# Master orchestration script for Pipeline Management end-to-end setup\n# Handles both greenfield and partially-configured orgs.\n#\n# Architecture: Each phase uses a \"harness loop\" pattern:\n# 1. Check if already done (skip if yes)\n# 2. Try primary approach (SOAP Metadata API)\n# 3. If primary fails, try fallback (CLI retrieve/modify/deploy)\n# 4. If fallback fails, try tertiary (Tooling API / REST)\n# 5. VERIFY the result via readMetadata before proceeding\n# 6. Only report BLOCKER after ALL approaches exhausted\n#\n# Working Directory Note:\n# Agent bundle publishing uses isolated temp directories with automatic cleanup.\n# Settings/flow deployment operations may create force-app/ in the current directory\n# when SOAP/Tooling API approaches fail and CLI deploy is attempted as fallback.\n#\n# Usage: ./setup-all.sh \u003corg-alias> [flags]\n#\n# Flags (all optional; SAFE by default):\n# --autonomous Enable autonomous field updates (agent auto-applies\n# without human review). OFF by default — suggestions only.\n# --create-stage-descriptions Create sales-methodology stage descriptions (needed only\n# for StageName suggestions). OFF by default.\n# --non-interactive | --yes Run without prompts. Combined with the flags above this\n# gives fully unattended setup; without them the run stays\n# on the safe (suggestion-only, no stage-desc) defaults.\n# --users \"[email protected],[email protected]\" Explicit comma-separated usernames to also grant the\n# SalesManagementUserPsg (in addition to the running user).\n# This is the ONLY non-interactive way to add users — the\n# script NEVER queries all/standard users and bulk-assigns.\n\nset -euo pipefail\n\n# Require bash 4.0+ for associative arrays (macOS ships bash 3.2 by default)\nif [[ ${BASH_VERSINFO[0]} -lt 4 ]]; then\n echo \"ERROR: bash 4.0+ required. Your version: $BASH_VERSION\"\n echo \"On macOS, install via: brew install bash\"\n echo \"Then run: /opt/homebrew/bin/bash $0 $*\"\n exit 1\nfi\n\n# Set consistent locale to prevent jq parsing issues with special characters\nexport LC_ALL=en_US.UTF-8\nexport LANG=en_US.UTF-8\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\n# Exported here so they propagate to sourced libs and child bash subprocesses.\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\nORG_ALIAS=\"${1:-}\"\n# Validate org alias (prevent shell injection via metacharacters)\nif [[ -n \"$ORG_ALIAS\" && ! \"$ORG_ALIAS\" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then\n echo \"ERROR: Invalid org alias. Only alphanumeric, dots, hyphens, and underscores allowed (must start with alphanumeric).\"\n exit 1\nfi\n\n# --- Consent flags (SAFE by default) ---\n# Autonomous updates and stage-description creation are BOTH opt-in. In non-interactive\n# mode we never enable them implicitly; the operator must pass the explicit flag.\nENABLE_AUTONOMOUS=false # --autonomous\nCREATE_STAGE_DESCRIPTIONS=false # --create-stage-descriptions\nNON_INTERACTIVE=false # --non-interactive | --yes (also implied when stdin is not a TTY)\nUSERS_LIST=\"\" # --users \"[email protected],[email protected]\" — explicit PSG grantees (never bulk)\n\n# --- Field selection (3-phase setup) ---\n# Which Opportunity fields the suggestion flow should manage. Chosen UP FRONT\n# (before the flow is built) so the flow is built with ONLY these fields — no\n# deploy-then-strip. At least one field is REQUIRED; setup ABORTS if none are\n# selected (there is no silent default). OOTB fields (NextStep, StageName) count\n# toward the 5-field cap. Per-field goal/instruction customization is collected\n# into FIELD_PROMPTS keyed \"\u003cField>_goal\" / \"\u003cField>_instruction\".\nSELECTED_FIELDS=\"\" # --fields \"NextStep,StageName,Risk__c\"\nSKIP_PROMPT_VERIFICATION=false # --skip-prompt-verification (bypass the Phase 2e gate)\ndeclare -A FIELD_PROMPTS # per-field goal/instruction, from --field-goal/--field-instruction\nreadonly MAX_SUGGESTION_FIELDS=5\n\n# --- Phase split (agent-driven interactive tune-loop) ---\n# The full run is one long non-interactive pass, which (a) can't pause for the\n# agent to tune each field's prompt with the user and (b) can exceed the harness\n# tool timeout. --through-phase / --from-phase cut the run at the prompt boundary\n# so the AGENT owns the interactive tune-loop BETWEEN two calls:\n# Call 1: --through-phase prompts → enable, provision, deploy+activate templates,\n# then STOP before the flow is built (context phases 0/1/1.7 always run).\n# (agent tunes each field via add-field-suggestion.sh --verify-with-note --note …)\n# Call 2: --from-phase flow → build+activate flow, agent, PSG, final verify.\n# Both calls are idempotent, so this is a resumable checkpoint, not new state.\n# Recognized cut points: \"prompts\" (end of Phase 4d) and \"flow\" (start of Phase 5).\nRUN_FROM_PHASE=\"\" # --from-phase flow (skip everything before Phase 5)\nRUN_THROUGH_PHASE=\"\" # --through-phase prompts (stop after templates, before flow)\n\n# --- License preflight (agent's first action, before any clarifying question) ---\n# --check-license runs ONLY auth + the Phase 0.5 capability gate, then exits. It\n# takes no --fields (so it can run before the field questions the gate must\n# precede), asks nothing, and mutates nothing. The SKILL drives this FIRST so an\n# unlicensed org is blocked before the user is asked anything.\nCHECK_LICENSE_ONLY=false # --check-license\n\n# Parse flags after the positional org alias. `shift` past $1 first if present.\nif [[ $# -ge 1 ]]; then shift || true; fi\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --autonomous) ENABLE_AUTONOMOUS=true; shift ;;\n --create-stage-descriptions) CREATE_STAGE_DESCRIPTIONS=true; shift ;;\n --non-interactive|--yes) NON_INTERACTIVE=true; shift ;;\n --check-license) CHECK_LICENSE_ONLY=true; shift ;;\n --skip-prompt-verification) SKIP_PROMPT_VERIFICATION=true; shift ;;\n --users) { [[ $# -ge 2 ]] && [[ \"$2\" != --* ]]; } || { echo \"ERROR: --users requires a value (comma-separated usernames), e.g. --users \\\"[email protected],[email protected]\\\"\"; exit 1; }\n IFS=',' read -ra _TMP_USERS \u003c\u003c\u003c \"$2\"\n for _u in \"${_TMP_USERS[@]}\"; do\n _u=$(echo \"$_u\" | xargs)\n [[ \"$_u\" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$ ]] || { echo \"ERROR: Invalid email format in --users: '$_u'. Each value must be a valid email address.\"; exit 1; }\n done\n USERS_LIST=\"$2\"; shift 2 ;;\n --fields) { [[ $# -ge 2 ]] && [[ \"$2\" != --* ]]; } || { echo \"ERROR: --fields requires a value (comma-separated field API names), e.g. --fields \\\"NextStep,Risk__c\\\"\"; exit 1; }; SELECTED_FIELDS=\"$2\"; shift 2 ;;\n --field-goal) { [[ $# -ge 2 ]] && [[ \"$2\" == *:* ]]; } || { echo \"ERROR: --field-goal requires \\\"FieldApiName:goal text\\\"\"; exit 1; }; FIELD_PROMPTS[\"${2%%:*}_goal\"]=\"${2#*:}\"; shift 2 ;;\n --field-instruction) { [[ $# -ge 2 ]] && [[ \"$2\" == *:* ]]; } || { echo \"ERROR: --field-instruction requires \\\"FieldApiName:instruction text\\\"\"; exit 1; }; FIELD_PROMPTS[\"${2%%:*}_instruction\"]=\"${2#*:}\"; shift 2 ;;\n --through-phase) { [[ $# -ge 2 ]] && [[ \"$2\" != --* ]]; } || { echo \"ERROR: --through-phase requires a value (currently: prompts)\"; exit 1; }; RUN_THROUGH_PHASE=\"$2\"; shift 2 ;;\n --from-phase) { [[ $# -ge 2 ]] && [[ \"$2\" != --* ]]; } || { echo \"ERROR: --from-phase requires a value (currently: flow)\"; exit 1; }; RUN_FROM_PHASE=\"$2\"; shift 2 ;;\n *) echo \"ERROR: Unknown option: $1\"; echo \"Usage: $0 \u003corg-alias> [--check-license] [--fields \\\"NextStep,StageName,Risk__c\\\"] [--field-goal \\\"Field:text\\\"] [--field-instruction \\\"Field:text\\\"] [--through-phase prompts] [--from-phase flow] [--autonomous] [--create-stage-descriptions] [--skip-prompt-verification] [--non-interactive|--yes] [--users \\\"[email protected],[email protected]\\\"]\"; exit 1 ;;\n esac\ndone\n\n# --- Validate phase-split flags ---\n# Only \"prompts\" (--through-phase) and \"flow\" (--from-phase) are recognized. They\n# are the two ends of the split; passing both, or an unknown name, is rejected so a\n# typo never silently runs the whole thing.\nif [[ -n \"$RUN_THROUGH_PHASE\" && \"$RUN_THROUGH_PHASE\" != \"prompts\" ]]; then\n echo \"ERROR: --through-phase supports only 'prompts' (got '$RUN_THROUGH_PHASE').\"; exit 1\nfi\nif [[ -n \"$RUN_FROM_PHASE\" && \"$RUN_FROM_PHASE\" != \"flow\" ]]; then\n echo \"ERROR: --from-phase supports only 'flow' (got '$RUN_FROM_PHASE').\"; exit 1\nfi\nif [[ -n \"$RUN_THROUGH_PHASE\" && -n \"$RUN_FROM_PHASE\" ]]; then\n echo \"ERROR: --through-phase and --from-phase are mutually exclusive (they are the two halves of a split run).\"; exit 1\nfi\n\n# A non-TTY stdin is treated as non-interactive regardless of flags, so prompts never hang.\nif [[ ! -t 0 ]]; then NON_INTERACTIVE=true; fi\n\n# The license preflight asks nothing and mutates nothing — force non-interactive\n# so the Phase 0 Setup Options block (guarded below) is skipped entirely.\nif [[ \"$CHECK_LICENSE_ONLY\" == \"true\" ]]; then NON_INTERACTIVE=true; fi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# Source shared libraries\nsource \"$SCRIPT_DIR/shared/agent-bundle-publish.sh\"\nsource \"$SCRIPT_DIR/shared/soap.sh\"\nsource \"$SCRIPT_DIR/shared/psg.sh\"\nsource \"$SCRIPT_DIR/shared/flow-builder.sh\"\nsource \"$SCRIPT_DIR/shared/stage-descriptions.sh\"\n\ntrap 'echo \"\"; echo \"Setup interrupted.\"; exit 130' INT\n\n# --- Configuration Constants ---\nreadonly API_VERSION=\"64.0\"\nreadonly CURL_TIMEOUT=30\nreadonly SLEEP_SHORT=2\nreadonly SLEEP_MEDIUM=3\nreadonly SLEEP_LONG=5\nreadonly SLEEP_TOGGLE_OFF=10\nreadonly SLEEP_TOGGLE_ON=20\n\n# --- Helpers ---\nlog_pass() { echo \" [PASS] $1\"; }\nlog_warn() { echo \" [WARN] $1\"; }\nlog_fail() { echo \" [FAIL] $1\"; }\nlog_info() { echo \" [....] $1\"; }\nlog_try() { echo \" [TRY ] $1\"; }\nsection() { echo \"\"; echo \"=== $1 ===\"; echo \"\"; }\n\n# with_timeout \u003cseconds> \u003ccmd...> — run a command under a wall-clock bound so a\n# hung network call (e.g. the /generations REST request, which has no built-in\n# --max-time) can't stall the whole run past the harness's tool timeout. Uses\n# coreutils `timeout` (or `gtimeout`) when available; on a stock macOS that has\n# neither, it degrades to running the command unwrapped rather than failing.\n_TIMEOUT_BIN=\"$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)\"\nwith_timeout() {\n local secs=\"$1\"; shift\n if [[ -n \"$_TIMEOUT_BIN\" ]]; then\n \"$_TIMEOUT_BIN\" \"${secs}s\" \"$@\"\n else\n \"$@\"\n fi\n}\n\n# confirm \u003cprompt> — yes/no gate. In non-interactive mode returns the SAFE default\n# (decline: return 1) without prompting. Only ever use as `if confirm ...; then`,\n# never as a bare last statement (a nonzero return would trip `set -e`).\nconfirm() {\n local prompt=\"$1\"\n if [[ \"$NON_INTERACTIVE\" == \"true\" ]]; then\n return 1\n fi\n local reply=\"\"\n read -rp \" $prompt [y/N]: \" reply || reply=\"\"\n [[ \"$reply\" =~ ^[Yy] ]]\n}\n\n# schedule_flow_active \u003corg-alias> — resolve the Pipeline Management suggestion flow\n# using the SAME SourceTemplateId-first resolver as Phase 5 (NOT a fuzzy label match),\n# and print its IsActive state as \"true\" | \"false\" | \"none\" (no flow found). This keeps\n# the summary consistent with detection elsewhere (#6): the old summary read used a\n# label LIKE '%OppSugg%' query that could miss the deployed flow and report it as\n# \"Not Found\" even right after a successful deploy.\nschedule_flow_active() {\n local org=\"$1\" api_name=\"\" q\n for q in \\\n \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false\" \\\n \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=false\" \\\n \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false\"; do\n local rec\n rec=$(sf data query --query \"$q\" --target-org \"$org\" --json 2>/dev/null \\\n | jq -r 'if (.result.records | length) > 0 then (if .result.records[0].IsActive == true then \"true\" else \"false\" end) else empty end' 2>/dev/null || echo \"\")\n if [[ -n \"$rec\" ]]; then printf '%s' \"$rec\"; return 0; fi\n done\n printf 'none'\n}\n\n# sanitize_dev_name \u003craw> [prefix] — derive a VALID Salesforce DeveloperName.\n# DeveloperName must be alphanumeric/underscore, start with a letter, and must NOT\n# contain consecutive underscores or a trailing underscore. The naive\n# `tr ' ' '_' | sed 's/\\./_/g'` turned \"Id. Decision Makers\" into \"Id__Decision_Makers\"\n# (double underscore) which the platform REJECTS — the silent cause behind #8.\n# This collapses runs of underscores, trims leading/trailing underscores, and\n# ensures a leading letter.\nsanitize_dev_name() {\n local raw=\"$1\" prefix=\"${2:-}\" name\n name=$(printf '%s' \"${prefix:+${prefix}_}${raw}\" \\\n | tr ' ' '_' \\\n | sed 's/[^A-Za-z0-9_]/_/g' \\\n | sed -E 's/_+/_/g' \\\n | sed -E 's/^_+//; s/_+$//')\n # DeveloperName must start with a letter.\n if [[ ! \"$name\" =~ ^[A-Za-z] ]]; then name=\"X_${name}\"; fi\n printf '%s' \"$name\"\n}\n\n# Safe jq extraction - strips CLI warnings, handles parse failures gracefully\nsafe_jq() {\n local json=\"$1\" expr=\"$2\" default=\"${3:-}\"\n [[ -z \"$json\" ]] && { printf '%s' \"$default\"; return; }\n local cleaned\n # Strip ANSI color escapes (sf --json can emit them into stdout), THEN find JSON start.\n cleaned=$(printf '%s' \"$json\" | sed -E 's/'

Agentforce Pipeline Management Configuration Configure Agentforce Pipeline Management end-to-end in a Salesforce org. This skill handles both greenfield orgs (zero configuration) and partially configured orgs that need repair or completion. Scope - In scope : Enabling Pipeline Management, configuring the Sales Management agent, activating/customizing flows (Process Field Update Suggestions, Get Opportunity Grounding Data, Get AI Recommendations from Call Transcripts), prompt template configuration, permission set assignment, data source setup, opportunity stage descriptions, autonomous field…

\\x1b''\\[[0-9;]*m//g' | sed -n '/^[[:space:]]*[{[]/,$p')\n printf '%s' \"$cleaned\" | jq -r \"$expr\" 2>/dev/null || printf '%s' \"$default\"\n}\n\nsafe_jq_int() {\n local result\n result=$(safe_jq \"$1\" \"$2\" \"0\")\n [[ \"$result\" =~ ^[0-9]+$ ]] && echo \"$result\" || echo \"0\"\n}\n\n# Progress indicators for long-running operations (write to stderr to avoid polluting piped output)\nshow_progress() {\n [[ -t 2 ]] || return 0\n local msg=\"${1:-Working...}\"\n printf '\\r [....] %s...' \"$msg\" >&2\n}\n\nclear_progress() {\n [[ -t 2 ]] || return 0\n printf '\\r\\033[K' >&2\n}\n\n# Sanitize display strings (remove control chars, truncate)\nsanitize_display() {\n local input=\"$1\" max_len=\"${2:-80}\"\n printf '%s' \"$input\" | tr -d '\\000-\\037' | cut -c1-\"$max_len\"\n}\n\n# Sanitize a value for safe SOQL interpolation (alphanumeric, underscores, dots, spaces, slashes)\nsanitize_soql_value() {\n local input=\"$1\"\n printf '%s' \"$input\" | tr -cd 'A-Za-z0-9_./ -'\n}\n\n# redact_token() is provided by shared/soap.sh (sourced above). The shared version\n# fixes session-id tail under-redaction (W-23215519). Do not redefine it here.\n\n# Helper: Get license count with error handling\n# Returns: license count or \"ERROR\" if query fails\nget_license_count() {\n local license_name=\"$1\"\n local result\n result=$(sf data query --query \"SELECT TotalLicenses FROM UserLicense WHERE Name = '$license_name'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>&1)\n\n if echo \"$result\" | grep -qi \"ERROR\"; then\n echo \"ERROR\"\n else\n safe_jq \"$result\" '.result.records[0].TotalLicenses // 0' \"0\"\n fi\n}\n\n# --- State tracking for before/after summary ---\ndeclare -A SETUP_STATE_BEFORE\ndeclare -A SETUP_STATE_AFTER\ndeclare -A SETUP_CHANGES\ndeclare -a SETUP_ISSUES=()\ndeclare -a SETUP_DEPENDENCIES=()\n\ntrack_before() {\n local KEY=$1\n local VALUE=$2\n SETUP_STATE_BEFORE[\"$KEY\"]=\"$VALUE\"\n}\n\ntrack_after() {\n local KEY=$1\n local VALUE=$2\n SETUP_STATE_AFTER[\"$KEY\"]=\"$VALUE\"\n\n # Determine change status\n local BEFORE=\"${SETUP_STATE_BEFORE[$KEY]:-N/A}\"\n if [[ \"$BEFORE\" == \"N/A\" ]]; then\n SETUP_CHANGES[\"$KEY\"]=\"🆕 CREATED\"\n elif [[ \"$BEFORE\" != \"$VALUE\" ]]; then\n SETUP_CHANGES[\"$KEY\"]=\"✏️ CHANGED\"\n else\n SETUP_CHANGES[\"$KEY\"]=\"✅ NO CHANGE\"\n fi\n}\n\ntrack_issue() {\n local SEVERITY=$1\n local DESCRIPTION=$2\n local RESOLUTION=$3\n SETUP_ISSUES+=(\"$SEVERITY|$DESCRIPTION|$RESOLUTION\")\n}\n\ntrack_dependency() {\n local PHASE=$1\n local DEPENDS_ON=$2\n SETUP_DEPENDENCIES+=(\"$PHASE|$DEPENDS_ON\")\n}\n\n\nif [[ -z \"$ORG_ALIAS\" ]]; then\n echo \"Usage: $0 \u003corg-alias> [--autonomous] [--create-stage-descriptions] [--non-interactive|--yes]\"\n exit 1\nfi\n\n# ============================================================\n# Phase 0: Consent preamble\n# ============================================================\n# Pipeline Management setup is provisioning-heavy. Two behaviours change what the\n# agent does to the org and are therefore gated on explicit consent:\n# 1. Autonomous field updates — the agent auto-applies suggestions with no human review.\n# 2. Stage-description creation — only needed for StageName suggestions; may overwrite\n# or create descriptions the org intentionally left blank.\n# Interactive runs are asked here; non-interactive runs use the SAFE resolved defaults\n# (opt-in only via --autonomous / --create-stage-descriptions).\n# The --check-license preflight skips this block wholesale — it decides nothing\n# about provisioning behaviour and must ask the user nothing.\nif [[ \"$CHECK_LICENSE_ONLY\" != \"true\" ]]; then\nsection \"Phase 0: Setup Options\"\n\nif [[ \"$NON_INTERACTIVE\" == \"true\" ]]; then\n log_info \"Non-interactive mode — using resolved options:\"\nelse\n echo \" Before provisioning, confirm two optional behaviours (both default to OFF/safe):\"\n echo \"\"\n # Autonomous updates: opt-in. Flag pre-answers; otherwise ask.\n if [[ \"$ENABLE_AUTONOMOUS\" != \"true\" ]]; then\n if confirm \"Let the agent update fields AUTONOMOUSLY (auto-apply without human review)? Default is suggestions-only\"; then\n ENABLE_AUTONOMOUS=true\n fi\n fi\n # Stage descriptions: opt-in, and only relevant if the user wants StageName suggestions.\n if [[ \"$CREATE_STAGE_DESCRIPTIONS\" != \"true\" ]]; then\n if confirm \"Create sales-methodology stage descriptions (only needed for StageName suggestions)?\"; then\n CREATE_STAGE_DESCRIPTIONS=true\n fi\n fi\n echo \"\"\nfi\n\necho \" • Autonomous field updates: $([[ \"$ENABLE_AUTONOMOUS\" == \"true\" ]] && echo \"ENABLED (auto-apply)\" || echo \"off (suggestions only)\")\"\necho \" • Stage-description creation: $([[ \"$CREATE_STAGE_DESCRIPTIONS\" == \"true\" ]] && echo \"ENABLED\" || echo \"off\")\"\nfi # END Phase 0 Setup Options (skipped under --check-license)\n\n# ============================================================\n# Phase 1: Authentication\n# ============================================================\nsection \"Phase 1: Authentication\"\n\n# Filter CLI warnings that pollute JSON output (causes jq parse errors)\n# Use buffering pattern to avoid grep eating newlines that jq needs\nORG_DISPLAY_OUTPUT=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>&1)\nINSTANCE_URL=$(safe_jq \"$ORG_DISPLAY_OUTPUT\" '.result.instanceUrl // empty' \"\")\nif [[ -z \"$INSTANCE_URL\" ]]; then\n log_fail \"Cannot connect to org '$ORG_ALIAS'. Run: sf org login web --alias $ORG_ALIAS\"\n exit 1\nfi\n# Enforce HTTPS on instance URL (prevent MITM with access tokens)\nif [[ -n \"$INSTANCE_URL\" && ! \"$INSTANCE_URL\" =~ ^https:// ]]; then\n log_fail \"Instance URL is not HTTPS: $INSTANCE_URL\"\n log_fail \"Refusing to send access tokens over insecure connection.\"\n exit 1\nfi\n\n# Extract access token with same warning filtering\nACCESS_TOKEN=$(safe_jq \"$ORG_DISPLAY_OUTPUT\" '.result.accessToken // empty' \"\")\nif [[ -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" || \"$ACCESS_TOKEN\" == *\"REDACTED\"* ]]; then\n TOKEN_OUTPUT=$(echo \"y\" | sf org auth show-access-token --target-org \"$ORG_ALIAS\" --no-prompt --json 2>&1)\n ACCESS_TOKEN=$(safe_jq \"$TOKEN_OUTPUT\" '.result.accessToken // empty' \"\")\nfi\n\nif [[ -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" ]]; then\n log_fail \"Cannot extract access token. Re-authenticate: sf org login web --alias $ORG_ALIAS\"\n exit 1\nfi\n\nlog_pass \"Connected to $(sanitize_display \"$INSTANCE_URL\" 80)\"\n\n# Detect test/trial/scratch orgs and warn about potential API restrictions\nif [[ -n \"$INSTANCE_URL\" ]]; then\n if [[ \"$INSTANCE_URL\" =~ (test[0-9]*\\.|\\.pc-rnd\\.|scratch|sandbox|-dev-ed\\.|\\.trailhead\\.|--) ]]; then\n log_warn \"TEST/TRIAL ORG DETECTED: $INSTANCE_URL\"\n echo \"\"\n echo \" Note: Test, trial, and sandbox orgs may have SOAP API restrictions.\"\n echo \" This script will use alternative approaches (REST API, CLI) where possible.\"\n echo \" Some features may require manual UI enablement if API access is limited.\"\n echo \"\"\n fi\nfi\n\n# --- API Helpers ---\n# NOTE: These helpers are defined here — right after Phase 1 Authentication — so\n# they are available to the Step 0 capability gate (Phase 0.5, below) that must run\n# BEFORE any interactive question. They depend only on INSTANCE_URL/ACCESS_TOKEN\n# (set in Phase 1), API_VERSION/CURL_TIMEOUT (readonly, top), and\n# show_progress/clear_progress (top-level) — nothing from Phase 1.7 onward. They\n# are also used by BLOCK B (soap_read/soap_extract_bool in the AFTER snapshot,\n# sed_inplace in the flow reconciliation, ensure_sfdx_project before flow/agent\n# deploys); on a --from-phase flow run BLOCK A is skipped entirely, so defining\n# them at this top level keeps them in scope for both blocks and Step 0.\n\n# SOAP readMetadata — returns raw XML response\nsoap_read() {\n local TYPE=\"$1\" NAME=\"$2\"\n curl -s --max-time $CURL_TIMEOUT \"${INSTANCE_URL}/services/Soap/m/${API_VERSION}\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>${TYPE}\u003c/met:type>\u003cmet:fullNames>${NAME}\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null\n}\n\n# Classify a readMetadata response via the shared shared/soap.sh parser.\n# Distinguishes a genuine \"false\" from an auth/SOAP fault or a network/service\n# error (the W-23215519 fix) instead of collapsing all three to empty.\n# soap_read() does not capture an HTTP status, so \"200\" is passed as the\n# nominal status; the parser still detects empty bodies, SOAP faults, HTML\n# error pages, and truncated XML from the response content itself.\n# Args: $1 = readMetadata XML, $2 = field name\n# Echoes: \"true\" | \"false\" | \"AUTH_ERROR:...\" | \"TYPE_UNAVAILABLE:...\" | \"NETWORK_ERROR\"\nsoap_classify() {\n local XML=\"$1\" FIELD=\"$2\"\n parse_soap_response \"$XML\" \"$FIELD\" \"200\"\n}\n\n# SOAP updateMetadata — returns raw XML response\nsoap_update() {\n local BODY=\"$1\"\n show_progress \"SOAP Metadata API call\"\n local response\n response=$(curl -s --max-time $CURL_TIMEOUT \"${INSTANCE_URL}/services/Soap/m/${API_VERSION}\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: update\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:updateMetadata>${BODY}\u003c/met:updateMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\n clear_progress\n printf '%s' \"$response\"\n}\n\n# Extract a boolean field value from SOAP XML response.\n# Delegates to the shared shared/soap.sh parser for robust extraction (handles\n# truncated XML, HTML error pages, multi-record responses) but preserves this\n# function's original contract — \"true\", \"false\", or \"\" — so the ~15 call sites\n# that only test `== \"true\"` keep their exact behavior. Auth/type/network\n# classifications collapse to \"\" here (same as the old grep failure); callers\n# that need to distinguish them use soap_classify directly (see the gate).\nsoap_extract_bool() {\n local XML=\"$1\" FIELD=\"$2\"\n local result\n result=$(parse_soap_response \"$XML\" \"$FIELD\" \"200\")\n case \"$result\" in\n true) echo \"true\" ;;\n false) echo \"false\" ;;\n *) echo \"\" ;;\n esac\n}\n\n# Cross-platform sed -i\nsed_inplace() {\n if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n sed -i '' \"$@\"\n else\n sed -i \"$@\"\n fi\n}\n\n# Ensure sfdx-project.json exists for CLI operations\nensure_sfdx_project() {\n if [[ ! -f \"sfdx-project.json\" ]]; then\n cat > sfdx-project.json \u003c\u003c 'SFDX'\n{\"packageDirectories\": [{\"path\": \"force-app\", \"default\": true}], \"sourceApiVersion\": \"67.0\"}\nSFDX\n fi\n}\n\n# Print the \"org can't enable Pipeline Management\" blocker body (license list,\n# edition guidance, Setup URL). Shared by the Step 0 gate (Phase 0.5) and the\n# Phase 2.5 backstop so the message is authored once. Callers own the log_fail\n# banner and the exit; this only prints the explanatory body.\n# Args: $1 = Einstein Agent license count, $2 = Agentforce-for-Sales license count\n# (either may be a number, \"unknown\", or empty)\nprint_license_blocker() {\n local ea_count=\"${1:-unknown}\" sales_count=\"${2:-unknown}\"\n echo \"\"\n echo \" BLOCKER: This org cannot enable Pipeline Management programmatically.\"\n echo \"\"\n echo \" Reason: The SalesDealAgentSettings metadata type is not provisioned.\"\n echo \" This typically means the org is missing required licenses.\"\n echo \"\"\n echo \" Current org licenses:\"\n echo \" • Einstein Agent licenses: $ea_count\"\n echo \" • Agentforce for Sales licenses: $sales_count\"\n echo \"\"\n echo \" Pipeline Management requires:\"\n echo \" ✓ Agentforce for Sales add-on license, OR\"\n echo \" ✓ Sales Cloud with Einstein capabilities, OR\"\n echo \" ✓ Agentforce 1 Sales Edition\"\n echo \"\"\n echo \" Actions:\"\n echo \" 1. Contact your Salesforce account team to provision Agentforce for Sales licenses\"\n echo \" 2. Verify org edition supports Pipeline Management (Enterprise Edition or higher)\"\n echo \" 3. Try manual enablement via Setup UI (if available):\"\n echo \" ${INSTANCE_URL}/lightning/setup/PipelineManagement/home\"\n echo \" (Note: Assumes Lightning Experience. For Classic, use Setup menu.)\"\n echo \"\"\n}\n\n# ============================================================\n# Phase 0.5: License & Capability Gate (Step 0)\n# ============================================================\n# Block an ineligible org UP FRONT — before any interactive question (Phase 1.7\n# field selection) or mutation. The authoritative signal is the same one the\n# Phase 2.5 backstop uses: probe the SalesDealAgentSettings metadata type over\n# SOAP and hard-block ONLY on INVALID_TYPE (soap_classify => TYPE_UNAVAILABLE).\n# License/edition COUNTS are informational context, never a block, because\n# UserLicense can read 0 while PM still works via base Einstein Agent\n# entitlements. An already-provisioned org has the type (probe => true/false),\n# so it passes and idempotent re-runs stay green. Runs on BOTH the single-shot\n# and --from-phase flow paths — it's read-only and fast.\nsection \"Phase 0.5: License & Capability Gate\"\n\nSTEP0_CAP=$(soap_classify \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgent\")\n\n# Informational context (non-blocking): edition + license counts.\nSTEP0_EDITION=$(sf data query --query \"SELECT OrganizationType FROM Organization\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].OrganizationType // \"unknown\"' 2>/dev/null || echo \"unknown\")\nSTEP0_EA_COUNT=$(get_license_count \"Einstein Agent\")\n[[ \"$STEP0_EA_COUNT\" == \"ERROR\" ]] && STEP0_EA_COUNT=\"unknown\"\nSTEP0_SALES_COUNT=$(safe_jq_int \"$(sf data query --query \"SELECT COUNT(Id) FROM UserLicense WHERE (Name LIKE '%Agentforce for Sales%' OR Name LIKE '%Sales Cloud Einstein%' OR Name LIKE '%Agentforce 1%') AND TotalLicenses > 0\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\" '.result.records[0].expr0 // 0')\n\necho \" Org edition: $STEP0_EDITION\"\necho \" Einstein Agent licenses: $STEP0_EA_COUNT\"\necho \" Agentforce for Sales / Sales Cloud Einstein / Agentforce 1 licenses: $STEP0_SALES_COUNT\"\necho \"\"\n\ncase \"$STEP0_CAP\" in\n true|false)\n log_pass \"GATE: Pipeline Management capability confirmed (SalesDealAgentSettings available) ✓\"\n ;;\n TYPE_UNAVAILABLE:*)\n log_fail \"GATE: This org is not licensed for Pipeline Management\"\n print_license_blocker \"$STEP0_EA_COUNT\" \"$STEP0_SALES_COUNT\"\n echo \" Stopping now — before any configuration questions — so nothing is changed on an org that cannot run Pipeline Management.\"\n echo \"\"\n exit 1\n ;;\n AUTH_ERROR:*)\n log_warn \"GATE: Couldn't confirm capability over SOAP yet (SOAP API access issue).\"\n echo \" This is a separate, fixable prerequisite — it's re-checked and explained\"\n echo \" at the SOAP prerequisites step (Phase 1.5). Proceeding.\"\n echo \"\"\n ;;\n NETWORK_ERROR|*)\n log_warn \"GATE: Capability probe inconclusive (transient network/service error). Proceeding; the Phase 2.5 backstop will re-check.\"\n echo \"\"\n ;;\nesac\n\n# Non-blocking permission reminder — scripts cannot check these; the executing\n# user must hold them for setup to complete (see SKILL.md \"You must verify\").\necho \" Reminder — the executing user needs these permissions (not script-checkable):\"\necho \" • View Setup AND (Modify All Data OR Customize Application)\"\necho \" • Manage AI Agents AND (Manage Agentforce Employee Agents OR Customize Application)\"\necho \" • Assign Permission Sets\"\necho \"\"\n\n# --- License preflight stops here (--check-license) ---\n# We've run auth + the Phase 0.5 gate. If the org were unlicensed the gate would\n# already have printed the blocker and exit 1'd; reaching here means it's capable\n# (or the probe was inconclusive — a non-blocking AUTH/NETWORK warning). Exit 0 so\n# the driving agent can proceed to ask its clarifying questions. No mutation ran.\nif [[ \"$CHECK_LICENSE_ONLY\" == \"true\" ]]; then\n log_pass \"Preflight passed — org is capable of Pipeline Management. No changes made.\"\n echo \" Proceed to configuration (field selection, prompt tuning, then setup).\"\n echo \"\"\n exit 0\nfi\n\n# ============================================================\n# Phase 1.7: Field Selection (up front, before any provisioning)\n# ============================================================\n# 3-phase setup, part 1 of the \"field\" story: decide WHICH Opportunity fields the\n# suggestion flow will manage BEFORE we build/deploy the flow, so the flow is built\n# with only those fields (no deploy-then-strip). Done here — right after auth,\n# before the heavy provisioning phases — so an empty/oversized selection fails fast\n# rather than after minutes of enablement work.\n#\n# Rules:\n# - At least 1 field REQUIRED; setup ABORTS on an empty selection (no default).\n# - Hard cap MAX_SUGGESTION_FIELDS (OOTB NextStep/StageName count toward it).\n# - StageName is a picklist: NOT prompt-customizable, and it needs stage\n# descriptions (Phase 6) — flagged via STAGENAME_SELECTED.\n# - Per non-StageName field we collect an optional goal/instruction for the\n# prompt template (NextStep -> OOTB override; custom fields -> new template).\nsection \"Phase 1.7: Field Selection\"\n\nif [[ -z \"$SELECTED_FIELDS\" ]]; then\n if [[ \"$NON_INTERACTIVE\" == \"true\" ]]; then\n log_fail \"No fields selected. Pipeline Management needs at least one field to manage.\"\n echo \" Pass --fields with a comma-separated list of Opportunity field API names.\"\n echo \" OOTB fields: NextStep (Next Step), StageName (Opportunity Stage).\"\n echo \" Example: $0 $ORG_ALIAS --fields \\\"NextStep\\\" --non-interactive\"\n exit 1\n fi\n echo \"Which Opportunity fields should the agent generate suggestions for?\"\n echo \" • NextStep — Next Step (OOTB; works out of the box)\"\n echo \" • StageName — Opportunity Stage (OOTB; requires stage descriptions)\"\n echo \" • \u003cAPI name> — any eligible custom text field (Text or Text Area \u003c=255)\"\n echo \"\"\n echo \" Enter a comma-separated list (at least 1, at most $MAX_SUGGESTION_FIELDS). Setup aborts if empty.\"\n read -rp \" Fields: \" SELECTED_FIELDS || SELECTED_FIELDS=\"\"\nfi\n\n# Normalize: trim whitespace around each entry, drop empties, de-dup (order-preserving).\ndeclare -a FIELD_ARRAY=()\nIFS=',' read -ra _raw_fields \u003c\u003c\u003c \"$SELECTED_FIELDS\"\nfor _f in \"${_raw_fields[@]}\"; do\n _f=\"$(echo \"$_f\" | xargs)\" # trim\n [[ -z \"$_f\" ]] && continue\n _dup=false\n for _seen in \"${FIELD_ARRAY[@]:-}\"; do [[ \"$_seen\" == \"$_f\" ]] && { _dup=true; break; }; done\n [[ \"$_dup\" == \"true\" ]] || FIELD_ARRAY+=(\"$_f\")\ndone\n\nif [[ ${#FIELD_ARRAY[@]} -eq 0 ]]; then\n log_fail \"No fields selected. Setup aborted — Pipeline Management requires at least one field.\"\n echo \" Re-run and select at least one field (e.g., NextStep).\"\n exit 1\nfi\nif [[ ${#FIELD_ARRAY[@]} -gt $MAX_SUGGESTION_FIELDS ]]; then\n log_fail \"Too many fields selected (${#FIELD_ARRAY[@]}). Maximum is $MAX_SUGGESTION_FIELDS (OOTB fields count toward the cap).\"\n exit 1\nfi\n\n# Rebuild the canonical comma list from the normalized array so downstream\n# consumers (flow builder, summary) see the de-duped, trimmed set.\nSELECTED_FIELDS=\"$(IFS=,; echo \"${FIELD_ARRAY[*]}\")\"\n\nSTAGENAME_SELECTED=false\nfor _f in \"${FIELD_ARRAY[@]}\"; do [[ \"$_f\" == \"StageName\" ]] && STAGENAME_SELECTED=true; done\n\nlog_pass \"Fields selected (${#FIELD_ARRAY[@]}): $SELECTED_FIELDS\"\n[[ \"$STAGENAME_SELECTED\" == \"true\" ]] && log_info \"StageName selected — stage descriptions will be needed (Phase 6).\"\n\n# Collect prompt customization for each non-StageName field. StageName is a\n# picklist and is never customizable. Interactive only — in non-interactive mode\n# the goal/instruction come from --field-goal / --field-instruction (or defaults\n# inside add-field-suggestion.sh). NextStep with no customization stays on its\n# curated OOTB managed prompt.\nif [[ \"$NON_INTERACTIVE\" != \"true\" ]]; then\n for _f in \"${FIELD_ARRAY[@]}\"; do\n [[ \"$_f\" == \"StageName\" ]] && continue\n if [[ \"$_f\" == \"NextStep\" ]]; then\n confirm \"Customize the AI prompt for NextStep? (N = keep the OOTB prompt)\" || continue\n else\n echo \"\"\n echo \"Configure the AI prompt for custom field '$_f':\"\n fi\n read -rp \" Goal (completes 'You must think about ...'): \" _goal || _goal=\"\"\n read -rp \" Instruction (one line of extraction guidance): \" _instr || _instr=\"\"\n [[ -n \"$_goal\" ]] && FIELD_PROMPTS[\"${_f}_goal\"]=\"$_goal\"\n [[ -n \"$_instr\" ]] && FIELD_PROMPTS[\"${_f}_instruction\"]=\"$_instr\"\n done\nfi\necho \"\"\n\n# ============================================================\n# Pre-Block-A: Snapshot org state for accurate Configuration Summary\n# (W-23277501 — status column must reflect before-vs-after, not hardcoded\n# greenfield literals, so idempotent reruns report UNCHANGED)\n# ============================================================\n# This is READ-ONLY org state and MUST run on BOTH the single-shot and the\n# --from-phase flow paths, so it lives ABOVE the Block A `if`. It was previously\n# inside Block A, which left BEFORE_* unbound on a --from-phase flow run and\n# crashed the Phase 8 before/after table under `set -u`. All helpers it calls\n# (soap_read, soap_extract_bool, schedule_flow_active, sanitize_display) are\n# top-level, so this is safe here.\n# Cache one SOAP read so both settings are extracted from the same document\nSOAP_SETTINGS_BEFORE=$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\" 2>/dev/null || echo \"\")\nBEFORE_PM=$(soap_extract_bool \"$SOAP_SETTINGS_BEFORE\" \"enableDealAgent\" 2>/dev/null || echo \"false\")\nBEFORE_AUTO=$(soap_extract_bool \"$SOAP_SETTINGS_BEFORE\" \"enableDealAgentAutoApproveAllTasks\" 2>/dev/null || echo \"false\")\nBEFORE_AGENT_USER=$(sf data query --query \"SELECT Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Username // \"Not Created\"' 2>/dev/null || echo \"Not Created\")\nBEFORE_AGENT_USER=$(sanitize_display \"$BEFORE_AGENT_USER\" 60)\nBEFORE_PSG_COUNT=$(sf data query --query \"SELECT Id FROM PermissionSetGroup WHERE DeveloperName IN ('SalesManagementUserPsg','SalesManagementAgentUserPsg')\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || echo \"0\")\nBEFORE_PSG_COUNT=$((${BEFORE_PSG_COUNT:-0} + 0))\n# Normalize jq boolean output — treat literal null / missing / non-true as \"none\"\nBEFORE_FLOW_ACTIVE=$(schedule_flow_active \"$ORG_ALIAS\")\n# OpptStageDescription does not exist until Pipeline Management is enabled, so on\n# a greenfield org this query fails with INVALID_TYPE. Under `set -o pipefail` the\n# failed pipeline leaves jq's \"0\" on stdout AND (with a trailing `|| echo \"0\"`)\n# would append a second \"0\", yielding a two-line value that crashes the arithmetic\n# below with \"0\\n0\". Swallow the failure with `|| true` and let jq's `// 0` supply\n# the default — this preserves the BEFORE snapshot (W-23277501) on greenfield orgs.\nBEFORE_STAGE_DESC_COUNT=$(sf data query --query \"SELECT Id FROM OpptStageDescription\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || true)\nBEFORE_STAGE_DESC_COUNT=$((${BEFORE_STAGE_DESC_COUNT:-0} + 0))\n\n# ============================================================\n# BLOCK A — Provisioning & Prompts (Phase 1.5 → 4e)\n# ============================================================\n# Everything from SOAP prereqs through prompt-template deploy + the verification\n# gate. On a --from-phase flow run this entire block is SKIPPED: the org was\n# already enabled/provisioned and the templates deployed+tuned in the prior\n# --through-phase prompts call, so we jump straight to building the flow (Phase 5)\n# from the agent-approved --fields set. The context phases above (0/1/1.7) always\n# run, so SELECTED_FIELDS / FIELD_ARRAY / STAGENAME_SELECTED are populated either way.\nif [[ \"$RUN_FROM_PHASE\" == \"flow\" ]]; then\n section \"Provisioning & Prompts (Phase 1.5–4e)\"\n log_info \"Skipped (--from-phase flow): org already enabled and prompt templates deployed in the prior --through-phase prompts run.\"\n log_info \"Building the flow from the approved field set: $SELECTED_FIELDS\"\nelse\n\n# ============================================================\n# Phase 1.5: SOAP API Prerequisites Verification\n# ============================================================\nsection \"Phase 1.5: SOAP API Prerequisites\"\n\necho \"This script requires SOAP API access to enable Pipeline Management settings.\"\necho \"Testing SOAP API connectivity...\"\necho \"\"\n\n# Test SOAP API with a simple read operation\nshow_progress \"Testing SOAP API connectivity\"\nSOAP_TEST=$(curl -s --max-time $CURL_TIMEOUT \"${INSTANCE_URL}/services/Soap/m/${API_VERSION}\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\u003cmet:type>EinsteinGptSettings\u003c/met:type>\u003cmet:fullNames>EinsteinGpt\u003c/met:fullNames>\u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>&1)\n\nclear_progress\n\n# Check for INVALID_LOGIN error (most common SOAP access issue)\nif echo \"$SOAP_TEST\" | grep -q \"INVALID_LOGIN\"; then\n log_fail \"SOAP API login failed\"\n echo \"\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \" BLOCKER: SOAP API Access Not Enabled\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \"\"\n echo \"This script uses the SOAP Metadata API to enable Pipeline Management\"\n echo \"settings. Your org or user account does not have SOAP API access enabled.\"\n echo \"\"\n echo \"Error received: INVALID_LOGIN\"\n echo \"\"\n echo \"SOAP API requires two prerequisites:\"\n echo \"\"\n echo \"1️⃣ ORG-LEVEL SETTING: Enable SOAP API Login\"\n echo \"\"\n echo \" a. Navigate to: ${INSTANCE_URL}/lightning/setup/UserInterfaceUI/home\"\n echo \" OR: Setup → Quick Find: 'User Interface'\"\n echo \"\"\n echo \" b. Check the box: ☑ Enable SOAP API login()\"\n echo \"\"\n echo \" c. Click Save\"\n echo \"\"\n echo \"2️⃣ USER-LEVEL PERMISSION: Grant 'Use Any API Auth' Permission\"\n echo \"\"\n echo \" Option A: Via Permission Set (Recommended)\"\n echo \" ─────────────────────────────────────────\"\n echo \" a. Navigate to: ${INSTANCE_URL}/lightning/setup/PermSets/home\"\n echo \" OR: Setup → Quick Find: 'Permission Sets'\"\n echo \"\"\n echo \" b. Create new Permission Set (e.g., 'API Access')\"\n echo \"\"\n echo \" c. Click 'System Permissions' → Edit\"\n echo \"\"\n echo \" d. Enable: ☑ Use Any API Auth\"\n echo \"\"\n echo \" e. Save → Manage Assignments → Assign to your user\"\n echo \"\"\n echo \" Option B: Via Profile (Alternative)\"\n echo \" ────────────────────────────────────\"\n echo \" a. Navigate to your user profile settings\"\n echo \"\"\n echo \" b. Edit → System Permissions\"\n echo \"\"\n echo \" c. Enable: ☑ Use Any API Auth\"\n echo \"\"\n echo \" d. Save\"\n echo \"\"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \"\"\n echo \"After enabling both prerequisites, re-run this script:\"\n echo \" bash setup-all.sh $ORG_ALIAS\"\n echo \"\"\n exit 1\nfi\n\n# Check for other SOAP errors\nif echo \"$SOAP_TEST\" | grep -q \"\u003cfaultcode>\"; then\n FAULT_CODE=$(echo \"$SOAP_TEST\" | grep -o \"\u003cfaultcode>[^\u003c]*\" | sed 's/\u003cfaultcode>//')\n FAULT_STRING=$(redact_token \"$(echo \"$SOAP_TEST\" | grep -o \"\u003cfaultstring>[^\u003c]*\" | sed 's/\u003cfaultstring>//')\")\n\n log_warn \"SOAP API test returned an error\"\n echo \"\"\n echo \" Fault Code: $FAULT_CODE\"\n echo \" Fault Message: $FAULT_STRING\"\n echo \"\"\n\n if [[ \"$FAULT_CODE\" == *\"INVALID_TYPE\"* ]]; then\n log_info \"This may be expected for test/trial orgs. Proceeding...\"\n track_issue \"⚠️ WARN\" \"SOAP API returned INVALID_TYPE\" \"Expected for test/trial orgs - Continued\"\n echo \"\"\n else\n echo \" This may indicate a SOAP API configuration issue.\"\n echo \" If setup fails, verify SOAP API prerequisites above.\"\n track_issue \"⚠️ WARN\" \"SOAP API error: $FAULT_CODE\" \"User confirmed continue\"\n echo \"\"\n # Non-interactive: skip (don't block on user input in agent context)\n if [[ -t 0 ]]; then\n read -p \"Press Enter to continue anyway, or Ctrl+C to exit...\"\n fi\n echo \"\"\n fi\nelse\n log_pass \"SOAP API access verified ✓\"\nfi\n\n# --- Generic enable-and-verify harness ---\n# Tries SOAP → CLI deploy → verifies via SOAP readMetadata\n# Returns:\n# 0 = success\n# 1 = failure (all approaches exhausted)\n# 2 = metadata type not available (INVALID_TYPE - org lacks license/edition)\n#\n# Usage: enable_setting \u003cMetadataType> \u003cfullName> \u003cfield> \u003cdisplayName>\nenable_setting() {\n local META_TYPE=\"$1\"\n local FULL_NAME=\"$2\"\n local FIELD=\"$3\"\n local DISPLAY_NAME=\"$4\"\n\n # --- CHECK: already enabled? ---\n local CURRENT\n local READ_RESULT\n READ_RESULT=$(soap_read \"$META_TYPE\" \"$FULL_NAME\")\n\n # Check for INVALID_TYPE in read response (metadata type not available)\n if echo \"$READ_RESULT\" | grep -qi \"INVALID_TYPE\"; then\n log_warn \"$DISPLAY_NAME: metadata type not available in this org (licensing/edition limitation)\"\n return 2\n fi\n\n CURRENT=$(soap_extract_bool \"$READ_RESULT\" \"$FIELD\")\n if [[ \"$CURRENT\" == \"true\" ]]; then\n log_pass \"$DISPLAY_NAME: enabled\"\n return 0\n fi\n\n # --- APPROACH 1: SOAP updateMetadata ---\n log_try \"$DISPLAY_NAME: enabling via SOAP Metadata API...\"\n local SOAP_RESULT\n SOAP_RESULT=$(soap_update \"\u003cmet:metadata xsi:type='met:${META_TYPE}' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\u003cmet:fullName>${FULL_NAME}\u003c/met:fullName>\u003cmet:${FIELD}>true\u003c/met:${FIELD}>\u003c/met:metadata>\")\n\n # Check for INVALID_TYPE in update response\n if echo \"$SOAP_RESULT\" | grep -qi \"INVALID_TYPE\"; then\n log_warn \"$DISPLAY_NAME: metadata type not available in this org (licensing/edition limitation)\"\n return 2\n fi\n\n if echo \"$SOAP_RESULT\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n sleep $SLEEP_SHORT\n # VERIFY\n CURRENT=$(soap_extract_bool \"$(soap_read \"$META_TYPE\" \"$FULL_NAME\")\" \"$FIELD\")\n if [[ \"$CURRENT\" == \"true\" ]]; then\n log_pass \"$DISPLAY_NAME: enabled (SOAP)\"\n return 0\n fi\n log_warn \"$DISPLAY_NAME: SOAP reported success but verification failed. Retrying...\"\n sleep $SLEEP_LONG\n CURRENT=$(soap_extract_bool \"$(soap_read \"$META_TYPE\" \"$FULL_NAME\")\" \"$FIELD\")\n if [[ \"$CURRENT\" == \"true\" ]]; then\n log_pass \"$DISPLAY_NAME: enabled (SOAP, delayed verification)\"\n return 0\n fi\n fi\n\n local SOAP_ERR\n SOAP_ERR=$(echo \"$SOAP_RESULT\" | grep -o '\u003cmessage>[^\u003c]*\u003c/message>' | sed 's/\u003c[^>]*>//g' | head -1 || echo \"\")\n [[ -n \"$SOAP_ERR\" ]] && echo \" SOAP error: $(redact_token \"$SOAP_ERR\")\"\n\n # --- APPROACH 2: CLI retrieve → modify → deploy ---\n # Map MetadataType to CLI settings name (strip \"Settings\" suffix for CLI)\n local CLI_NAME=\"${META_TYPE%Settings}\"\n # Special cases where CLI name differs\n case \"$META_TYPE\" in\n EinsteinGptSettings) CLI_NAME=\"EinsteinGpt\" ;;\n EinsteinCopilotSettings) CLI_NAME=\"EinsteinCopilot\" ;;\n AgentPlatformSettings) CLI_NAME=\"AgentPlatform\" ;;\n EnhancedNotesSettings) CLI_NAME=\"EnhancedNotes\" ;;\n OpportunitySettings) CLI_NAME=\"Opportunity\" ;;\n SalesDealAgentSettings) CLI_NAME=\"SalesDealAgent\" ;;\n esac\n\n log_try \"$DISPLAY_NAME: trying CLI retrieve/modify/deploy...\"\n ensure_sfdx_project\n mkdir -p force-app/main/default/settings\n\n # Attempt retrieve\n sf project retrieve start --metadata \"Settings:${CLI_NAME}\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null >/dev/null || true\n\n local SETTINGS_FILE=\"force-app/main/default/settings/${CLI_NAME}.settings-meta.xml\"\n if [[ -f \"$SETTINGS_FILE\" ]]; then\n # Modify: update existing field or insert new field\n if grep -q \"\u003c${FIELD}>\" \"$SETTINGS_FILE\"; then\n sed_inplace \"s|\u003c${FIELD}>[^\u003c]*\u003c/${FIELD}>|\u003c${FIELD}>true\u003c/${FIELD}>|g\" \"$SETTINGS_FILE\"\n else\n # Insert before closing tag\n sed_inplace \"/\u003c\\/.*Settings>/i\\\\\n \u003c${FIELD}>true\u003c/${FIELD}>\" \"$SETTINGS_FILE\"\n fi\n\n # Deploy\n local DEPLOY_RESULT\n DEPLOY_RESULT=$(sf project deploy start --source-dir force-app/main/default/settings --target-org \"$ORG_ALIAS\" --json 2>/dev/null || echo '{\"status\":1}')\n\n if echo \"$DEPLOY_RESULT\" | jq -e '.status == 0' >/dev/null 2>&1; then\n sleep $SLEEP_MEDIUM\n # VERIFY\n CURRENT=$(soap_extract_bool \"$(soap_read \"$META_TYPE\" \"$FULL_NAME\")\" \"$FIELD\")\n if [[ \"$CURRENT\" == \"true\" ]]; then\n log_pass \"$DISPLAY_NAME: enabled (CLI deploy)\"\n return 0\n fi\n log_warn \"$DISPLAY_NAME: CLI deploy succeeded but SOAP verification failed\"\n else\n local DEPLOY_ERR\n DEPLOY_ERR=$(safe_jq \"$DEPLOY_RESULT\" '.result.details.componentFailures[0].problem // .message // \"unknown\"' \"unknown\")\n echo \" CLI deploy error: $DEPLOY_ERR\"\n fi\n else\n echo \" CLI retrieve returned no settings file for $CLI_NAME\"\n fi\n\n # --- APPROACH 3: REST Composite API (for settings that support it) ---\n log_try \"$DISPLAY_NAME: trying REST API PATCH...\"\n show_progress \"REST API PATCH for $DISPLAY_NAME\"\n local REST_RESULT\n REST_RESULT=$(curl -s --max-time $CURL_TIMEOUT -X PATCH \"${INSTANCE_URL}/services/data/v${API_VERSION}/tooling/sobjects/${META_TYPE}/${FULL_NAME}\" \\\n -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"${FIELD}\\\": true}\" 2>/dev/null || echo \"\")\n clear_progress\n\n # REST may return empty on success (204) or an error body\n if [[ -z \"$REST_RESULT\" ]] || echo \"$REST_RESULT\" | grep -qi \"\\\"success\\\":true\\|\\\"id\\\"\"; then\n sleep $SLEEP_MEDIUM\n CURRENT=$(soap_extract_bool \"$(soap_read \"$META_TYPE\" \"$FULL_NAME\")\" \"$FIELD\")\n if [[ \"$CURRENT\" == \"true\" ]]; then\n log_pass \"$DISPLAY_NAME: enabled (REST API)\"\n return 0\n fi\n fi\n\n # --- ALL APPROACHES EXHAUSTED ---\n # Final verification attempt after waiting\n sleep $SLEEP_LONG\n CURRENT=$(soap_extract_bool \"$(soap_read \"$META_TYPE\" \"$FULL_NAME\")\" \"$FIELD\")\n if [[ \"$CURRENT\" == \"true\" ]]; then\n log_pass \"$DISPLAY_NAME: enabled (delayed propagation)\"\n return 0\n fi\n\n log_fail \"$DISPLAY_NAME: COULD NOT ENABLE (all 3 approaches failed)\"\n return 1\n}\n\n# ============================================================\n# Phase 2: Prerequisites (with verification gate)\n# ============================================================\nsection \"Phase 2: Prerequisites\"\n\nPREREQ_BLOCKERS=0\n\n# --- 2a: Einstein Generative AI (CRITICAL — everything depends on this) ---\nGENAI_RESULT=0\nenable_setting \"EinsteinGptSettings\" \"EinsteinGpt\" \"enableEinsteinGptPlatform\" \"Einstein Generative AI\" || GENAI_RESULT=$?\n\nif [[ \"$GENAI_RESULT\" -eq 2 ]]; then\n # Metadata type not available (INVALID_TYPE) - org lacks required license/edition\n echo \"\"\n echo \" BLOCKER: Einstein Generative AI metadata type not available in this org.\"\n echo \" This is the foundation for ALL Agentforce features.\"\n echo \"\"\n echo \" Root cause: EinsteinGptSettings metadata type returned INVALID_TYPE\"\n echo \" This means the org is missing:\"\n echo \" - Enterprise Edition (or higher) with Einstein features\"\n echo \" - Einstein Generative AI licenses\"\n echo \" - Platform-level Einstein feature enablement\"\n echo \"\"\n echo \" Resolution:\"\n echo \" 1. Verify org edition: Setup → Company Information\"\n echo \" Required: Enterprise Edition or higher\"\n echo \" 2. Check licenses: Setup → Company Information → User Licenses\"\n echo \" Look for: 'Einstein', 'Sales Cloud Einstein', 'Service Cloud Einstein'\"\n echo \" 3. If licenses are missing:\"\n echo \" → Contact your Salesforce account team to provision Einstein licenses\"\n echo \" → Trial orgs: Request Einstein trial extension\"\n echo \" 4. Re-run this script: ./setup-all.sh $ORG_ALIAS\"\n echo \"\"\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nelif [[ \"$GENAI_RESULT\" -ne 0 ]]; then\n # Other failure (return code 1) - could be ToS, permissions, or other config issue\n echo \"\"\n echo \" BLOCKER: Einstein Generative AI cannot be enabled.\"\n echo \" This is the foundation for ALL Agentforce features.\"\n echo \"\"\n echo \" Common causes:\"\n echo \" - Org edition doesn't include Einstein GenAI features\"\n echo \" - Einstein Terms of Service not accepted (Setup → Einstein Setup)\"\n echo \" - Org-level feature toggle disabled by Salesforce\"\n echo \" - Missing required licenses for Einstein features\"\n echo \"\"\n echo \" Manual resolution:\"\n echo \" 1. Setup → Einstein Setup → Review and accept Terms of Service\"\n echo \" 2. Setup → Einstein Generative AI → Enable\"\n echo \" 3. Check Setup → Company Information → User Licenses for Einstein/Agentforce licenses\"\n echo \"\"\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nfi\n\n# --- 2b: Agentforce Agent (CRITICAL — depends on GenAI) ---\nCOPILOT_RESULT=0\nenable_setting \"EinsteinCopilotSettings\" \"EinsteinCopilot\" \"enableEinsteinGptCopilot\" \"Agentforce Agent\" || COPILOT_RESULT=$?\n\nif [[ \"$COPILOT_RESULT\" -eq 2 ]]; then\n # Metadata type not available (INVALID_TYPE) - org lacks required license/edition\n echo \"\"\n echo \" BLOCKER: Agentforce Agent metadata type not available in this org.\"\n echo \" This indicates the org does NOT have the required Salesforce edition or licenses.\"\n echo \"\"\n echo \" Root cause: EinsteinCopilotSettings metadata type returned INVALID_TYPE\"\n echo \" This means the org is missing:\"\n echo \" - Enterprise Edition (or higher) with Einstein/Agentforce features\"\n echo \" - Einstein Agent licenses\"\n echo \" - Agentforce for Sales add-on licenses\"\n echo \"\"\n echo \" Resolution:\"\n echo \" 1. Verify org edition: Setup → Company Information\"\n echo \" Required: Enterprise Edition or higher\"\n echo \" 2. Check licenses: Setup → Company Information → User Licenses\"\n echo \" Look for: 'Einstein Agent', 'Agentforce for Sales', 'Sales Cloud Einstein'\"\n echo \" 3. If licenses are missing:\"\n echo \" → Contact your Salesforce account team to provision licenses\"\n echo \" → Trial orgs: Request Einstein/Agentforce trial extension\"\n echo \" 4. Re-run this script: ./setup-all.sh $ORG_ALIAS\"\n echo \"\"\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nelif [[ \"$COPILOT_RESULT\" -ne 0 ]]; then\n # Other failure (return code 1) - could be ToS, permissions, or other config issue\n echo \"\"\n echo \" BLOCKER: Agentforce Agent cannot be enabled.\"\n echo \" Requires Einstein Generative AI (step above) to be enabled first.\"\n echo \"\"\n echo \" Common causes:\"\n echo \" - Missing Einstein Agent licenses\"\n echo \" - Org edition doesn't include Einstein/Agentforce features\"\n echo \" - Einstein Terms of Service not accepted\"\n echo \"\"\n # Check if Einstein Agent licenses exist\n EA_LICENSE=$(get_license_count \"Einstein Agent\")\n if [[ \"$EA_LICENSE\" == \"ERROR\" ]]; then\n echo \" License Check: ⚠️ Could not verify license status (API error)\"\n echo \" → Check org connectivity and try again\"\n echo \"\"\n elif [[ \"$EA_LICENSE\" -eq 0 ]]; then\n echo \" License Check: ❌ No Einstein Agent licenses found\"\n echo \" → Contact your Salesforce account team to provision Einstein Agent licenses\"\n echo \" → View licenses: Setup → Company Information → User Licenses\"\n echo \"\"\n else\n echo \" License Check: ✓ Einstein Agent licenses available ($EA_LICENSE)\"\n echo \"\"\n echo \" Most likely cause: Einstein Terms of Service NOT accepted\"\n echo \"\"\n echo \" Required action:\"\n echo \" 1. Navigate to: ${INSTANCE_URL}/lightning/setup/EinsteinGPTSetup/home\"\n echo \" (Note: Assumes Lightning Experience. For Classic, use Setup menu.)\"\n echo \" 2. Review and accept the Einstein Terms of Service\"\n echo \" 3. Then enable Agentforce Agent at: ${INSTANCE_URL}/lightning/setup/EinsteinCopilot/home\"\n echo \" 4. Re-run this script: ./setup-all.sh $ORG_ALIAS\"\n echo \"\"\n fi\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nfi\n\n# --- 2b2: Agentforce Studio / Agent Platform (CRITICAL — gates Deal Agent) ---\n# AgentPlatformSettings.enableAgentPlatform (OrgPreference AgentPlatformEnabled)\n# must be ON before SalesDealAgentSettings can be enabled. Core enforces this:\n# DealAgentEnabledOrgPreference.validateAgentforcePlatformAndLog() throws if the\n# Agentforce Platform preference is not enabled, so a missing step here surfaces\n# later as an opaque Deal Agent activation failure. Ordering: GenAI → Copilot →\n# AgentPlatform → SalesDealAgent.\nAGENT_PLATFORM_RESULT=0\nenable_setting \"AgentPlatformSettings\" \"AgentPlatform\" \"enableAgentPlatform\" \"Agentforce Studio (Agent Platform)\" || AGENT_PLATFORM_RESULT=$?\n\nif [[ \"$AGENT_PLATFORM_RESULT\" -eq 2 ]]; then\n # Metadata type not available (INVALID_TYPE) - org lacks required license/edition\n echo \"\"\n echo \" BLOCKER: Agentforce Studio (Agent Platform) metadata type not available in this org.\"\n echo \" AgentPlatformSettings returned INVALID_TYPE — the org is missing the\"\n echo \" Enterprise Edition (or higher) or Agentforce licenses required for Agent Platform.\"\n echo \"\"\n echo \" Resolution:\"\n echo \" 1. Verify org edition: Setup → Company Information (Enterprise Edition or higher)\"\n echo \" 2. Check licenses: Setup → Company Information → User Licenses\"\n echo \" Look for: 'Agentforce', 'Einstein Agent', 'Agentforce for Sales'\"\n echo \" 3. Re-run this script: ./setup-all.sh $ORG_ALIAS\"\n echo \"\"\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nelif [[ \"$AGENT_PLATFORM_RESULT\" -ne 0 ]]; then\n echo \"\"\n echo \" BLOCKER: Agentforce Studio (Agent Platform) cannot be enabled.\"\n echo \" This preference (AgentPlatformEnabled) gates the Sales Deal Agent —\"\n echo \" Deal Agent activation fails downstream if it is not enabled first.\"\n echo \"\"\n echo \" Common causes:\"\n echo \" - Einstein Generative AI / Agentforce Agent not enabled (steps above)\"\n echo \" - Missing Agentforce licenses\"\n echo \" - Einstein Terms of Service not accepted\"\n echo \"\"\n echo \" Manual resolution:\"\n echo \" 1. Enable Einstein GenAI and Agentforce Agent (steps above)\"\n echo \" 2. Navigate to: ${INSTANCE_URL}/lightning/setup/EinsteinAgentBuilder/home\"\n echo \" (Agentforce Studio) and confirm the platform is enabled\"\n echo \" 3. Re-run this script: ./setup-all.sh $ORG_ALIAS\"\n echo \"\"\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nfi\n\n# --- 2c: Enhanced Notes (CRITICAL — PM data source) ---\nif ! enable_setting \"EnhancedNotesSettings\" \"EnhancedNotes\" \"enableEnhancedNotes\" \"Enhanced Notes\"; then\n echo \"\"\n echo \" BLOCKER: Enhanced Notes cannot be enabled.\"\n echo \" Pipeline Management uses ContentNote as a primary data source.\"\n echo \"\"\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nfi\n\n# --- 2d: Opportunity Team (CRITICAL — flow uses OpportunityTeamMember) ---\nif ! enable_setting \"OpportunitySettings\" \"Opportunity\" \"enableOpportunityTeam\" \"Opportunity Team\"; then\n echo \"\"\n echo \" BLOCKER: Opportunity Team cannot be enabled.\"\n echo \" Pipeline Management flow requires OpportunityTeamMember object access.\"\n echo \"\"\n PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))\nfi\n\n# --- 2e: Pipeline Inspection (required for UI review of suggestions) ---\nenable_setting \"OpportunitySettings\" \"Opportunity\" \"enablePipelineInspection\" \"Pipeline Inspection\" || \\\n log_warn \"Pipeline Inspection not enabled — suggestions generate but users have no UI to review/accept them\"\n\n# --- 2f: Enhanced Email (optional) ---\nenable_setting \"EmailAdministrationSettings\" \"EmailAdministration\" \"enableEnhancedEmailEnabled\" \"Enhanced Email\" || \\\n log_warn \"Enhanced Email not enabled (email body indexing unavailable — agent uses notes instead)\"\n\n# --- VERIFICATION GATE: All critical prerequisites must pass ---\necho \"\"\necho \" --- Prerequisite Verification Gate ---\"\n\nGATE_PASS=true\n\n# Classify each gate setting so a real \"disabled\" is distinguished from an\n# auth/SOAP fault or a network/service error (W-23215519). gate_check logs the\n# right diagnostic and fails the gate for any non-\"true\" outcome.\n# Args: $1 = display label, $2 = classification string from soap_classify\ngate_check() {\n local label=\"$1\" status=\"$2\"\n case \"$status\" in\n true)\n return 0\n ;;\n false)\n log_fail \"GATE: $label NOT confirmed (setting is disabled)\"\n ;;\n AUTH_ERROR:*)\n log_fail \"GATE: $label cannot be verified — auth/SOAP fault (${status#AUTH_ERROR:})\"\n echo \" Re-authenticate: sf org login web --alias $ORG_ALIAS\"\n ;;\n TYPE_UNAVAILABLE:*)\n log_fail \"GATE: $label cannot be verified — metadata type unavailable (${status#TYPE_UNAVAILABLE:})\"\n ;;\n NETWORK_ERROR)\n log_fail \"GATE: $label cannot be verified — network/service error (check connectivity)\"\n ;;\n *)\n log_fail \"GATE: $label NOT confirmed (unexpected status: $status)\"\n ;;\n esac\n GATE_PASS=false\n return 1\n}\n\nGENAI_STATUS=$(soap_classify \"$(soap_read \"EinsteinGptSettings\" \"EinsteinGpt\")\" \"enableEinsteinGptPlatform\")\nCOPILOT_STATUS=$(soap_classify \"$(soap_read \"EinsteinCopilotSettings\" \"EinsteinCopilot\")\" \"enableEinsteinGptCopilot\")\nAGENT_PLATFORM_STATUS=$(soap_classify \"$(soap_read \"AgentPlatformSettings\" \"AgentPlatform\")\" \"enableAgentPlatform\")\nNOTES_STATUS=$(soap_classify \"$(soap_read \"EnhancedNotesSettings\" \"EnhancedNotes\")\" \"enableEnhancedNotes\")\nOPP_TEAM_STATUS=$(soap_classify \"$(soap_read \"OpportunitySettings\" \"Opportunity\")\" \"enableOpportunityTeam\")\n\n# Keep boolean mirrors for the existing failure-summary conditionals below.\nVERIFY_GENAI=$([[ \"$GENAI_STATUS\" == \"true\" ]] && echo \"true\" || echo \"false\")\nVERIFY_COPILOT=$([[ \"$COPILOT_STATUS\" == \"true\" ]] && echo \"true\" || echo \"false\")\nVERIFY_AGENT_PLATFORM=$([[ \"$AGENT_PLATFORM_STATUS\" == \"true\" ]] && echo \"true\" || echo \"false\")\nVERIFY_NOTES=$([[ \"$NOTES_STATUS\" == \"true\" ]] && echo \"true\" || echo \"false\")\nVERIFY_OPP_TEAM=$([[ \"$OPP_TEAM_STATUS\" == \"true\" ]] && echo \"true\" || echo \"false\")\n\ngate_check \"Einstein Generative AI\" \"$GENAI_STATUS\" || true\ngate_check \"Agentforce Agent\" \"$COPILOT_STATUS\" || true\ngate_check \"Agentforce Studio (Agent Platform)\" \"$AGENT_PLATFORM_STATUS\" || true\ngate_check \"Enhanced Notes\" \"$NOTES_STATUS\" || true\ngate_check \"Opportunity Team\" \"$OPP_TEAM_STATUS\" || true\n\nif [[ \"$GATE_PASS\" != \"true\" ]]; then\n echo \"\"\n log_fail \"PREREQUISITE GATE FAILED — cannot proceed to Pipeline Management enablement.\"\n echo \"\"\n echo \" The following critical settings must be enabled before PM can be activated:\"\n [[ \"$VERIFY_GENAI\" != \"true\" ]] && echo \" ❌ EinsteinGptSettings.enableEinsteinGptPlatform\"\n [[ \"$VERIFY_COPILOT\" != \"true\" ]] && echo \" ❌ EinsteinCopilotSettings.enableEinsteinGptCopilot\"\n [[ \"$VERIFY_AGENT_PLATFORM\" != \"true\" ]] && echo \" ❌ AgentPlatformSettings.enableAgentPlatform\"\n [[ \"$VERIFY_NOTES\" != \"true\" ]] && echo \" ❌ EnhancedNotesSettings.enableEnhancedNotes\"\n [[ \"$VERIFY_OPP_TEAM\" != \"true\" ]] && echo \" ❌ OpportunitySettings.enableOpportunityTeam\"\n echo \"\"\n echo \" Manual resolution:\"\n echo \" 1. Setup → Einstein → Einstein Generative AI → Enable\"\n echo \" 2. Setup → Einstein → Copilot → Enable\"\n echo \" 3. Setup → Agentforce Studio → Enable (Agent Platform)\"\n echo \" 4. Setup → Notes → Enable Enhanced Notes\"\n echo \" 5. Setup → Opportunity Settings → Enable Opportunity Team\"\n echo \" 6. Re-run: bash setup-all.sh $ORG_ALIAS\"\n exit 1\nfi\n\nlog_pass \"GATE: All critical prerequisites verified ✓\"\n\n# ============================================================\n# Phase 2.5: License and Metadata Type Validation\n# ============================================================\necho \"\"\necho \" --- Pipeline Management Compatibility Gate ---\"\n\n# Expected license names: \"Agentforce for Sales\", \"Sales Cloud Einstein\", \"Agentforce 1\"\n# If Salesforce introduces new variants, this query may need updating\nSALES_LICENSE_OUTPUT=$(sf data query --query \"SELECT COUNT(Id) FROM UserLicense WHERE (Name LIKE '%Agentforce for Sales%' OR Name LIKE '%Sales Cloud Einstein%' OR Name LIKE '%Agentforce 1%') AND TotalLicenses > 0\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\nSALES_LICENSE_COUNT=$(safe_jq_int \"$SALES_LICENSE_OUTPUT\" '.result.records[0].expr0 // 0')\n\nEINSTEIN_AGENT_COUNT=$(get_license_count \"Einstein Agent\")\n# Handle API errors gracefully\nif [[ \"$EINSTEIN_AGENT_COUNT\" == \"ERROR\" ]]; then\n EINSTEIN_AGENT_COUNT=\"unknown\"\nfi\n\n# Check if SalesDealAgentSettings metadata type is available\nMETADATA_TYPE_CHECK=$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\" 2>/dev/null || echo \"\")\n\n# Backstop only — the Step 0 gate (Phase 0.5) is the primary check and normally\n# stops an unlicensed org before any question. This catches a post-enablement\n# regression (e.g. the type going away mid-run). Shares print_license_blocker().\nif echo \"$METADATA_TYPE_CHECK\" | grep -q \"INVALID_TYPE\"; then\n log_fail \"GATE: SalesDealAgentSettings metadata type NOT available\"\n print_license_blocker \"$EINSTEIN_AGENT_COUNT\" \"$SALES_LICENSE_COUNT\"\n exit 1\nfi\n\nif [[ \"$SALES_LICENSE_COUNT\" -eq 0 ]]; then\n log_warn \"GATE: No Agentforce for Sales licenses found (but metadata type exists)\"\n echo \"\"\n echo \" Warning: The org has the metadata type but no explicit Sales licenses.\"\n echo \" Pipeline Management may work via base Einstein Agent licenses ($EINSTEIN_AGENT_COUNT).\"\n echo \" Proceeding with enablement attempt...\"\n echo \"\"\nelse\n log_pass \"GATE: Agentforce for Sales licenses found ($SALES_LICENSE_COUNT) ✓\"\nfi\n\n# ============================================================\n# Phase 3: Enable Pipeline Management (with multi-approach + verification)\n# ============================================================\nsection \"Phase 3: Pipeline Management\"\n\nPM_ENABLED=false\n\n# Check current state\nPM_CURRENT=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgent\")\nif [[ \"$PM_CURRENT\" == \"true\" ]]; then\n log_pass \"Pipeline Management: already enabled\"\n PM_ENABLED=true\nfi\n\nif [[ \"$PM_ENABLED\" != \"true\" ]]; then\n # --- APPROACH 1: SOAP updateMetadata ---\n log_try \"Enabling via SOAP Metadata API v${API_VERSION}...\"\n RESULT=$(soap_update \"\u003cmet:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\u003cmet:fullName>SalesDealAgent\u003c/met:fullName>\u003cmet:enableDealAgent>true\u003c/met:enableDealAgent>\u003cmet:enableDealAgentAutoApproveAllTasks>false\u003c/met:enableDealAgentAutoApproveAllTasks>\u003c/met:metadata>\")\n\n if echo \"$RESULT\" | grep -q \"\u003csuccess>true\u003c/success>\"; then\n sleep $SLEEP_MEDIUM\n PM_CHECK=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgent\")\n if [[ \"$PM_CHECK\" == \"true\" ]]; then\n log_pass \"Pipeline Management: enabled (SOAP)\"\n PM_ENABLED=true\n fi\n fi\n\n if [[ \"$PM_ENABLED\" != \"true\" ]]; then\n SOAP_ERR=$(echo \"$RESULT\" | grep -o '\u003cmessage>[^\u003c]*\u003c/message>' | sed 's/\u003c[^>]*>//g' | head -1 || echo \"\")\n [[ -n \"$SOAP_ERR\" ]] && echo \" SOAP error: $(redact_token \"$SOAP_ERR\")\"\n fi\nfi\n\nif [[ \"$PM_ENABLED\" != \"true\" ]]; then\n # --- APPROACH 2: CLI Metadata deploy (SalesDealAgent settings XML) ---\n log_try \"Enabling via CLI Metadata deploy...\"\n ensure_sfdx_project\n mkdir -p force-app/main/default/settings\n\n # Create settings XML directly (CLI retrieve fails for this type)\n cat > force-app/main/default/settings/SalesDealAgent.settings-meta.xml \u003c\u003c 'XMLEOF'\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cSalesDealAgentSettings xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cenableDealAgent>true\u003c/enableDealAgent>\n \u003cenableDealAgentAutoApproveAllTasks>false\u003c/enableDealAgentAutoApproveAllTasks>\n\u003c/SalesDealAgentSettings>\nXMLEOF\n\n DEPLOY_RESULT=$(sf project deploy start --source-dir force-app/main/default/settings/SalesDealAgent.settings-meta.xml --target-org \"$ORG_ALIAS\" --json 2>/dev/null || echo '{\"status\":1}')\n if echo \"$DEPLOY_RESULT\" | jq -e '.status == 0' >/dev/null 2>&1; then\n sleep $SLEEP_LONG\n PM_CHECK=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgent\")\n if [[ \"$PM_CHECK\" == \"true\" ]]; then\n log_pass \"Pipeline Management: enabled (CLI deploy)\"\n PM_ENABLED=true\n fi\n else\n DEPLOY_ERR=$(safe_jq \"$DEPLOY_RESULT\" '.result.details.componentFailures[0].problem // .message // \"unknown\"' \"unknown\")\n echo \" CLI deploy error: $DEPLOY_ERR\"\n fi\nfi\n\nif [[ \"$PM_ENABLED\" != \"true\" ]]; then\n # --- APPROACH 3: Recognize genuine mid-provisioning WITHOUT asserting enablement ---\n # #9: PSG existence is NOT a setup signal — SalesManagementUserPsg /\n # SalesManagementAgentUserPsg ship with the Agentforce-for-Sales license and exist even\n # in a never-configured org, so \"PSGs exist\" says nothing about whether PM was set up.\n # The authoritative signal is SalesDealAgentSettings.enableDealAgent via SOAP readMetadata\n # (checked above). Here we re-read it authoritatively; only if it reads true do we mark\n # enabled. If it's still false, we check the REAL \"setup has run\" signals — an agent user\n # holding SalesManagementAgentUserPsg, a cloned scheduled flow, or a PM BotDefinition. If\n # any is present, a bundle-publish/template-provisioned org may be mid-flight, so we mark\n # \"unconfirmed\" and continue, letting the downstream verification gate confirm. Otherwise\n # we fall through to Approach 4.\n log_try \"Re-reading enableDealAgent authoritatively (SOAP) and checking real setup signals...\"\n PM_RECHECK=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgent\")\n\n if [[ \"$PM_RECHECK\" == \"true\" ]]; then\n log_pass \"Pipeline Management: enabled (confirmed via SOAP enableDealAgent)\"\n PM_ENABLED=true\n else\n AGENT_PSG_HELD=$(pm_agent_user_holds_psg \"$ORG_ALIAS\")\n PM_FLOW_EXISTS=$(pm_flow_clone_exists \"$ORG_ALIAS\")\n PM_BOT_EXISTS=$(pm_bot_count \"$ORG_ALIAS\")\n if [[ \"$AGENT_PSG_HELD\" -ge 1 || \"$PM_FLOW_EXISTS\" -ge 1 || \"$PM_BOT_EXISTS\" -ge 1 ]]; then\n log_warn \"enableDealAgent not confirmed true, but real setup signals exist (agentPSG=$AGENT_PSG_HELD flow=$PM_FLOW_EXISTS bot=$PM_BOT_EXISTS) — provisioning likely in progress.\"\n echo \" Continuing with setup; enablement is UNCONFIRMED and will be re-verified in Phase 8.\"\n PM_ENABLED=\"unconfirmed\"\n fi\n fi\nfi\n\nif [[ \"$PM_ENABLED\" != \"true\" && \"$PM_ENABLED\" != \"unconfirmed\" ]]; then\n # --- APPROACH 4: OrgPreference via Tooling API ---\n log_try \"Enabling via OrgPreference Tooling API...\"\n show_progress \"Tooling API OrgPreference update\"\n PREF_RESULT=$(curl -s --max-time $CURL_TIMEOUT -X PATCH \"${INSTANCE_URL}/services/data/v${API_VERSION}/tooling/sobjects/OrganizationSettingsDetail/DealAgentEnabled\" \\\n -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"value\": true}' 2>/dev/null || echo \"\")\n clear_progress\n\n sleep $SLEEP_LONG\n PM_CHECK=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgent\")\n if [[ \"$PM_CHECK\" == \"true\" ]]; then\n log_pass \"Pipeline Management: enabled (Tooling API OrgPreference)\"\n PM_ENABLED=true\n else\n # Do NOT treat PSG presence as a provisioning signal (the PM PSGs ship with the\n # license). Re-check the same real signals as Approach 3: an agent user holding\n # SalesManagementAgentUserPsg, a cloned scheduled flow, or a PM BotDefinition.\n AGENT_PSG_HELD=$(pm_agent_user_holds_psg \"$ORG_ALIAS\")\n PM_FLOW_EXISTS=$(pm_flow_clone_exists \"$ORG_ALIAS\")\n PM_BOT_EXISTS=$(pm_bot_count \"$ORG_ALIAS\")\n if [[ \"$AGENT_PSG_HELD\" -ge 1 || \"$PM_FLOW_EXISTS\" -ge 1 || \"$PM_BOT_EXISTS\" -ge 1 ]]; then\n log_warn \"Tooling API approach uncertain, but real setup signals appeared (agentPSG=$AGENT_PSG_HELD flow=$PM_FLOW_EXISTS bot=$PM_BOT_EXISTS) — PM provisioning in progress\"\n PM_ENABLED=\"unconfirmed\"\n fi\n fi\nfi\n\n# --- PM VERIFICATION GATE ---\nif [[ \"$PM_ENABLED\" != \"true\" && \"$PM_ENABLED\" != \"unconfirmed\" ]]; then\n echo \"\"\n log_fail \"PIPELINE MANAGEMENT GATE FAILED — all 4 approaches exhausted.\"\n echo \"\"\n echo \" Attempted:\"\n echo \" 1. SOAP Metadata API v${API_VERSION} (updateMetadata SalesDealAgentSettings)\"\n echo \" 2. CLI Metadata deploy (settings XML)\"\n echo \" 3. Authoritative SOAP re-read + provisioning-signal detection (agent-user / flow / bot presence)\"\n echo \" 4. Tooling API OrgPreference (DealAgentEnabled)\"\n echo \"\"\n echo \" This org may require manual enablement:\"\n echo \" Setup → Sales → Pipeline Management → Enable toggle\"\n echo \" URL: ${INSTANCE_URL}/lightning/setup/PipelineManagement/home\"\n echo \"\"\n echo \" After enabling, re-run: bash setup-all.sh $ORG_ALIAS\"\n exit 1\nfi\n\n# ============================================================\n# Phase 3.5: Enable Autonomous Field Updates\n# ============================================================\nsection \"Phase 3.5: Autonomous Field Updates\"\n\n# After Pipeline Management is enabled, set enableDealAgentAutoApproveAllTasks=true\n# to allow the agent to update opportunity fields without user approval.\n#\n# CONSENT GATE (#2): autonomous updates are opt-in only. Without --autonomous (or an\n# interactive \"yes\" in Phase 0), the agent stays in suggestion-only mode and this phase\n# makes NO change. This prevents the agent from silently auto-applying field writes.\n\nAUTO_APPROVE_CURRENT=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgentAutoApproveAllTasks\")\n\nif [[ \"$ENABLE_AUTONOMOUS\" != \"true\" ]]; then\n if [[ \"$AUTO_APPROVE_CURRENT\" == \"true\" ]]; then\n log_warn \"Autonomous updates are currently ON in this org, but --autonomous was not passed.\"\n echo \" Leaving the existing org setting unchanged (not disabling).\"\n echo \" To turn autonomous updates OFF, use the Pipeline Management setup UI.\"\n else\n log_pass \"Autonomous updates: OFF (suggestions only) — not enabled without --autonomous\"\n fi\nelif [[ \"$AUTO_APPROVE_CURRENT\" == \"true\" ]]; then\n log_pass \"Autonomous updates: already enabled\"\nelse\n log_info \"Enabling autonomous field updates (enableDealAgentAutoApproveAllTasks=true)...\"\n\n # SOAP Metadata API approach\n RESULT=$(soap_update \"\u003cmet:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\u003cmet:fullName>SalesDealAgent\u003c/met:fullName>\u003cmet:enableDealAgent>true\u003c/met:enableDealAgent>\u003cmet:enableDealAgentAutoApproveAllTasks>true\u003c/met:enableDealAgentAutoApproveAllTasks>\u003c/met:metadata>\")\n\n if echo \"$RESULT\" | grep -qi \"success.*true\"; then\n AUTO_CHECK=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgentAutoApproveAllTasks\")\n if [[ \"$AUTO_CHECK\" == \"true\" ]]; then\n log_pass \"Autonomous updates: enabled (SOAP API)\"\n else\n log_warn \"SOAP update reported success, but verification failed\"\n fi\n else\n log_warn \"SOAP API update failed, trying CLI metadata deploy...\"\n\n # CLI metadata deploy approach (fallback)\n TMP_DIR=$(mktemp -d)\n mkdir -p \"$TMP_DIR/settings\"\n\n cat > \"$TMP_DIR/settings/SalesDealAgent.settings-meta.xml\" \u003c\u003cEOF\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cSalesDealAgentSettings xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cenableDealAgent>true\u003c/enableDealAgent>\n \u003cenableDealAgentAutoApproveAllTasks>true\u003c/enableDealAgentAutoApproveAllTasks>\n\u003c/SalesDealAgentSettings>\nEOF\n\n CLI_RESULT=$(sf project deploy start --metadata-dir \"$TMP_DIR/settings\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null || echo '{\"status\":1}')\n rm -rf \"$TMP_DIR\"\n\n CLI_SUCCESS=$(echo \"$CLI_RESULT\" | jq -r '.status // 1' 2>/dev/null | head -1 || echo \"1\")\n if [[ \"$CLI_SUCCESS\" == \"0\" ]]; then\n AUTO_CHECK=$(soap_extract_bool \"$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\")\" \"enableDealAgentAutoApproveAllTasks\")\n if [[ \"$AUTO_CHECK\" == \"true\" ]]; then\n log_pass \"Autonomous updates: enabled (CLI deploy)\"\n else\n log_warn \"CLI deploy succeeded but verification failed\"\n fi\n else\n log_warn \"CLI deploy failed. Autonomous updates may require manual enablement:\"\n echo \" Setup → Sales → Pipeline Management → Settings\"\n echo \" Enable: 'Update Fields Autonomously'\"\n echo \" URL: ${INSTANCE_URL}/lightning/setup/PipelineManagement/home\"\n fi\n fi\nfi\n\n# ============================================================\n# Phase 4: Wait for Provisioning (with polling loop)\n# ============================================================\nsection \"Phase 4: Provisioning (agent user + PSG assignment)\"\n\nMAX_WAIT_SECONDS=90\nPOLL_INTERVAL=5\n\n# Provisioning is complete when the agent user exists AND holds\n# SalesManagementAgentUserPsg. The two PM PSGs themselves ship with the\n# Agentforce-for-Sales license and exist even in a never-configured org, so\n# \"PSG count >= 2\" is NOT a completion signal — the agent-user assignment is.\ncheck_agent_provisioned() { # sets AGENT_USER (Username) + AGENT_PSG_HELD; returns 0 when complete\n local rec agent_id\n rec=$(sf data query --query \"SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0] // empty' 2>/dev/null || echo \"\")\n AGENT_USER=$(echo \"$rec\" | jq -r '.Username // empty' 2>/dev/null || echo \"\")\n AGENT_PSG_HELD=0\n if [[ -n \"$AGENT_USER\" ]]; then\n AGENT_PSG_HELD=$(pm_agent_user_holds_psg \"$ORG_ALIAS\")\n fi\n [[ -n \"$AGENT_USER\" && \"${AGENT_PSG_HELD:-0}\" -ge 1 ]]\n}\n\nif check_agent_provisioned; then\n log_pass \"Agent user: $AGENT_USER\"\n log_pass \"Agent user holds SalesManagementAgentUserPsg\"\nelse\n echo -n \" Waiting for auto-provisioning (up to ${MAX_WAIT_SECONDS}s)\"\n WAITED=0\n while [[ $WAITED -lt $MAX_WAIT_SECONDS ]]; do\n if check_agent_provisioned; then\n echo \"\"\n log_pass \"Agent user: $AGENT_USER\"\n log_pass \"Agent user holds SalesManagementAgentUserPsg\"\n break\n fi\n\n echo -n \".\"\n sleep \"$POLL_INTERVAL\"\n WAITED=$((WAITED + POLL_INTERVAL))\n done\n\n if [[ $WAITED -ge $MAX_WAIT_SECONDS ]]; then\n echo \"\"\n # Report what we have\n if [[ -n \"$AGENT_USER\" ]]; then\n log_pass \"Agent user: $AGENT_USER\"\n [[ \"${AGENT_PSG_HELD:-0}\" -ge 1 ]] && log_pass \"Agent user holds SalesManagementAgentUserPsg\" || log_warn \"Agent user does not yet hold SalesManagementAgentUserPsg\"\n else\n log_warn \"Agent user: not yet provisioned\"\n fi\n echo \"\"\n echo \" Provisioning may still be in progress. Continuing...\"\n echo \" (Re-run verify-all.sh after 5 minutes to confirm.)\"\n fi\nfi\n\n# ------------------------------------------------------------\n# Phase 4a: Report recalculation Status for BOTH PSGs (no waiting)\n# ------------------------------------------------------------\n# #10: report the recalculation Status for BOTH SalesManagementUserPsg AND the\n# autonomous agent user's SalesManagementAgentUserPsg (previously only the former\n# was surfaced).\n#\n# What triggers each PSG's recalc:\n# - SalesManagementUserPsg: define-agent-access.sh inserts a\n# PermissionSetGroupComponent (the custom Agent Access permset), which fires\n# its recalc. That is a CUSTOM addition this skill owns.\n# - SalesManagementAgentUserPsg: this is a MANAGED PSG owned by the Deal Agent\n# package. Its components (and thus its recalc) are established by provisioning\n# (bundle publish / enablement) — we deliberately do NOT insert a component\n# into it, because writing to a managed PSG's membership is not ours to do and\n# PermissionSetGroup.Status is not client-writable. So for the agent PSG we\n# only REPORT Status; a lingering non-'Updated' here points to a provisioning\n# issue, not something setup can nudge.\n#\n# Both recalcs converge asynchronously — we deliberately do NOT block on either\n# (completion only matters when a user views suggestions in the UI or the\n# scheduled flow runs). We read Status once for reporting; a 'Failed' Status is\n# tracked as an issue.\nfor PSG_NAME in SalesManagementUserPsg SalesManagementAgentUserPsg; do\n PSG_STATE=$(psg_status \"$ORG_ALIAS\" \"$PSG_NAME\")\n case \"$PSG_STATE\" in\n Updated) log_pass \"PSG '$PSG_NAME' Status: Updated\" ;;\n not-found) log_info \"PSG '$PSG_NAME' not present yet (provisioning may still be in progress)\" ;;\n Failed) track_issue \"🟡 WARNING\" \"PSG '$PSG_NAME' recalculation reported Failed\" \"Permissions from this PSG may be incomplete. Investigate the PSG in Setup and re-run verify-all.sh.\" ;;\n *) log_info \"PSG '$PSG_NAME' Status: $PSG_STATE — recalculation is async and will complete on its own.\" ;;\n esac\ndone\n\n# ============================================================\n# Phase 4b: Agent Verification\n# ============================================================\n# CRITICAL: A Pipeline Management BotDefinition is REQUIRED for a functional\n# Pipeline Management agent. Users need an interactive Sales Agent they can\n# chat with.\n#\n# Detection matches the scheduled flow's logic: BotDefinition.AgentTemplate\n# IN ('SalesMgmt__NGASalesAgent','SalesMgmt__SalesAgent'). We do NOT match\n# by hardcoded DeveloperName — different orgs publish under different local\n# names (SalesAgent, Sale_Agent, etc.).\n#\n# This phase verifies:\n# 1. A PM BotDefinition exists (created via authoring bundle)\n# 2. Agent user + PSG provisioned\n#\n# If BotDefinition is missing, it MUST be created. The script exits 1 on failure.\n# ============================================================\n\nsection \"Phase 4b: Agent Architecture Verification\"\n\n# Check 1: Does a PM BotDefinition exist (matched by AgentTemplate)?\nAGENT_EXISTS=$(pm_bot_count \"$ORG_ALIAS\")\n\nif [[ \"$AGENT_EXISTS\" -ge 1 ]]; then\n PM_BOT_NAME=$(pm_bot_developer_name \"$ORG_ALIAS\")\n log_pass \"BotDefinition:${PM_BOT_NAME:-\u003cunknown>} exists (matched by AgentTemplate)\"\n\n # Existence is not enough: auto-provisioning leaves the BotVersion Inactive.\n # An inactive agent means the scheduled flow never adds the agent user to\n # opportunity teams (no suggestions) and Agent Access (Phase 4c) can't be\n # defined. Verify an active version exists and activate the latest if not.\n BOT_ACTIVE_COUNT=$(pm_bot_active_version_count \"$ORG_ALIAS\")\n if [[ \"$BOT_ACTIVE_COUNT\" -ge 1 ]]; then\n log_pass \"BotDefinition:${PM_BOT_NAME:-\u003cunknown>} has an active version\"\n else\n log_warn \"BotDefinition:${PM_BOT_NAME:-\u003cunknown>} has no active version — activating (auto-provisioned bots land Inactive)...\"\n if activate_pm_bot_if_inactive \"$ORG_ALIAS\"; then\n log_pass \"BotDefinition:${PM_BOT_NAME:-\u003cunknown>} activated\"\n else\n track_issue \"🔴 BLOCKER\" \"BotDefinition exists but has no active version and activation failed\" \"The scheduled flow will not generate suggestions and Agent Access cannot be defined. Activate the Sales Agent (Setup → Agents) or re-run: bash create-agent.sh $ORG_ALIAS\"\n fi\n fi\nelse\n # BotDefinition missing - must be created\n # Re-check agent user (may have been set in Phase 4)\n AGENT_USER_RECHECK=$(sf data query --query \"SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo \"\")\n\n if [[ -n \"$AGENT_USER_RECHECK\" ]]; then\n # Agent user exists but BotDefinition missing - create it via bundle publish\n log_warn \"No PM BotDefinition (AgentTemplate match) found - attempting to create...\"\n log_pass \" Agent user: $AGENT_USER_RECHECK\"\n echo \" The agent user + PSGs are sufficient for suggestion generation.\"\n echo \" The schedule-triggered flow runs as the agent user directly.\"\n\n # OPTIONAL: Deploy standalone agent for richer agent experience\n log_info \"Deploying standalone Sales Management Agent for enhanced capabilities...\"\n\n # Use shared library function (returns on failure so setup-all can track issues)\n if ! publish_and_activate_agent \"$ORG_ALIAS\" \"return\" \"structured\"; then\n track_issue \"🔴 BLOCKER\" \"Agent bundle publish/activate failed\" \"A Pipeline Management BotDefinition (AgentTemplate match) is REQUIRED\"\n echo \" SETUP FAILED: Cannot proceed without an active Sales Agent.\"\n exit 1\n fi\n else\n # Neither BotDefinition nor agent user — genuinely failed provisioning\n log_warn \"No agent user or BotDefinition found after provisioning wait\"\n track_issue \"🔴 ERROR\" \"Agent user not provisioned after enablement\" \"Attempting SOAP toggle to re-provision agent user + PSGs, then publish authoring bundle for BotDefinition\"\n echo \"\"\n echo \" Pipeline Management provisioning appears incomplete.\"\n echo \" This is unusual — the agent user is normally created on enablement.\"\n echo \"\"\n\n # Only attempt fallback deployment if we have NO agent user at all\n # --- APPROACH 1: SOAP toggle — re-provisions the agent user and PSGs only.\n # It does NOT create BotDefinition:SalesAgent; that comes from the\n # authoring-bundle publish in APPROACH 2 below.\n log_try \"Attempting SOAP toggle to re-provision agent user + PSGs (does NOT create BotDefinition)...\"\n # Disable\n soap_update \"\u003cmet:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\u003cmet:fullName>SalesDealAgent\u003c/met:fullName>\u003cmet:enableDealAgent>false\u003c/met:enableDealAgent>\u003c/met:metadata>\" >/dev/null\n sleep $SLEEP_TOGGLE_OFF\n # Re-enable\n soap_update \"\u003cmet:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\u003cmet:fullName>SalesDealAgent\u003c/met:fullName>\u003cmet:enableDealAgent>true\u003c/met:enableDealAgent>\u003cmet:enableDealAgentAutoApproveAllTasks>false\u003c/met:enableDealAgentAutoApproveAllTasks>\u003c/met:metadata>\" >/dev/null\n sleep $SLEEP_TOGGLE_ON\n\n # Verify: check for agent user (primary indicator that the toggle took effect)\n AGENT_USER_RETRY=$(sf data query --query \"SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo \"\")\n if [[ -n \"$AGENT_USER_RETRY\" ]]; then\n log_pass \"Agent user + PSGs re-provisioned via SOAP toggle: $AGENT_USER_RETRY\"\n else\n log_warn \"SOAP toggle did not re-provision the agent user\"\n fi\n\n # --- APPROACH 2: Publish the authoring bundle to create BotDefinition:SalesAgent.\n # Always attempt this: the toggle above never creates the BotDefinition,\n # so it must come from the bundle publish regardless of whether the\n # toggle re-provisioned the agent user.\n log_try \"Publishing authoring bundle to create BotDefinition:SalesAgent...\"\n if publish_and_activate_agent \"$ORG_ALIAS\" \"return\" \"structured\"; then\n log_pass \"BotDefinition:SalesAgent published and activated via authoring bundle\"\n else\n log_warn \"Authoring bundle publish/activate failed\"\n fi\n\n # Final check\n FINAL_AGENT_USER=$(sf data query --query \"SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo \"\")\n if [[ -z \"$FINAL_AGENT_USER\" ]]; then\n log_warn \"Agent provisioning failed via all approaches\"\n echo \" BLOCKER: No agent user found. Suggestions cannot generate without it.\"\n echo \"\"\n echo \" Manual resolution:\"\n echo \" 1. Setup → Sales → Pipeline Management → disable and re-enable\"\n echo \" 2. Wait 5 minutes for provisioning\"\n echo \" 3. Re-run: bash verify-all.sh $ORG_ALIAS\"\n # Recovery exhausted: upgrade the provisional ERROR to a terminal BLOCKER so the\n # final gate reports SETUP INCOMPLETE / exit 1 instead of a false SETUP COMPLETE.\n track_issue \"🔴 BLOCKER\" \"Agent user not provisioned after all recovery attempts\" \"Manually disable/re-enable Pipeline Management (Setup → Sales), wait 5 min, then re-run: bash setup-all.sh $ORG_ALIAS\"\n fi\n fi\nfi\n\n# ============================================================\n# Phase 4c: Agent Access (W-23242378)\n# Once a PM BotDefinition exists, add it to \"Agent Access\" on a custom permset\n# linked into SalesManagementUserPsg so human users can launch the agent.\n# define-agent-access.sh is idempotent and exits gracefully if no active bot.\n# ============================================================\nsection \"Phase 4c: Agent Access\"\n\nif [[ -f \"$SCRIPT_DIR/define-agent-access.sh\" ]]; then\n bash \"$SCRIPT_DIR/define-agent-access.sh\" \"$ORG_ALIAS\" \\\n || track_issue \"🟡 WARNING\" \"Agent Access definition reported an issue\" \"Users holding SalesManagementUserPsg may not be able to launch the agent until Agent Access is defined (re-run: bash define-agent-access.sh $ORG_ALIAS)\"\nelse\n log_warn \"define-agent-access.sh not found — skipping Agent Access definition\"\nfi\n\n# ============================================================\n# Phase 4c.5: Stage Descriptions (BEFORE the StageName prompt)\n# ============================================================\n# Stage descriptions are the grounding source the managed StageName template reads;\n# they MUST exist before Phase 4d deploys/activates and Phase 4e tests that prompt,\n# otherwise a StageName-selected run would verify a prompt that can generate nothing.\n# run_stage_descriptions() self-gates on STAGENAME_SELECTED (no-op otherwise) and\n# sets the script-global STAGE_DESCRIPTIONS_BLOCKED for the Phase 8.5 summary. This\n# lives in Block A, so on a --from-phase flow (Call 2) run it is skipped entirely —\n# by then Call 1 has already created the descriptions.\nrun_stage_descriptions\n\n# ============================================================\n# Phase 4d: Prompt Templates for Selected Fields\n# ============================================================\n# 3-phase setup: create/activate the field-completion prompt templates BEFORE\n# the flow goes live, so the verification gate (Phase 4e) can test them and the\n# admin can confirm output before any scheduled run. Delegates to\n# add-field-suggestion.sh --skip-flow (the flow wiring is owned by Phase 5).\n# - StageName: picklist, uses the managed template — nothing to deploy.\n# - NextStep : has a curated OOTB managed prompt. Deploy an OVERRIDE only when\n# the admin supplied a goal/instruction; otherwise leave the OOTB prompt.\n# - Custom fields: always deploy a new field-completion template (with the\n# admin's goal/instruction, or add-field-suggestion.sh's defaults).\nsection \"Phase 4d: Prompt Templates for Selected Fields\"\n\ndeclare -a DEPLOYED_TEMPLATE_FIELDS=()\nfor _f in \"${FIELD_ARRAY[@]}\"; do\n if [[ \"$_f\" == \"StageName\" ]]; then\n log_info \"StageName: uses the managed picklist template (nothing to deploy).\"\n continue\n fi\n _goal=\"${FIELD_PROMPTS[\"${_f}_goal\"]:-}\"\n _instr=\"${FIELD_PROMPTS[\"${_f}_instruction\"]:-}\"\n if [[ \"$_f\" == \"NextStep\" && -z \"$_goal\" && -z \"$_instr\" ]]; then\n log_info \"NextStep: keeping the OOTB managed prompt (no customization requested).\"\n DEPLOYED_TEMPLATE_FIELDS+=(\"$_f\")\n continue\n fi\n\n # Build the add-field-suggestion.sh argument list safely (array, not eval).\n _afs_args=(\"$ORG_ALIAS\" \"$_f\" --skip-flow)\n [[ -n \"$_goal\" ]] && _afs_args+=(--goal \"$_goal\")\n [[ -n \"$_instr\" ]] && _afs_args+=(--instruction \"$_instr\")\n\n log_try \"Deploying prompt template for '$_f'...\"\n if bash \"$SCRIPT_DIR/add-field-suggestion.sh\" \"${_afs_args[@]}\"; then\n log_pass \"Prompt template ready for '$_f'.\"\n DEPLOYED_TEMPLATE_FIELDS+=(\"$_f\")\n else\n log_warn \"Prompt template deploy failed for '$_f' — suggestions for this field may not generate.\"\n track_issue \"🟡 WARNING\" \"Prompt template deploy failed for field '$_f'\" \"Re-run: bash add-field-suggestion.sh $ORG_ALIAS $_f\"\n fi\ndone\n\n# --- Prune fields whose template deploy failed, BEFORE the flow is built ---\n# A field with no active template that is still wired into the flow produces zero\n# suggestions at runtime — the exact silent failure this 3-phase refactor exists to\n# prevent. The wired set must be: successfully-deployed fields + StageName (managed\n# picklist template, nothing to deploy). Rebuild SELECTED_FIELDS/FIELD_ARRAY from\n# those survivors so Phase 5 wires only fields that will actually generate.\n_WIRE_FIELDS=(\"${DEPLOYED_TEMPLATE_FIELDS[@]}\")\nif [[ \"$STAGENAME_SELECTED\" == \"true\" ]]; then\n _WIRE_FIELDS+=(\"StageName\")\nfi\nif [[ ${#_WIRE_FIELDS[@]} -lt ${#FIELD_ARRAY[@]} ]]; then\n # At least one selected field was dropped (template deploy failed).\n _dropped=()\n for _sf in \"${FIELD_ARRAY[@]}\"; do\n _keep=false\n for _wf in \"${_WIRE_FIELDS[@]}\"; do [[ \"$_sf\" == \"$_wf\" ]] && { _keep=true; break; }; done\n [[ \"$_keep\" == \"false\" ]] && _dropped+=(\"$_sf\")\n done\n log_warn \"Dropping field(s) with no active template from the flow: ${_dropped[*]}\"\nfi\n# Abort if every field fell out — a flow with an empty OpportunityFields collection\n# would be a no-op, and this preserves the up-front abort-on-zero invariant.\nif [[ ${#_WIRE_FIELDS[@]} -eq 0 ]]; then\n log_fail \"No field has an active prompt template — cannot build a working flow.\"\n echo \" Every selected field's template failed to deploy in Phase 4d (see warnings above).\"\n echo \" Fix the template deploy(s), then re-run setup.\"\n exit 1\nfi\nFIELD_ARRAY=(\"${_WIRE_FIELDS[@]}\")\nSELECTED_FIELDS=$(IFS=,; echo \"${_WIRE_FIELDS[*]}\")\n\n# Fields to VERIFY (Phase 4e) — a superset of DEPLOYED_TEMPLATE_FIELDS. StageName\n# deploys nothing (managed picklist template) so it's absent from the deployed set,\n# but it still generates a default suggestion the admin should see and get a chance\n# to override. Same for NextStep left uncustomized (OOTB prompt) — it's already in\n# the deployed set. So: show a default suggestion for every SELECTED field, even\n# uncustomized ones (\"only when selected\" — never pull in unselected fields).\ndeclare -a VERIFY_FIELDS=(\"${DEPLOYED_TEMPLATE_FIELDS[@]}\")\nif [[ \"$STAGENAME_SELECTED\" == \"true\" ]]; then\n VERIFY_FIELDS+=(\"StageName\")\nfi\n\n# --- --through-phase prompts: stop here, hand off to the agent tune-loop ---\n# The templates are deployed and active but the flow is NOT built. The agent now\n# runs the interactive per-field tune-loop (generate a tailored note → seed → run\n# /generations → show the suggestion → loop until the user approves), then resumes\n# with `--from-phase flow`. We STOP BEFORE the built-in Phase 4e gate because the\n# agent's loop supersedes it (and a piped-stdin run couldn't block on it anyway).\nif [[ \"$RUN_THROUGH_PHASE\" == \"prompts\" ]]; then\n section \"Prompt templates ready — handing off for tuning\"\n log_pass \"Deployed/active prompt template field(s): ${DEPLOYED_TEMPLATE_FIELDS[*]:-none}\"\n if [[ \"$STAGENAME_SELECTED\" == \"true\" ]]; then\n log_info \"StageName is also selected (managed picklist template; no per-field tuning).\"\n fi\n echo \"\"\n echo \" NEXT (agent tune-loop): for each CUSTOMIZED field, seed a tailored note and test:\"\n echo \" bash add-field-suggestion.sh $ORG_ALIAS \u003cField> --skip-flow --force \\\\\"\n echo \" --goal '\u003cgoal>' --instruction '\u003cinstruction>' \\\\\"\n echo \" --verify-with-note --note '\u003cnote tailored to the instruction>'\"\n echo \"\"\n echo \" ALSO show the DEFAULT suggestion for every selected field left UNCUSTOMIZED\"\n echo \" (StageName always; NextStep when no goal/instruction was given) so the user\"\n echo \" can decide whether to keep or change the default even though they didn't tune it:\"\n echo \" bash verify-prompt-generation.sh $ORG_ALIAS \u003cField> \u003coppId>\"\n echo \" To CHANGE it: NextStep is tunable via add-field-suggestion.sh (above);\"\n echo \" StageName's suggestion is shaped by its stage descriptions — re-run with\"\n echo \" --create-stage-descriptions (or edit OpptStageDescription) to change it.\"\n echo \"\"\n echo \" Loop until the user approves each suggestion, then build & activate the flow:\"\n echo \" bash setup-all.sh $ORG_ALIAS --fields \\\"$SELECTED_FIELDS\\\" --from-phase flow\"\n echo \"\"\n log_info \"Stopping after Phase 4d (--through-phase prompts). Flow NOT yet built.\"\n exit 0\nfi\n\n# ============================================================\n# Phase 4e: Prompt Verification Gate (before the flow goes live)\n# ============================================================\n# 3-phase setup: the templates from Phase 4d are now active but the flow is NOT\n# yet deployed/activated. Test each template SYNCHRONOUSLY against a real\n# opportunity + a seeded note, show the admin exactly what the AI will suggest,\n# and (interactively) gate flow activation on their approval. This catches a bad\n# goal/instruction BEFORE the scheduled run ever fires — far cheaper to fix now\n# than after go-live.\n#\n# We verify VERIFY_FIELDS — every SELECTED field that generates a suggestion,\n# including ones left uncustomized (NextStep on its OOTB prompt, and StageName on\n# the managed picklist template). Showing those defaults lets the admin override a\n# default they never explicitly tuned. StageName generates only because Phase 4c.5\n# already created its grounding stage descriptions.\n#\n# Fully skipped when:\n# - --skip-prompt-verification was passed (explicit opt-out), OR\n# - no selected field generates a suggestion (VERIFY_FIELDS empty).\n# In NON_INTERACTIVE runs (incl. agent-driven — a piped stdin forces it) the\n# generation still RUNS and the suggestions are PRINTED for review; only the human\n# yes/no gate degrades to informational (the flow proceeds without blocking).\nsection \"Phase 4e: Prompt Verification Gate\"\n\nif [[ \"$SKIP_PROMPT_VERIFICATION\" == \"true\" ]]; then\n log_info \"Prompt verification skipped (--skip-prompt-verification).\"\nelif [[ ${#VERIFY_FIELDS[@]} -eq 0 ]]; then\n log_info \"No selected field generates a suggestion to verify.\"\nelse\n # NOTE ON NON-INTERACTIVE RUNS (incl. agent-driven, since a piped stdin forces\n # NON_INTERACTIVE): we STILL seed a test opp/note and run /generations below so\n # the generated suggestions are printed for review. What a non-interactive run\n # cannot do is BLOCK on a human yes/no — so the approval gate at the end degrades\n # to informational output and the flow proceeds. An agent relays this output to\n # the user; a true unattended run (cron/CI) simply logs it.\n # --- Find an eligible open opportunity (matches the flow's own filter) ---\n # IsClosed=false AND CloseDate within +90 days, so we don't test against an opp\n # the scheduled flow would itself skip (which would look like a false negative).\n VERIFY_OPP_ID=$(sf data query -q \"SELECT Id FROM Opportunity WHERE IsClosed=false AND CloseDate>=TODAY AND CloseDate\u003c=NEXT_N_DAYS:90 ORDER BY LastModifiedDate DESC LIMIT 1\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty')\n\n if [[ -z \"$VERIFY_OPP_ID\" ]]; then\n log_try \"No eligible open opportunity found — creating one for verification.\"\n # A bare Name + future CloseDate + open Stage satisfies the flow's eligibility\n # filter. Don't hardcode 'Prospecting' — orgs with custom Sales Processes/record\n # types may not have it. Query the first active, open (non-won/lost) stage.\n _open_stage=$(sf data query -q \"SELECT MasterLabel FROM OpportunityStage WHERE IsActive=true AND IsClosed=false ORDER BY SortOrder LIMIT 1\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].MasterLabel // empty')\n [[ -z \"$_open_stage\" ]] && _open_stage=\"Prospecting\" # last-resort default\n VERIFY_OPP_ID=$(sf data create record --sobject Opportunity \\\n --values \"Name='PM Setup Verification Opp' StageName='${_open_stage}' CloseDate=$(python3 -c 'import datetime; print((datetime.date.today()+datetime.timedelta(days=30)).isoformat())')\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.id // empty')\n if [[ -n \"$VERIFY_OPP_ID\" ]]; then\n log_pass \"Created verification opportunity $VERIFY_OPP_ID\"\n else\n log_warn \"Could not create a verification opportunity; skipping the prompt gate.\"\n track_issue \"🟡 WARNING\" \"Prompt verification skipped — no eligible opportunity\" \"Create an open Opportunity (CloseDate within 90 days) and test with verify-prompt-generation.sh.\"\n VERIFY_OPP_ID=\"\"\n fi\n else\n log_pass \"Using eligible opportunity $VERIFY_OPP_ID for verification.\"\n fi\n\n if [[ -n \"$VERIFY_OPP_ID\" ]]; then\n # --- Seed a ContentNote with sample conversation signal + link to the opp ---\n # Interpolate relative dates (today / today+9d) so the sample never reads as\n # stale to the model on a run weeks after this was authored.\n _note_today=$(python3 -c 'import datetime; print(datetime.date.today().strftime(\"%B %-d\"))')\n _note_soon=$(python3 -c 'import datetime; print((datetime.date.today()+datetime.timedelta(days=9)).strftime(\"%B %-d\"))')\n NOTE_BODY=\"Call with John Smith on ${_note_today}: Discussed Q4 budget approval. John mentioned competitor Acme Corp is being evaluated. Next step: Schedule executive sponsor meeting by ${_note_soon}. Risk: Budget freeze possible if exec sponsor changes.\"\n # macOS/BSD `base64` line-wraps at 76 cols (embeds newlines); GNU's `-w0` isn't\n # portable. `tr -d '\\n'` strips wraps on both so the value stays single-line and\n # doesn't break the --values string below.\n NOTE_B64=$(printf '%s' \"$NOTE_BODY\" | base64 | tr -d '\\n')\n log_try \"Seeding a ContentNote with sample conversation data.\"\n # Note-seeding is best-effort: `|| true` keeps a create failure from tripping\n # `set -e` so it degrades to the log_warn fallback instead of aborting the gate.\n VERIFY_NOTE_ID=$(sf data create record --sobject ContentNote --values \"Title='PM Setup Verification Note' Content='${NOTE_B64}'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.id // empty' || true)\n if [[ -n \"$VERIFY_NOTE_ID\" ]]; then\n # ContentNote Id IS the ContentDocumentId (shared 069 prefix); link directly.\n # The ContentVersion→LatestVersionId subquery returns null on some orgs.\n if sf data create record --sobject ContentDocumentLink --values \"ContentDocumentId='${VERIFY_NOTE_ID}' LinkedEntityId='${VERIFY_OPP_ID}' ShareType='V' Visibility='AllUsers'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null >/dev/null; then\n log_pass \"Seeded note and linked to $VERIFY_OPP_ID\"\n else\n log_warn \"Note created but could not link to the opportunity; generation runs against existing grounding data only.\"\n fi\n else\n log_warn \"Could not seed a note; generation runs against existing grounding data only.\"\n fi\n\n # --- Generate a suggestion per verified field and show it to the admin ---\n echo \"\"\n echo \" Testing each prompt against opportunity $VERIFY_OPP_ID:\"\n echo \"\"\n _gen_ok=0 # count templates that returned usable output\n for _vf in \"${VERIFY_FIELDS[@]}\"; do\n # Resolve the template name the /generations endpoint expects:\n # - StageName: the managed picklist template (nothing deployed, but it still\n # generates a default the admin should see and can choose to override).\n # - NextStep: the managed template (an override folds into it as a new\n # version; the OOTB prompt shares the same name), so always query the\n # managed name.\n # - Custom field: Recommend\u003cField, stripped of __c and non-alnum>forOpp,\n # matching add-field-suggestion.sh's dev-name derivation.\n if [[ \"$_vf\" == \"StageName\" ]]; then\n _vtmpl=\"sales_pipe_mgmt__RecommendStageforOpp\"\n elif [[ \"$_vf\" == \"NextStep\" ]]; then\n _vtmpl=\"sales_pipe_mgmt__RecommendNextStepforOpp\"\n else\n _vtmpl=\"Recommend$(printf '%s' \"$_vf\" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')forOpp\"\n fi\n\n # Bound at 120s: generation can legitimately take longer than a plain REST\n # call (CURL_TIMEOUT=30), but must not hang indefinitely and blow the run's\n # tool timeout. A timeout yields non-zero → the `|| echo '{}'` fallback, so a\n # stall reads as \"no output\" and flows into the blocked-field handling below.\n _gen=$(with_timeout 120 sf api request rest \"/services/data/v${API_VERSION}/einstein/prompt-templates/${_vtmpl}/generations\" \\\n --method POST --target-org \"$ORG_ALIAS\" \\\n --body \"{\\\"isPreview\\\":false,\\\"inputParams\\\":{\\\"valueMap\\\":{\\\"Input:Opportunity\\\":{\\\"value\\\":{\\\"id\\\":\\\"${VERIFY_OPP_ID}\\\"}}}},\\\"additionalConfig\\\":{\\\"numGenerations\\\":1,\\\"temperature\\\":0,\\\"applicationName\\\":\\\"PromptTemplateGenerationsInvocable\\\"}}\" 2>/dev/null || echo '{}')\n _gentext=$(echo \"$_gen\" | jq -r '.generations[0].text // empty' 2>/dev/null)\n if [[ -n \"$_gentext\" ]]; then\n # The generation text is itself a JSON blob (FieldName/SuggestedNewValue/Reasoning).\n _sugg=$(echo \"$_gentext\" | jq -r '.SuggestedNewValue // empty' 2>/dev/null)\n _reason=$(echo \"$_gentext\" | jq -r '.Reasoning // empty' 2>/dev/null)\n _gen_ok=$((_gen_ok + 1))\n if [[ -n \"$_sugg\" ]]; then\n echo \" ── $_vf ($_vtmpl)\"\n echo \" Suggested: $_sugg\"\n [[ -n \"$_reason\" ]] && echo \" Reasoning: $_reason\"\n else\n echo \" ── $_vf ($_vtmpl)\"\n echo \" Raw output: $_gentext\"\n fi\n else\n _generr=$(echo \"$_gen\" | jq -r '.[0].message // .message // empty' 2>/dev/null | head -1)\n echo \" ── $_vf ($_vtmpl)\"\n log_warn \"No suggestion generated${_generr:+ — $_generr}\"\n fi\n echo \"\"\n done\n\n # --- The gate: admin must approve before the flow is deployed/activated ---\n # If NOT ONE template produced output, the gate tested nothing — approving\n # \"prompts look good\" here would be meaningless. Require an explicit,\n # differently-worded override so a reviewer who saw only warnings cannot\n # wave through unvalidated prompts on autopilot.\n if [[ \"$NON_INTERACTIVE\" == \"true\" ]]; then\n # No TTY to block on — the gate degrades to informational output. Report\n # what was (or wasn't) generated and proceed; the caller (agent/operator)\n # reviews the printed suggestions and can re-run add-field-suggestion.sh to\n # adjust a prompt afterward.\n if [[ \"$_gen_ok\" -eq 0 ]]; then\n log_warn \"Non-interactive run: NO template produced output — flow will activate on UNVERIFIED prompts.\"\n echo \" Review the warnings above; adjust with:\"\n echo \" bash add-field-suggestion.sh $ORG_ALIAS \u003cField> --skip-flow --goal '...' --instruction '...' --force\"\n track_issue \"🟡 WARNING\" \"Prompts unverified (non-interactive, no generation output)\" \"Review suggestions; re-run add-field-suggestion.sh to adjust, then re-run setup.\"\n else\n log_info \"Non-interactive run: suggestions shown above are informational — proceeding to flow activation.\"\n echo \" If a suggestion looks wrong, adjust its prompt with:\"\n echo \" bash add-field-suggestion.sh $ORG_ALIAS \u003cField> --skip-flow --goal '...' --instruction '...' --force\"\n fi\n elif [[ \"$_gen_ok\" -eq 0 ]]; then\n log_warn \"No template produced any output — the prompts could NOT be validated.\"\n echo \"\"\n echo \" Common causes: templates still activating (retry in a minute), no\"\n echo \" grounding data on the opportunity, or a template deploy that failed above.\"\n echo \" Proceeding now would activate the flow on UNVERIFIED prompts.\"\n echo \"\"\n if ! confirm \"Proceed to build & activate the flow anyway, WITHOUT verified prompt output?\"; then\n log_fail \"Verification could not complete and was not overridden. Flow NOT activated.\"\n echo \"\"\n echo \" The prompt templates are deployed and active, but the suggestion flow\"\n echo \" has not been built or activated, so no scheduled suggestions will run.\"\n echo \" Inspect a prompt with:\"\n echo \" bash verify-prompt-generation.sh $ORG_ALIAS \u003cField>\"\n echo \" or adjust one with:\"\n echo \" bash add-field-suggestion.sh $ORG_ALIAS \u003cField> --skip-flow --goal '...' --instruction '...' --force\"\n echo \" Then re-run this setup to complete flow activation.\"\n exit 1\n fi\n log_warn \"Proceeding to flow activation on UNVERIFIED prompts (explicit override).\"\n elif ! confirm \"Prompts look good — proceed to build & activate the flow?\"; then\n log_fail \"Verification declined. Flow NOT activated.\"\n echo \"\"\n echo \" The prompt templates are deployed and active, but the suggestion flow\"\n echo \" has not been built or activated, so no scheduled suggestions will run.\"\n echo \" To adjust a prompt, edit it with:\"\n echo \" bash add-field-suggestion.sh $ORG_ALIAS \u003cField> --skip-flow --goal '...' --instruction '...' --force\"\n echo \" Then re-run this setup to complete flow activation.\"\n exit 1\n else\n log_pass \"Verification approved — proceeding to flow activation.\"\n fi\n fi\nfi\n\nfi # END BLOCK A (Phase 1.5–4e) — skipped entirely on a --from-phase flow run.\n\n# ============================================================\n# BLOCK B — Flow, Agent wiring & Verification (Phase 5 → 8)\n# ============================================================\n# Builds the suggestion flow from the approved field set, activates it, wires the\n# agent/PSG, and runs final verification. Self-contained: everything it needs it\n# re-derives from the org (bot count, PSG id/state) or from the context vars set in\n# Phase 0/1/1.7 — so it runs correctly both in a full end-to-end pass and as the\n# second half of a split (--from-phase flow) run.\n\n# --- Split-path re-prune: fields must have an ACTIVE prompt template ---\n# On a full run, Block A already pruned SELECTED_FIELDS/FIELD_ARRAY to fields with\n# a successfully-deployed active template (lines ~1635-1664). On a --from-phase flow\n# run Block A is skipped, so SELECTED_FIELDS is still the RAW --fields the agent\n# passed. Wiring a field with no active template into the flow yields zero\n# suggestions at runtime — the exact silent failure this refactor prevents. So\n# re-derive the survivor set here from the org (read-only), mirroring Block A's\n# invariant: keep StageName unconditionally (managed picklist, nothing to deploy),\n# keep any other field whose GenAiPromptTemplate is active.\nif [[ \"$RUN_FROM_PHASE\" == \"flow\" ]]; then\n _ACTIVE_TEMPLATES=$(sf api request rest \"/services/data/v${API_VERSION}/einstein/prompt-templates\" --target-org \"$ORG_ALIAS\" 2>/dev/null \\\n | jq -r '.promptRecords[]? | select(.fields.IsActive.value==true or .fields.IsActive.value==\"true\") | .fields.DeveloperName.value' 2>/dev/null || echo \"\")\n _WIRE_FIELDS=()\n _dropped=()\n for _sf in \"${FIELD_ARRAY[@]}\"; do\n if [[ \"$_sf\" == \"StageName\" ]]; then\n _WIRE_FIELDS+=(\"StageName\")\n continue\n fi\n # Expected dev name: Recommend\u003cField-without-__c-and-non-alnum>forOpp. Match with\n # or without the sales_pipe_mgmt__ namespace prefix (NextStep's managed template\n # is namespaced; standalone custom-field templates are not).\n _clean=$(echo \"$_sf\" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')\n _expect=\"Recommend${_clean}forOpp\"\n if echo \"$_ACTIVE_TEMPLATES\" | grep -qiE \"(^|__)${_expect}\\$\"; then\n _WIRE_FIELDS+=(\"$_sf\")\n else\n _dropped+=(\"$_sf\")\n fi\n done\n if [[ ${#_dropped[@]} -gt 0 ]]; then\n log_warn \"Dropping field(s) with no active prompt template from the flow: ${_dropped[*]}\"\n echo \" These templates were not active on the org. Re-run the tune-loop\"\n echo \" (add-field-suggestion.sh ... --force) for each, then re-run --from-phase flow.\"\n fi\n if [[ ${#_WIRE_FIELDS[@]} -eq 0 ]]; then\n log_fail \"No selected field has an active prompt template — cannot build a working flow.\"\n echo \" Deploy/activate at least one field's template (Call 1 + tune-loop), then re-run --from-phase flow.\"\n exit 1\n fi\n FIELD_ARRAY=(\"${_WIRE_FIELDS[@]}\")\n SELECTED_FIELDS=$(IFS=,; echo \"${_WIRE_FIELDS[*]}\")\n log_info \"Flow will wire ${#FIELD_ARRAY[@]} field(s) with active templates: $SELECTED_FIELDS\"\nfi\n\n# ============================================================\n# Phase 5: Schedule-Triggered Flow (with deploy + activation)\n# ============================================================\nsection \"Phase 5: Schedule-Triggered Flow\"\n\n# Detection: check by SourceTemplateId, naming convention, and exact API name\nFLOW_NAME=\"\"\nfor QUERY in \\\n \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false\" \\\n \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=false\" \\\n \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false\"; do\n\n FLOW_NAME=$(sf data query --query \"$QUERY\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].ApiName // empty' 2>/dev/null || echo \"\")\n [[ -n \"$FLOW_NAME\" ]] && break\ndone\n\nif [[ -n \"$FLOW_NAME\" ]]; then\n log_pass \"Flow found: $FLOW_NAME\"\nelse\n log_info \"Flow not found. Deploying from pre-retrieved template...\"\n\n TEMPLATE_FILE=\"$SCRIPT_DIR/../assets/pipeline_management_flow.flow-meta.xml\"\n\n if [[ -f \"$TEMPLATE_FILE\" ]]; then\n ensure_sfdx_project\n mkdir -p force-app/main/default/flows\n FLOW_FILE=\"force-app/main/default/flows/Process_Field_Update_Suggestions.flow-meta.xml\"\n\n # Greenfield: BUILD the flow with ONLY the up-front-selected fields (no\n # deploy-then-strip). build_field_collection reads the base template and\n # writes FLOW_FILE with exactly SELECTED_FIELDS wired into\n # AddOppFieldsToCollection, emitting a distinct nonzero exit on each failure\n # (2 empty set / 3 parse-fail / 4 assignment-missing / 5 write-fail).\n # We do NOT fall back to copying the base template: that verbatim copy wires\n # BOTH OOTB fields, which would ship StageName the admin excluded (and with no\n # supporting stage descriptions). The selected set is already validated\n # non-empty (Phase 1.7) and pruned to fields with active templates, so a build\n # failure here is a genuine environment problem — abort loudly instead.\n if build_field_collection \"$SELECTED_FIELDS\" \"$TEMPLATE_FILE\" \"$FLOW_FILE\"; then\n log_pass \"Flow built with ${#FIELD_ARRAY[@]} selected field(s): $SELECTED_FIELDS\"\n else\n _bfc_rc=$?\n log_fail \"Could not build the flow from selected fields (build_field_collection rc=$_bfc_rc).\"\n case $_bfc_rc in\n 2) echo \" Empty field set after pruning — nothing to wire.\" ;;\n 3) echo \" Base flow template did not parse as XML: $TEMPLATE_FILE\" ;;\n 4) echo \" AddOppFieldsToCollection assignment not found in the base template.\" ;;\n 5) echo \" Could not write the built flow to $FLOW_FILE (permissions/disk).\" ;;\n *) echo \" Unexpected failure; check that python3 is on PATH.\" ;;\n esac\n echo \" Flow NOT deployed. Fix the above and re-run setup.\"\n exit 1\n fi\n\n # Update startDate to today\n TODAY=$(date +%Y-%m-%d)\n sed_inplace \"s|\u003cstartDate>[^\u003c]*\u003c/startDate>|\u003cstartDate>${TODAY}\u003c/startDate>|g\" \"$FLOW_FILE\"\n\n # Sync the flow's SalesManagementAgentTemplate constant with the actual\n # AgentTemplate of the bot deployed to this org. The template's default\n # value is the legacy SalesMgmt__SalesAgent, but our authoring bundle\n # publishes the NGA variant (SalesMgmt__NGASalesAgent). The flow's\n # BotDefinition lookup matches on this constant, so a mismatch means\n # the action silently fails to find the agent at runtime.\n set +e # Allow pm_bot_agent_template to return non-zero without aborting\n DETECTED_TEMPLATE=$(pm_bot_agent_template \"$ORG_ALIAS\")\n DETECT_RC=$?\n set -e\n case $DETECT_RC in\n 0)\n PM_BOT_TOTAL=$(pm_bot_count \"$ORG_ALIAS\")\n if [[ \"${PM_BOT_TOTAL:-0}\" -gt 1 ]]; then\n log_warn \"Multiple active Pipeline Management bots detected on org (${PM_BOT_TOTAL}). Flow will bind to: $DETECTED_TEMPLATE (NGA preferred). Clean up stale bots to avoid runtime ambiguity.\"\n fi\n sed_inplace \"s|\u003cstringValue>SalesMgmt__SalesAgent\u003c/stringValue>|\u003cstringValue>${DETECTED_TEMPLATE}\u003c/stringValue>|g\" \"$FLOW_FILE\"\n log_info \"Flow template constant set to: $DETECTED_TEMPLATE\"\n ;;\n 1)\n log_fail \"No active Pipeline Management bot found on org. Phase 4b should have created and activated the bot before flow deploy. Aborting flow deploy to prevent binding the flow to a missing or inactive agent.\"\n exit 1\n ;;\n *)\n log_fail \"Could not query BotDefinition for AgentTemplate (sf data query failed — auth, network, or API error). Re-run after resolving the underlying issue.\"\n exit 1\n ;;\n esac\n\n # Field wiring for the greenfield flow is already done: build_field_collection\n # (above) wrote FLOW_FILE with exactly the up-front-selected fields. There is\n # no deploy-then-strip here. strip_flow_field / add_flow_field (from\n # shared/flow-builder.sh) are reserved for the REPAIR path below, which reconciles\n # an already-deployed flow's wired fields to the selected set.\n\n # --- APPROACH 1: Deploy as source ---\n log_try \"Deploying flow via source deploy...\"\n DEPLOY_RESULT=$(sf project deploy start --source-dir \"$FLOW_FILE\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null || echo '{\"status\":1}')\n\n if echo \"$DEPLOY_RESULT\" | jq -e '.status == 0' >/dev/null 2>&1; then\n FLOW_NAME=\"Process_Field_Update_Suggestions\"\n log_pass \"Flow deployed: $FLOW_NAME\"\n else\n DEPLOY_ERR=$(safe_jq \"$DEPLOY_RESULT\" '.result.details.componentFailures[0].problem // .message // \"unknown\"' \"unknown\")\n echo \" Source deploy error: $DEPLOY_ERR\"\n\n # --- APPROACH 2: Deploy as metadata ---\n log_try \"Retrying as metadata deploy...\"\n DEPLOY_RESULT=$(sf project deploy start --metadata \"Flow:Process_Field_Update_Suggestions\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null || echo '{\"status\":1}')\n if echo \"$DEPLOY_RESULT\" | jq -e '.status == 0' >/dev/null 2>&1; then\n FLOW_NAME=\"Process_Field_Update_Suggestions\"\n log_pass \"Flow deployed: $FLOW_NAME (metadata approach)\"\n else\n log_warn \"Both deploy approaches failed.\"\n fi\n fi\n else\n log_warn \"Template file not found at: $TEMPLATE_FILE\"\n fi\n\n # --- If still no flow, UI guidance ---\n if [[ -z \"$FLOW_NAME\" ]]; then\n log_warn \"Flow requires manual creation via Setup UI.\"\n echo \"\"\n echo \" Steps:\"\n echo \" 1. Open: ${INSTANCE_URL}/lightning/setup/Flows/home\"\n echo \" 2. Find 'OppSuggGenSchFlow' (template, sales_pipe_mgmt namespace)\"\n echo \" 3. Click 'Save As' → Label: 'Process Field Update Suggestions'\"\n echo \" 4. Click 'Activate'\"\n echo \"\"\n\n # Non-interactive: skip (don't block on user input in agent context)\n if [[ -t 0 ]]; then\n while true; do\n read -rp \" Press Enter after completing (or 'skip' to continue): \" RESPONSE\n if [[ \"$RESPONSE\" == \"skip\" ]]; then break; fi\n FLOW_NAME=$(sf data query --query \"SELECT ApiName FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].ApiName // empty' 2>/dev/null || echo \"\")\n [[ -n \"$FLOW_NAME\" ]] && { log_pass \"Flow detected: $FLOW_NAME\"; break; }\n log_warn \"Flow not detected yet.\"\n done\n fi\n fi\nfi\n\n# --- Flow Activation + Field Reconciliation (REPAIR path for an existing flow) ---\n# When the flow already exists we do NOT rebuild it from the base template (that\n# would discard the org-specific SalesManagementAgentTemplate constant the\n# deployed flow carries). Instead we RECONCILE the flow's wired fields to the\n# up-front selected set — the \"fix method\": strip fields no longer selected,\n# add newly-selected ones (via strip_flow_field / add_flow_field). We then set\n# it Active and redeploy. This runs whether or not the flow is currently active,\n# because the field set may have changed even on an active flow.\nif [[ -n \"$FLOW_NAME\" ]]; then\n FLOW_ACTIVE=$(sf data query --query \"SELECT IsActive FROM FlowDefinitionView WHERE ApiName='${FLOW_NAME}'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].IsActive // \"false\"' 2>/dev/null || echo \"false\")\n\n log_try \"Reconciling existing flow to selected fields + activating...\"\n\n # Retrieve → reconcile fields → set Active → reconcile constant → deploy\n ensure_sfdx_project\n mkdir -p force-app/main/default/flows\n sf project retrieve start --metadata \"Flow:${FLOW_NAME}\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null >/dev/null || true\n\n FLOW_FILE=\"force-app/main/default/flows/${FLOW_NAME}.flow-meta.xml\"\n if [[ -f \"$FLOW_FILE\" ]]; then\n # --- Field reconciliation: make the wired set == SELECTED_FIELDS ---\n # Strip any wired field not in the selected set; add any selected field\n # not yet wired. Both helpers are idempotent and namespace-safe (python3).\n # The AddOppFieldsToCollection assignment must never end up empty — the\n # selected set is guaranteed non-empty (validated in Phase 1.7), so as long\n # as we add-before-we-finish the assignment stays valid.\n FIELDS_CHANGED=false\n WIRED_FIELDS=\"$(flow_wired_fields \"$FLOW_FILE\" 2>/dev/null || echo \"\")\"\n # Normalized \",a,b,c,\" form computed once for exact-comma membership tests\n # (mirrors the ,$SELECTED_FIELDS, idiom on the strip side).\n WIRED_CSV=\",$(printf '%s' \"$WIRED_FIELDS\" | tr '\\n' ',' | sed 's/,,*/,/g'),\"\n if [[ -n \"$WIRED_FIELDS\" ]]; then\n while IFS= read -r _wired; do\n [[ -z \"$_wired\" ]] && continue\n if [[ \",$SELECTED_FIELDS,\" != *\",$_wired,\"* ]]; then\n # strip: rc 0 = removed, 2 = not present (benign), other = real error.\n _rc=0; strip_flow_field \"$FLOW_FILE\" \"$_wired\" || _rc=$?\n case $_rc in\n 0) FIELDS_CHANGED=true; log_info \"Unwired field no longer selected: $_wired\" ;;\n 2) : ;; # not present — nothing to do\n *) log_warn \"Could not unwire '$_wired' (rc=$_rc) — it may remain in the flow.\"\n track_issue \"🟡 WARNING\" \"Flow field reconcile: strip of '$_wired' failed (rc=$_rc)\" \"The flow's wired field set may not match the selected set. Re-run setup or edit the flow manually.\" ;;\n esac\n fi\n done \u003c\u003c\u003c \"$WIRED_FIELDS\"\n fi\n for _sel in \"${FIELD_ARRAY[@]}\"; do\n if [[ \"$WIRED_CSV\" != *\",$_sel,\"* ]]; then\n # add: rc 0 = added, 2 = already wired (benign), other = real error.\n _rc=0; add_flow_field \"$FLOW_FILE\" \"$_sel\" || _rc=$?\n case $_rc in\n 0) FIELDS_CHANGED=true; log_info \"Wired newly-selected field: $_sel\" ;;\n 2) : ;; # already wired — nothing to do\n *) log_warn \"Could not wire '$_sel' (rc=$_rc) — it may not generate suggestions.\"\n track_issue \"🟡 WARNING\" \"Flow field reconcile: add of '$_sel' failed (rc=$_rc)\" \"The selected field '$_sel' may not be wired into the flow. Re-run: bash add-field-suggestion.sh $ORG_ALIAS $_sel\" ;;\n esac\n fi\n done\n if [[ \"$FIELDS_CHANGED\" == \"true\" ]]; then\n log_pass \"Flow fields reconciled to: $SELECTED_FIELDS\"\n else\n log_info \"Flow fields already match selected set: $SELECTED_FIELDS\"\n fi\n\n # Short-circuit only when nothing needs deploying: already active AND no\n # field change. Otherwise fall through to set-Active + redeploy.\n if [[ \"$FLOW_ACTIVE\" == \"true\" && \"$FIELDS_CHANGED\" == \"false\" ]]; then\n log_pass \"Flow is ACTIVE (no field changes needed)\"\n else\n sed_inplace 's|\u003cstatus>Draft\u003c/status>|\u003cstatus>Active\u003c/status>|g' \"$FLOW_FILE\"\n sed_inplace 's|\u003cstatus>Obsolete\u003c/status>|\u003cstatus>Active\u003c/status>|g' \"$FLOW_FILE\"\n\n # Reconcile the flow's SalesManagementAgentTemplate constant with the\n # on-org bot. The retrieved flow carries whatever template was wired\n # in at last deploy; if the org has since been upgraded (legacy ->\n # NGA) the constant is stale and the BotDefinition lookup misses.\n # The second sed handles the symmetric NGA -> legacy downgrade case.\n #\n # In this reconcile path we warn-and-skip rather than exit on failure:\n # the flow already exists on the org, so the worst case is that we\n # leave the existing constant in place and the deploy below may fail\n # loudly with a Salesforce-specific error — preferable to aborting\n # the whole setup script just because we couldn't sync the constant.\n set +e # Allow pm_bot_agent_template to return non-zero without aborting\n DETECTED_TEMPLATE=$(pm_bot_agent_template \"$ORG_ALIAS\")\n DETECT_RC=$?\n set -e\n case $DETECT_RC in\n 0)\n PM_BOT_TOTAL=$(pm_bot_count \"$ORG_ALIAS\")\n if [[ \"${PM_BOT_TOTAL:-0}\" -gt 1 ]]; then\n log_warn \"Multiple active Pipeline Management bots detected on org (${PM_BOT_TOTAL}). Flow will bind to: $DETECTED_TEMPLATE (NGA preferred). Clean up stale bots to avoid runtime ambiguity.\"\n fi\n sed_inplace \"s|\u003cstringValue>SalesMgmt__SalesAgent\u003c/stringValue>|\u003cstringValue>${DETECTED_TEMPLATE}\u003c/stringValue>|g\" \"$FLOW_FILE\"\n sed_inplace \"s|\u003cstringValue>SalesMgmt__NGASalesAgent\u003c/stringValue>|\u003cstringValue>${DETECTED_TEMPLATE}\u003c/stringValue>|g\" \"$FLOW_FILE\"\n log_info \"Flow template constant reconciled to: $DETECTED_TEMPLATE\"\n ;;\n 1)\n log_warn \"No active Pipeline Management bot found on org. Skipping template-constant reconciliation; activation will proceed with the existing constant and may fail if the bot is missing or inactive.\"\n ;;\n *)\n log_warn \"Could not query BotDefinition for AgentTemplate (sf data query failed). Skipping template-constant reconciliation; activation will proceed with the existing constant.\"\n ;;\n esac\n\n if sf project deploy start --metadata \"Flow:${FLOW_NAME}\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -e '.status == 0' >/dev/null 2>&1; then\n log_pass \"Flow activated\"\n else\n log_warn \"Flow activation failed. Activate manually: Setup → Flows → ${FLOW_NAME} → Activate\"\n fi\n fi # end: active+unchanged short-circuit vs set-Active+redeploy\n else\n log_warn \"Could not retrieve flow. Activate manually in Setup → Flows.\"\n fi\nfi\n\n# ============================================================\n# Phase 6: Stage Descriptions — RELOCATED\n# ============================================================\n# Stage-description creation now runs earlier, in Block A as Phase 4c.5 (before the\n# StageName prompt is deployed/activated/tested), via run_stage_descriptions() in\n# shared/stage-descriptions.sh. It is a prerequisite for the managed StageName template\n# to generate anything, so it must precede Phase 4d/4e. Nothing to do here.\n\n# ============================================================\n# Phase 7: User Permissions\n# ============================================================\nsection \"Phase 7: User Permissions\"\n\nCURRENT_USER=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.username // empty' 2>/dev/null || echo \"\")\nCURRENT_USER=$(sanitize_display \"$CURRENT_USER\" 80)\n# Sanitize for SOQL interpolation (prevent injection)\nCURRENT_USER_SAFE=$(sanitize_soql_value \"$CURRENT_USER\")\n\nPSG_ID=$(sf data query -q \"SELECT Id FROM PermissionSetGroup WHERE DeveloperName = 'SalesManagementUserPsg'\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\nif [[ -z \"$PSG_ID\" ]]; then\n log_warn \"SalesManagementUserPsg not found (provisioning may still be in progress)\"\nelse\n # The PSG recalc is triggered by provisioning and converges asynchronously; we do\n # NOT block on it. Report its current Status for context, then attempt assignment.\n # #5: if the PSG has not finished recalculating, the insert is rejected with\n # INVALID_CROSS_REFERENCE_KEY — assign_user_psg() classifies that explicitly and\n # tells the operator to re-run, instead of the old `|| log_pass` that masked the\n # rejection as a bogus \"Already assigned\".\n PSG_STATE=$(psg_status \"$ORG_ALIAS\" \"SalesManagementUserPsg\")\n if [[ \"$PSG_STATE\" != \"Updated\" ]]; then\n log_info \"SalesManagementUserPsg Status: $PSG_STATE — recalculation may still be in progress; assignment may need a re-run.\"\n fi\n\n # assign_user_psg \u003cuser-id> \u003cdisplay-name> — insert PermissionSetAssignment and\n # classify the outcome. Never masks a real rejection as success.\n assign_user_psg() {\n local user_id=\"$1\" display=\"$2\" res msg\n res=$(sf data create record --sobject PermissionSetAssignment \\\n --values \"AssigneeId='${user_id}' PermissionSetGroupId='${PSG_ID}'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null || echo '{\"status\":1}')\n if echo \"$res\" | jq -e '.result.id // empty' >/dev/null 2>&1; then\n log_pass \"Assigned SalesManagementUserPsg to: $display\"; return 0\n fi\n msg=$(echo \"$res\" | jq -r '.message // .result.message // \"\"' 2>/dev/null || echo \"\")\n if echo \"$msg\" | grep -qi \"DUPLICATE_VALUE\"; then\n log_info \"Already assigned: $display\"\n elif echo \"$msg\" | grep -qi \"INVALID_CROSS_REFERENCE_KEY\"; then\n log_warn \"Assignment for $display rejected (INVALID_CROSS_REFERENCE_KEY) — SalesManagementUserPsg has not finished recalculating.\"\n echo \" Re-run: bash setup-all.sh $ORG_ALIAS once the PSG Status reads 'Updated'.\"\n track_issue \"🟡 WARNING\" \"User PSG assignment deferred (PSG recalculating)\" \"Re-run setup-all.sh once SalesManagementUserPsg Status is 'Updated' to finish assigning $display.\"\n else\n log_warn \"Failed to assign $display: ${msg:-unknown}\"\n fi\n return 1\n }\n\n # assign_user_list \u003ccomma-separated-usernames> — resolve each username to an Id and\n # assign the PSG. Only ever processes the EXPLICIT list handed to it; there is no\n # code path that queries all/standard users and bulk-grants. Adding users is always\n # deliberate: the running user, an operator-typed interactive list, or --users.\n assign_user_list() {\n local raw_list=\"$1\" raw_user username username_safe user_id\n IFS=',' read -ra _USERS \u003c\u003c\u003c \"$raw_list\"\n for raw_user in \"${_USERS[@]}\"; do\n username=$(echo \"$raw_user\" | xargs)\n [[ -z \"$username\" ]] && continue\n username_safe=$(sanitize_soql_value \"$username\")\n user_id=$(sf data query -q \"SELECT Id FROM User WHERE Username = '${username_safe}'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n if [[ -z \"$user_id\" ]]; then\n log_fail \"User not found: $username\"\n else\n assign_user_psg \"$user_id\" \"$username\" || true\n fi\n done\n }\n\n # Assign current user\n if [[ -n \"$CURRENT_USER\" ]]; then\n USER_ID=$(sf data query -q \"SELECT Id FROM User WHERE Username = '${CURRENT_USER_SAFE}'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n if [[ -n \"$USER_ID\" ]]; then\n assign_user_psg \"$USER_ID\" \"$CURRENT_USER\" || true\n fi\n fi\n\n # Explicit --users list (works in both interactive and non-interactive runs).\n if [[ -n \"$USERS_LIST\" ]]; then\n log_info \"Assigning SalesManagementUserPsg to --users: $USERS_LIST\"\n assign_user_list \"$USERS_LIST\"\n fi\n\n # Interactive: ask for more users. Non-interactive runs NEVER prompt and NEVER\n # bulk-assign — the only additional grantees come from the explicit --users flag.\n if [[ \"$NON_INTERACTIVE\" != \"true\" ]]; then\n echo \"\"\n echo \" Assign SalesManagementUserPsg to additional users?\"\n read -rp \" Usernames (comma-separated, or Enter to skip): \" EXTRA_USERS\n\n if [[ -n \"$EXTRA_USERS\" ]]; then\n assign_user_list \"$EXTRA_USERS\"\n fi\n fi\nfi\n\n# ============================================================\n# Phase 8: Final Verification\n# ============================================================\nsection \"Phase 8: Final Verification\"\n\nif [[ -f \"$SCRIPT_DIR/verify-all.sh\" ]]; then\n # verify-all.sh is a DIAGNOSTIC: it exits nonzero when it reports any FAIL\n # (e.g. stage descriptions absent on a NextStep-only run). That is informational\n # status, NOT a setup failure — swallow it with `|| true` so `set -e` does not\n # abort here and skip the Configuration Summary table below. Capture the exit so\n # the summary can note that verification flagged issues to review.\n VERIFY_EXIT=0\n bash \"$SCRIPT_DIR/verify-all.sh\" \"$ORG_ALIAS\" || VERIFY_EXIT=$?\n if [[ \"$VERIFY_EXIT\" -ne 0 ]]; then\n log_warn \"verify-all.sh reported issues (exit $VERIFY_EXIT) — review its output above; setup summary follows.\"\n fi\nelse\n log_warn \"verify-all.sh not found. Run manually to confirm setup.\"\nfi\n\n# ============================================================\n# Phase 8.5: Stage Descriptions Summary Table\n# ============================================================\n# Display all persisted OpptStageDescription records after setup\n\nif [[ \"${STAGE_DESCRIPTIONS_BLOCKED:-}\" != \"true\" ]]; then\n # Query all stage descriptions (global-per-stage; no OpportunityRecordTypeId column)\n SUMMARY_QUERY=$(sf data query --query \"SELECT Id, MasterLabel, OpportunityStageApiName, Description FROM OpptStageDescription ORDER BY MasterLabel\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null || echo '{\"status\":1}')\n\n SUMMARY_COUNT=$(echo \"$SUMMARY_QUERY\" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\n SUMMARY_COUNT=\"${SUMMARY_COUNT:-0}\"\n\n if [[ \"$SUMMARY_COUNT\" -gt 0 ]]; then\n echo \"\"\n section \"Stage Descriptions Summary\"\n echo \"\"\n echo \"Persisted OpptStageDescription records: $SUMMARY_COUNT\"\n echo \"\"\n\n # Determine methodology from descriptions (detect MEDDIC/BANT/SPICED keywords)\n FIRST_DESC=$(echo \"$SUMMARY_QUERY\" | jq -r '.result.records[0].Description // \"\"' 2>/dev/null)\n METHODOLOGY=\"Unknown\"\n if echo \"$FIRST_DESC\" | grep -qi \"MEDDIC\\|Metrics.*Economic Buyer\"; then\n METHODOLOGY=\"MEDDIC\"\n elif echo \"$FIRST_DESC\" | grep -qi \"BANT\\|Budget.*Authority\"; then\n METHODOLOGY=\"BANT\"\n elif echo \"$FIRST_DESC\" | grep -qi \"SPICED\\|Situation.*Pain.*Impact\"; then\n METHODOLOGY=\"SPICED\"\n fi\n\n # Print table header — descriptions are global-per-stage, no record-type column\n printf \"┌──────────────────────────┬──────────────────────────────────────────────────────────────┬────────────┐\\n\"\n printf \"│ %-24s │ %-60s │ %-10s │\\n\" \"Stage Name\" \"Description (first 60 chars)\" \"Methodology\"\n printf \"├──────────────────────────┼──────────────────────────────────────────────────────────────┼────────────┤\\n\"\n\n # Print table rows\n while IFS= read -r DESC_JSON; do\n [[ -z \"$DESC_JSON\" ]] && continue\n\n STAGE_NAME=$(echo \"$DESC_JSON\" | jq -r '.MasterLabel // .OpportunityStageApiName // \"N/A\"' 2>/dev/null)\n STAGE_DESC=$(echo \"$DESC_JSON\" | jq -r '.Description // \"N/A\"' 2>/dev/null)\n\n # Truncate description to 60 characters\n if [[ ${#STAGE_DESC} -gt 60 ]]; then\n STAGE_DESC_TRUNC=\"${STAGE_DESC:0:57}...\"\n else\n STAGE_DESC_TRUNC=\"$STAGE_DESC\"\n fi\n\n # Print row\n printf \"│ %-24s │ %-60s │ %-10s │\\n\" \"$STAGE_NAME\" \"$STAGE_DESC_TRUNC\" \"$METHODOLOGY\"\n\n done \u003c \u003c(echo \"$SUMMARY_QUERY\" | jq -c '.result.records[]?' 2>/dev/null)\n\n # Print table footer\n printf \"└──────────────────────────┴──────────────────────────────────────────────────────────────┴────────────┘\\n\"\n echo \"\"\n echo \"Note: Full descriptions are stored in Salesforce. This table shows first 60 characters only.\"\n echo \"\"\n fi\nfi\n\necho \"\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" PIPELINE MANAGEMENT SETUP - CONFIGURATION SUMMARY \"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\n\n# Query current org configuration state\nlog_info \"Querying final org configuration state...\"\necho \"\"\n\n# Build configuration summary\nprintf \"┌──────────────────────────────────────────┬─────────────────────┬─────────────────────┬────────────────┐\\n\"\nprintf \"│ %-40s │ %-19s │ %-19s │ %-14s │\\n\" \"Configuration Item\" \"Before Setup\" \"After Setup\" \"Status\"\nprintf \"├──────────────────────────────────────────┼─────────────────────┼─────────────────────┼────────────────┤\\n\"\n\n# Configuration Summary rows compare BEFORE_* (snapshot captured pre-Phase-3)\n# vs. AFTER_* (live post-run query). Emit UNCHANGED when they match so\n# idempotent reruns don't misreport as CHANGED. See W-23277501.\n\n# Cache one SOAP read so both settings come from the same document\nSOAP_SETTINGS_AFTER=$(soap_read \"SalesDealAgentSettings\" \"SalesDealAgent\" 2>/dev/null || echo \"\")\n\n# --- Pipeline Management ---\nAFTER_PM=$(soap_extract_bool \"$SOAP_SETTINGS_AFTER\" \"enableDealAgent\" 2>/dev/null || echo \"false\")\nPM_BEFORE_LABEL=$([ \"$BEFORE_PM\" == \"true\" ] && echo \"✅ Enabled\" || echo \"❌ Not Enabled\")\nPM_AFTER_LABEL=$([ \"$AFTER_PM\" == \"true\" ] && echo \"✅ Enabled\" || echo \"❌ Not Enabled\")\nif [[ \"$BEFORE_PM\" == \"$AFTER_PM\" ]]; then\n PM_STATUS_COL=$([ \"$AFTER_PM\" == \"true\" ] && echo \"✅ UNCHANGED\" || echo \"⚠️ NOT ENABLED\")\nelse\n PM_STATUS_COL=$([ \"$AFTER_PM\" == \"true\" ] && echo \"✅ CHANGED\" || echo \"⚠️ REVERTED\")\nfi\nprintf \"│ %-40s │ %-19s │ %-19s │ %-14s │\\n\" \"Pipeline Management\" \"$PM_BEFORE_LABEL\" \"$PM_AFTER_LABEL\" \"$PM_STATUS_COL\"\n\n# --- Autonomous Field Updates ---\nAFTER_AUTO=$(soap_extract_bool \"$SOAP_SETTINGS_AFTER\" \"enableDealAgentAutoApproveAllTasks\" 2>/dev/null || echo \"false\")\nAUTO_BEFORE_LABEL=$([ \"$BEFORE_AUTO\" == \"true\" ] && echo \"✅ Enabled\" || echo \"❌ Disabled\")\nAUTO_AFTER_LABEL=$([ \"$AFTER_AUTO\" == \"true\" ] && echo \"✅ Enabled\" || echo \"❌ Disabled\")\nif [[ \"$BEFORE_AUTO\" == \"$AFTER_AUTO\" ]]; then\n AUTO_STATUS_COL=$([ \"$AFTER_AUTO\" == \"true\" ] && echo \"✅ UNCHANGED\" || echo \"⚠️ NOT ENABLED\")\nelse\n AUTO_STATUS_COL=$([ \"$AFTER_AUTO\" == \"true\" ] && echo \"✅ CHANGED\" || echo \"⚠️ REVERTED\")\nfi\nprintf \"│ %-40s │ %-19s │ %-19s │ %-14s │\\n\" \"Autonomous Field Updates\" \"$AUTO_BEFORE_LABEL\" \"$AUTO_AFTER_LABEL\" \"$AUTO_STATUS_COL\"\n\n# --- Agent User ---\nAFTER_AGENT_USER=$(sf data query --query \"SELECT Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Username // \"Not Created\"' 2>/dev/null || echo \"Not Created\")\nAFTER_AGENT_USER=$(sanitize_display \"$AFTER_AGENT_USER\" 60)\nAGENT_BEFORE_LABEL=$([ \"$BEFORE_AGENT_USER\" != \"Not Created\" ] && echo \"✅ Created\" || echo \"Not Created\")\nAGENT_AFTER_LABEL=$([ \"$AFTER_AGENT_USER\" != \"Not Created\" ] && echo \"✅ Created\" || echo \"❌ Not Created\")\nif [[ \"$BEFORE_AGENT_USER\" == \"$AFTER_AGENT_USER\" ]]; then\n AGENT_STATUS_COL=$([ \"$AFTER_AGENT_USER\" != \"Not Created\" ] && echo \"✅ UNCHANGED\" || echo \"⚠️ NOT CREATED\")\nelse\n AGENT_STATUS_COL=$([ \"$AFTER_AGENT_USER\" != \"Not Created\" ] && echo \"✅ CREATED\" || echo \"⚠️ REMOVED\")\nfi\nprintf \"│ %-40s │ %-19s │ %-19s │ %-14s │\\n\" \"Agent User (salesmanagementagent...)\" \"$AGENT_BEFORE_LABEL\" \"$AGENT_AFTER_LABEL\" \"$AGENT_STATUS_COL\"\n\n# --- PSGs ---\nAFTER_PSG_COUNT=$(sf data query --query \"SELECT Id FROM PermissionSetGroup WHERE DeveloperName IN ('SalesManagementUserPsg','SalesManagementAgentUserPsg')\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || echo \"0\")\nAFTER_PSG_COUNT=$((${AFTER_PSG_COUNT:-0} + 0))\nif [[ \"$BEFORE_PSG_COUNT\" == \"$AFTER_PSG_COUNT\" ]]; then\n PSG_STATUS_COL=$([ \"$AFTER_PSG_COUNT\" -ge 2 ] && echo \"✅ UNCHANGED\" || echo \"⚠️ PARTIAL\")\nelse\n PSG_STATUS_COL=$([ \"$AFTER_PSG_COUNT\" -ge 2 ] && echo \"✅ CREATED\" || echo \"⚠️ PARTIAL\")\nfi\nprintf \"│ %-40s │ %-19s │ %-19s │ %-14s │\\n\" \"Permission Set Groups (license)\" \"$BEFORE_PSG_COUNT PSGs\" \"$AFTER_PSG_COUNT PSGs\" \"$PSG_STATUS_COL\"\n\n# --- Schedule Flow ---\n# Resolve via the shared SourceTemplateId-first resolver (#6) so a freshly deployed\n# flow is not misreported as \"Not Found\" by a fuzzy label query.\nAFTER_FLOW_ACTIVE=$(schedule_flow_active \"$ORG_ALIAS\")\ncase \"$BEFORE_FLOW_ACTIVE\" in\n true) FLOW_BEFORE_LABEL=\"✅ Active\" ;;\n false) FLOW_BEFORE_LABEL=\"⚠️ Draft\" ;;\n *) FLOW_BEFORE_LABEL=\"Not Deployed\" ;;\nesac\ncase \"$AFTER_FLOW_ACTIVE\" in\n true) FLOW_AFTER_LABEL=\"✅ Active\" ;;\n false) FLOW_AFTER_LABEL=\"⚠️ Draft\" ;;\n *) FLOW_AFTER_LABEL=\"❌ Not Found\" ;;\nesac\nif [[ \"$BEFORE_FLOW_ACTIVE\" == \"$AFTER_FLOW_ACTIVE\" ]]; then\n FLOW_STATUS_COL=$([ \"$AFTER_FLOW_ACTIVE\" == \"true\" ] && echo \"✅ UNCHANGED\" || echo \"⚠️ PARTIAL\")\nelse\n FLOW_STATUS_COL=$([ \"$AFTER_FLOW_ACTIVE\" == \"true\" ] && echo \"✅ CHANGED\" || echo \"⚠️ PARTIAL\")\nfi\nprintf \"│ %-40s │ %-19s │ %-19s │ %-14s │\\n\" \"Schedule Flow (Suggestions)\" \"$FLOW_BEFORE_LABEL\" \"$FLOW_AFTER_LABEL\" \"$FLOW_STATUS_COL\"\n\n# --- Stage Descriptions ---\n# Same INVALID_TYPE / \"0\\n0\" arithmetic-crash guard as the BEFORE snapshot above:\n# if OpptStageDescription is not yet queryable (PM enable still settling), `|| true`\n# lets jq's `// 0` default stand instead of appending a second line.\nAFTER_STAGE_DESC_COUNT=$(sf data query --query \"SELECT Id FROM OpptStageDescription\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || true)\nAFTER_STAGE_DESC_COUNT=$((${AFTER_STAGE_DESC_COUNT:-0} + 0))\nif [[ \"$BEFORE_STAGE_DESC_COUNT\" == \"$AFTER_STAGE_DESC_COUNT\" ]]; then\n DESC_STATUS_COL=$([ \"$AFTER_STAGE_DESC_COUNT\" -gt 0 ] && echo \"✅ UNCHANGED\" || echo \"⚠️ NONE\")\nelse\n DESC_STATUS_COL=$([ \"$AFTER_STAGE_DESC_COUNT\" -gt 0 ] && echo \"✅ CREATED\" || echo \"⚠️ NONE\")\nfi\nprintf \"│ %-40s │ %-19s │ %-19s │ %-14s │\\n\" \"Opportunity Stage Descriptions\" \"$BEFORE_STAGE_DESC_COUNT descriptions\" \"$AFTER_STAGE_DESC_COUNT descriptions\" \"$DESC_STATUS_COL\"\n\nprintf \"└──────────────────────────────────────────┴─────────────────────┴─────────────────────┴────────────────┘\\n\"\necho \"\"\n\n# Feature Dependency Sequence\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" FEATURE DEPENDENCY SEQUENCE \"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\necho \" Phase 1: Prerequisites\"\necho \" ├─ SOAP API Enabled\"\necho \" ├─ User: 'Use Any API Auth' Permission\"\necho \" ├─ Einstein GenAI Enabled\"\necho \" └─ Enhanced Notes Enabled\"\necho \" │\"\necho \" ▼\"\necho \" Phase 2: Prerequisite Objects\"\necho \" ├─ Enhanced Notes Configuration\"\necho \" └─ Opportunity Splits Configuration\"\necho \" │\"\necho \" ▼\"\necho \" Phase 3: Pipeline Management Enablement\"\necho \" └─ SalesDealAgentSettings.enableDealAgent = true\"\necho \" │\"\necho \" ▼\"\necho \" Phase 3.5: Autonomous Field Updates (opt-in via --autonomous)\"\necho \" ├─ Requires: Phase 3 Complete\"\necho \" └─ Sets: enableDealAgentAutoApproveAllTasks = true (only when opted in; else suggestions-only)\"\necho \" │\"\necho \" ▼\"\necho \" Phase 4: Provisioning (async)\"\necho \" ├─ Agent User: salesmanagementagentuser@...\"\necho \" └─ PSGs: SalesManagementUserPsg, SalesManagementAgentUserPsg\"\necho \" │\"\necho \" ▼\"\necho \" Phase 4c.5: Opportunity Stage Descriptions (only if StageName selected; opt-in via --create-stage-descriptions)\"\necho \" ├─ Requires: Phase 4 Complete (OpptStageDescription API accessible)\"\necho \" ├─ Runs BEFORE the StageName prompt is deployed/tested (it is the grounding)\"\necho \" ├─ Standard Stages: Methodology-specific descriptions\"\necho \" └─ Custom Stages: Context-aware recommendations (user confirmation)\"\necho \" │\"\necho \" ▼\"\necho \" Phase 5: Schedule-Triggered Flow\"\necho \" └─ Deploy & Activate: Process_Field_Update_Suggestions\"\necho \" │\"\necho \" ▼\"\necho \" Phase 7: User Permissions\"\necho \" └─ Assign: SalesManagementUserPsg to users\"\necho \"\"\n\n# Issues Encountered\nif [[ ${#SETUP_ISSUES[@]} -gt 0 ]]; then\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \" ISSUES ENCOUNTERED \"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \"\"\n printf \"┌──────────┬────────────────────────────────────────────────────────────┬──────────────────────────────────┐\\n\"\n printf \"│ %-8s │ %-58s │ %-32s │\\n\" \"Severity\" \"Issue Description\" \"Resolution\"\n printf \"├──────────┼────────────────────────────────────────────────────────────┼──────────────────────────────────┤\\n\"\n\n for ISSUE in \"${SETUP_ISSUES[@]}\"; do\n IFS='|' read -r SEVERITY DESC RESOLUTION \u003c\u003c\u003c \"$ISSUE\"\n printf \"│ %-8s │ %-58s │ %-32s │\\n\" \"$SEVERITY\" \"${DESC:0:58}\" \"${RESOLUTION:0:32}\"\n done\n\n printf \"└──────────┴────────────────────────────────────────────────────────────┴──────────────────────────────────┘\\n\"\n echo \"\"\nfi\n\n# A tracked \"🔴 BLOCKER\" means setup did NOT complete — do not report success.\n# (track_issue only records; some blocking paths exit inline, but a BLOCKER can also\n# be recorded without an inline exit, e.g. bot-activation failure at Phase 4b. This\n# gate ensures such a run reports SETUP INCOMPLETE and exits non-zero.)\n# NOTE: only \"🔴 BLOCKER\" is terminal. \"🔴 ERROR\" is provisional — it marks a state\n# the script then attempts to recover from (e.g. SOAP re-provisioning toggle); a\n# recovered run must still be allowed to report SETUP COMPLETE.\nBLOCKER_COUNT=0\nfor ISSUE in \"${SETUP_ISSUES[@]}\"; do\n case \"$ISSUE\" in\n \"🔴 BLOCKER\"*) BLOCKER_COUNT=$((BLOCKER_COUNT + 1)) ;;\n esac\ndone\n\nif [[ \"$BLOCKER_COUNT\" -gt 0 ]]; then\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \" SETUP INCOMPLETE \"\n echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n echo \"\"\n echo \" $BLOCKER_COUNT blocking issue(s) (🔴) were recorded above. Setup did not finish successfully.\"\n echo \" Resolve the issue(s) in the table and re-run: bash setup-all.sh $ORG_ALIAS\"\n echo \"\"\n exit 1\nfi\n\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" SETUP COMPLETE \"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\necho \"Next steps:\"\necho \" - If stage descriptions show FAIL, re-run in 5 minutes (provisioning)\"\necho \" - Add notes/emails to opportunities to give the agent data\"\necho \" - Wait for daily schedule flow (or check Setup → Flows → run history)\"\necho \" - Agent Analytics: Setup → Einstein Feedback and Monitoring\"\necho \"\"\necho \"Setup URL: $(sanitize_display \"${INSTANCE_URL}/lightning/setup/PipelineManagement/home\" 80)\"\necho \"\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":150088,"content_sha256":"c4b14a6dc39a54c0b0ef8ed21d2121ee97ec42d549939f189646c7d210617532"},{"filename":"scripts/shared/agent-bundle-publish.sh","content":"#!/bin/bash\n# Shared library for Sales Agent authoring bundle publish and activation\n#\n# Functions:\n# publish_and_activate_agent \u003corg-alias> \u003con-error-behavior> [output-style]\n#\n# Parameters:\n# org-alias: Salesforce org alias\n# on-error-behavior: \"exit\" (hard fail on error) or \"return\" (return non-zero)\n# output-style: \"plain\" (default, plain echo) or \"structured\" (log_pass/log_info style)\n#\n# Returns:\n# 0 on success\n# 1 on failure\n#\n# Environment:\n# SCRIPT_DIR must be set (parent script's directory)\n\n# Source agent-detection helpers (pm_bot_latest_version, etc.)\n# Use a local resolution that doesn't depend on the caller's SCRIPT_DIR.\n# shellcheck source=./agent-detection.sh\nsource \"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agent-detection.sh\"\n\npublish_and_activate_agent() {\n local ORG_ALIAS=\"$1\"\n local ON_ERROR=\"${2:-exit}\"\n local OUTPUT_STYLE=\"${3:-plain}\"\n\n # Clear exported state from any prior call to prevent stale values on failure\n unset AGENT_VERSION\n unset AGENT_API_NAME\n\n local AGENT_BUNDLE=\"$SCRIPT_DIR/../assets/sales_management_agent.agent\"\n local BUNDLE_META=\"$SCRIPT_DIR/../assets/sales_management_agent.bundle-meta.xml\"\n\n # Output helper functions based on style\n local msg_header msg_pass msg_error msg_info\n if [[ \"$OUTPUT_STYLE\" == \"structured\" ]]; then\n msg_header() { log_info \"$1\"; }\n msg_pass() { log_pass \"$1\"; }\n msg_error() { echo \" ERROR: $1\"; }\n msg_info() { echo \" $1\"; }\n else\n msg_header() { echo \"=== $1 ===\"; }\n msg_pass() { echo \" ✓ $1\"; }\n msg_error() { echo \" ERROR: $1\"; }\n msg_info() { echo \" $1\"; }\n fi\n\n # Check for bundle assets\n if [[ ! -f \"$AGENT_BUNDLE\" || ! -f \"$BUNDLE_META\" ]]; then\n msg_error \"Agent bundle assets not found at: $AGENT_BUNDLE\"\n if [[ \"$ON_ERROR\" == \"exit\" ]]; then\n echo \"\"\n echo \"SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction.\"\n exit 1\n fi\n return 1\n fi\n\n msg_header \"Creating Sales Agent via authoring bundle\"\n\n # Create temp directory for bundle deployment\n local TEMP_DIR\n TEMP_DIR=$(mktemp -d -t sales-agent-bundle.XXXXXX)\n # Use EXIT trap so it fires even on explicit exit calls\n trap 'rm -rf \"$TEMP_DIR\"' EXIT\n\n # Create sfdx-project.json in temp dir\n cat > \"$TEMP_DIR/sfdx-project.json\" \u003c\u003c 'SFDX'\n{\"packageDirectories\": [{\"path\": \"force-app\", \"default\": true}], \"sourceApiVersion\": \"67.0\"}\nSFDX\n\n # Copy authoring bundle to temp deployment directory\n mkdir -p \"$TEMP_DIR/force-app/main/default/aiAuthoringBundles/SalesAgent\"\n if ! cp \"$AGENT_BUNDLE\" \"$TEMP_DIR/force-app/main/default/aiAuthoringBundles/SalesAgent/SalesAgent.agent\"; then\n msg_error \"Failed to stage bundle agent file\"\n rm -rf \"$TEMP_DIR\"\n trap - EXIT\n if [[ \"$ON_ERROR\" == \"exit\" ]]; then\n echo \"\"\n echo \"SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction.\"\n exit 1\n fi\n return 1\n fi\n if ! cp \"$BUNDLE_META\" \"$TEMP_DIR/force-app/main/default/aiAuthoringBundles/SalesAgent/SalesAgent.bundle-meta.xml\"; then\n msg_error \"Failed to stage bundle metadata file\"\n rm -rf \"$TEMP_DIR\"\n trap - EXIT\n if [[ \"$ON_ERROR\" == \"exit\" ]]; then\n echo \"\"\n echo \"SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction.\"\n exit 1\n fi\n return 1\n fi\n\n # Publish authoring bundle from temp directory\n msg_info \"Publishing Sales Agent authoring bundle...\"\n local PUBLISH_RESULT\n PUBLISH_RESULT=$(cd \"$TEMP_DIR\" && sf agent publish authoring-bundle --json --api-name SalesAgent --target-org \"$ORG_ALIAS\" 2>/dev/null || echo '{\"status\":1}')\n\n if ! echo \"$PUBLISH_RESULT\" | jq -e '.status == 0' >/dev/null 2>&1; then\n local PUBLISH_ERR\n PUBLISH_ERR=$(echo \"$PUBLISH_RESULT\" | jq -r '.message // .result.message // .result.errors[0].message // \"unknown\"' 2>/dev/null || echo \"unknown\")\n msg_error \"Authoring bundle publish failed: $PUBLISH_ERR\"\n rm -rf \"$TEMP_DIR\"\n trap - EXIT\n if [[ \"$ON_ERROR\" == \"exit\" ]]; then\n echo \"\"\n echo \"SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction.\"\n exit 1\n fi\n return 1\n fi\n\n msg_pass \"Sales Agent published successfully\"\n\n # Query for the latest BotVersion with retry (async provisioning).\n # Match the bot by AgentTemplate (the same field the scheduled flow uses),\n # not by hardcoded DeveloperName — the bundle may publish under a different\n # local name in some orgs.\n msg_info \"Querying for latest version...\"\n local LATEST_VERSION=\"\"\n for attempt in {1..3}; do\n LATEST_VERSION=$(pm_bot_latest_version \"$ORG_ALIAS\")\n\n if [[ -n \"$LATEST_VERSION\" ]]; then\n break\n fi\n\n if [[ $attempt -lt 3 ]]; then\n echo \" Version not yet available, retrying in 5s... (attempt $attempt/3)\"\n sleep 5\n fi\n done\n\n if [[ -z \"$LATEST_VERSION\" ]]; then\n msg_error \"Could not query BotVersion after publish. BotDefinition may still be provisioning. Wait 30s and check Setup → Agents.\"\n rm -rf \"$TEMP_DIR\"\n trap - EXIT\n if [[ \"$ON_ERROR\" == \"exit\" ]]; then\n echo \"\"\n echo \"SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction.\"\n exit 1\n fi\n return 1\n fi\n\n # Resolve the actual published DeveloperName from AgentTemplate so we\n # activate whatever name the bundle landed under (defaults to \"SalesAgent\"\n # but the org may already have an upgraded NGA bot with a different name).\n AGENT_API_NAME=$(pm_bot_developer_name \"$ORG_ALIAS\")\n if [[ -z \"$AGENT_API_NAME\" ]]; then\n AGENT_API_NAME=\"SalesAgent\"\n fi\n\n # Activate the agent with dynamic version\n msg_info \"Activating $AGENT_API_NAME version $LATEST_VERSION...\"\n local ACTIVATE_RESULT\n ACTIVATE_RESULT=$(sf agent activate --api-name \"$AGENT_API_NAME\" --version \"$LATEST_VERSION\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null || echo '{\"status\":1}')\n\n if ! echo \"$ACTIVATE_RESULT\" | jq -e '.status == 0' >/dev/null 2>&1; then\n local ACTIVATE_ERR\n ACTIVATE_ERR=$(echo \"$ACTIVATE_RESULT\" | jq -r '.message // .result.message // .result.errors[0].message // \"unknown\"' 2>/dev/null || echo \"unknown\")\n msg_error \"Agent activation failed: $ACTIVATE_ERR. The agent was published but activation failed.\"\n rm -rf \"$TEMP_DIR\"\n trap - EXIT\n if [[ \"$ON_ERROR\" == \"exit\" ]]; then\n echo \"\"\n echo \"SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction.\"\n exit 1\n fi\n return 1\n fi\n\n msg_pass \"$AGENT_API_NAME activated successfully (version $LATEST_VERSION)\"\n\n # Export version and resolved api-name for caller to use\n export AGENT_VERSION=\"$LATEST_VERSION\"\n export AGENT_API_NAME\n\n # Clean up temp directory before returning (EXIT trap only fires on exit, not return)\n rm -rf \"$TEMP_DIR\"\n trap - EXIT # Remove the trap since we've already cleaned up\n\n return 0\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6887,"content_sha256":"f8ceef5d9eaabde4076ca6acf19a271cfe4c15c8011d77b0a959c96a284f9f75"},{"filename":"scripts/shared/agent-detection.sh","content":"#!/bin/bash\n# Shared library for Pipeline Management agent detection.\n#\n# The Pipeline Management scheduled flow (sales_pipe_mgmt__OppSuggGenSchFlow)\n# finds the Sales Agent bot by querying BotDefinition.AgentTemplate, NOT by\n# a hardcoded DeveloperName. Setup/verify scripts must match the same logic\n# so they detect the agent regardless of what local name (SalesAgent,\n# Sale_Agent, etc.) it was published with.\n#\n# The product currently supports two agent templates:\n# - SalesMgmt__SalesAgent (legacy)\n# - SalesMgmt__NGASalesAgent (upgraded NGA, used by our authoring bundle)\n#\n# Both flows (Test.flow, Process_Field_Update_Suggestions.flow) check\n# AgentTemplate. We do the same here.\n\n# Comma-separated AgentTemplate API names that identify a Pipeline Management\n# Sales Agent. Quoted for SOQL IN(...) usage.\nPM_AGENT_TEMPLATES_SOQL=\"'SalesMgmt__NGASalesAgent','SalesMgmt__SalesAgent'\"\n\n# Activation state lives on BotVersion.Status (Active|Inactive), not on\n# BotDefinition (which only has IsDeleted).\n#\n# DEPRECATED: This correlated subquery hangs 60+ seconds on some orgs and must\n# NOT be embedded in detection queries (removed in de77d520). Retained only so\n# any lingering external caller still resolves the symbol; callers that need to\n# know whether a bot is active should run a SEPARATE flat query on BotVersion\n# (e.g. SELECT COUNT() FROM BotVersion WHERE BotDefinitionId='...' AND\n# Status='Active') instead of splicing this into a BotDefinition WHERE clause.\nPM_ACTIVE_BOT_FILTER=\"Id IN (SELECT BotDefinitionId FROM BotVersion WHERE Status = 'Active')\"\n\n# Print the DeveloperName of the Pipeline Management BotDefinition (if any),\n# detected by AgentTemplate. Empty string when not found.\n#\n# NOTE: Does NOT filter by Active status to avoid slow/hanging subqueries.\n# Callers should verify Active status separately if needed.\n#\n# Usage: name=$(pm_bot_developer_name \"$ORG_ALIAS\")\npm_bot_developer_name() {\n local org_alias=\"$1\"\n sf data query \\\n --query \"SELECT DeveloperName FROM BotDefinition WHERE AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) ORDER BY LastModifiedDate DESC LIMIT 1\" \\\n --target-org \"$org_alias\" \\\n --json 2>/dev/null \\\n | jq -r '.result.records[0].DeveloperName // empty' 2>/dev/null \\\n || echo \"\"\n}\n\n# Print the total number of Pipeline Management bots (BotDefinition rows\n# matching PM AgentTemplate). Returns 0 if none, on error, or if the result\n# can't be parsed.\n#\n# NOTE: Does NOT filter by Active status to avoid slow/hanging subqueries.\n#\n# Usage: count=$(pm_bot_count \"$ORG_ALIAS\")\npm_bot_count() {\n local org_alias=\"$1\"\n local count\n count=$(sf data query \\\n --query \"SELECT Id FROM BotDefinition WHERE AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL})\" \\\n --target-org \"$org_alias\" \\\n --json 2>/dev/null \\\n | jq -r '.result.totalSize // 0' 2>/dev/null \\\n | head -1 | tr -cd '0-9')\n echo \"${count:-0}\"\n}\n\n# Print the AgentTemplate API name of the active Pipeline Management bot.\n# Used to keep the deployed flow's SalesManagementAgentTemplate constant\n# in sync with the actual bot on the org (legacy SalesMgmt__SalesAgent vs\n# upgraded SalesMgmt__NGASalesAgent), so the flow's BotDefinition lookup\n# will match at runtime.\n#\n# Selection rule when both templates have a bot: prefer NGA (the forward\n# direction; our authoring bundle publishes NGA). SOQL does not support CASE\n# in ORDER BY, so we query NGA first and fall back to legacy.\n#\n# NOTE: Does NOT filter by Active status to avoid slow/hanging subqueries.\n# Callers should verify Active status separately if needed.\n#\n# Exit codes distinguish failure modes for callers:\n# 0 + stdout populated -> bot found, prints template name\n# 1 + empty stdout -> no PM bot exists on the org\n# 2 + empty stdout -> SOQL/auth/network failure (transient)\n#\n# Usage:\n# if tpl=$(pm_bot_agent_template \"$ORG_ALIAS\"); then ... fi\n# rc=$? # 0=ok, 1=no-bot, 2=query-failed\npm_bot_agent_template() {\n local org_alias=\"$1\"\n local raw rc tpl\n\n raw=$(sf data query \\\n --query \"SELECT AgentTemplate FROM BotDefinition WHERE AgentTemplate = 'SalesMgmt__NGASalesAgent' ORDER BY LastModifiedDate DESC LIMIT 1\" \\\n --target-org \"$org_alias\" \\\n --json 2>/dev/null)\n rc=$?\n if [[ $rc -ne 0 || -z \"$raw\" ]]; then\n return 2\n fi\n tpl=$(echo \"$raw\" | jq -r '.result.records[0].AgentTemplate // empty' 2>/dev/null)\n if [[ -n \"$tpl\" ]]; then\n echo \"$tpl\"\n return 0\n fi\n\n raw=$(sf data query \\\n --query \"SELECT AgentTemplate FROM BotDefinition WHERE AgentTemplate = 'SalesMgmt__SalesAgent' ORDER BY LastModifiedDate DESC LIMIT 1\" \\\n --target-org \"$org_alias\" \\\n --json 2>/dev/null)\n rc=$?\n if [[ $rc -ne 0 || -z \"$raw\" ]]; then\n return 2\n fi\n tpl=$(echo \"$raw\" | jq -r '.result.records[0].AgentTemplate // empty' 2>/dev/null)\n if [[ -n \"$tpl\" ]]; then\n echo \"$tpl\"\n return 0\n fi\n return 1\n}\n\n# Print the number of agent users that HOLD SalesManagementAgentUserPsg.\n# This — not PSG existence — is a real \"PM setup has run\" signal, because the\n# two PM PSGs (SalesManagementUserPsg / SalesManagementAgentUserPsg) ship with\n# the Agentforce-for-Sales license and exist even in a never-configured org.\n# Returns 0 if none / on error.\n#\n# Usage: n=$(pm_agent_user_holds_psg \"$ORG_ALIAS\")\npm_agent_user_holds_psg() {\n local org_alias=\"$1\" count\n count=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetAssignment WHERE Assignee.Username LIKE '%salesmanagementagentuser%' AND PermissionSetGroup.DeveloperName='SalesManagementAgentUserPsg'\" \\\n --target-org \"$org_alias\" --json 2>/dev/null \\\n | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9')\n echo \"${count:-0}\"\n}\n\n# Print the number of scheduled flows cloned from the PM template\n# (sales_pipe_mgmt__OppSuggGenSchFlow). A real \"PM setup has run\" signal.\n# Returns 0 if none / on error.\n#\n# Usage: n=$(pm_flow_clone_exists \"$ORG_ALIAS\")\npm_flow_clone_exists() {\n local org_alias=\"$1\" count\n count=$(sf data query \\\n --query \"SELECT Id FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false\" \\\n --target-org \"$org_alias\" --json 2>/dev/null \\\n | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9')\n echo \"${count:-0}\"\n}\n\n# Print the latest BotVersion VersionNumber for the PM bot (any status). Empty\n# if no version exists. Note: This returns the most recent version regardless\n# of activation status, which is necessary for the publish-then-activate flow\n# where the freshly published version is still Inactive.\n#\n# Usage: ver=$(pm_bot_latest_version \"$ORG_ALIAS\")\npm_bot_latest_version() {\n local org_alias=\"$1\"\n sf data query \\\n --query \"SELECT VersionNumber FROM BotVersion WHERE BotDefinition.AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) ORDER BY VersionNumber DESC LIMIT 1\" \\\n --target-org \"$org_alias\" \\\n --json 2>/dev/null \\\n | jq -r '.result.records[0].VersionNumber // empty' 2>/dev/null \\\n || echo \"\"\n}\n\n# Print the number of ACTIVE BotVersions for the PM bot. A bot can exist\n# (BotDefinition present) yet have no active version — this is exactly the\n# state auto-provisioning leaves the bot in: the BotVersion lands Inactive and\n# must be activated before the scheduled flow will add the agent user to\n# opportunity teams or Agent Access can be defined. Uses a SEPARATE flat query\n# on BotVersion (never a correlated subquery — see PM_ACTIVE_BOT_FILTER note).\n# Returns 0 if none / on error.\n#\n# Usage: n=$(pm_bot_active_version_count \"$ORG_ALIAS\")\npm_bot_active_version_count() {\n local org_alias=\"$1\" count\n count=$(sf data query \\\n --query \"SELECT Id FROM BotVersion WHERE BotDefinition.AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) AND Status = 'Active'\" \\\n --target-org \"$org_alias\" \\\n --json 2>/dev/null \\\n | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9')\n echo \"${count:-0}\"\n}\n\n# Ensure the PM bot has an ACTIVE BotVersion, activating the latest version if\n# not. Idempotent: a no-op (returns 0) when an active version already exists.\n# Uses `sf agent activate`, matching the activation path in\n# publish_and_activate_agent. Prints progress via plain echo.\n#\n# Returns:\n# 0 - an active version exists (already, or after successful activation)\n# 1 - activation was attempted but failed, or no version exists to activate\n#\n# Usage: activate_pm_bot_if_inactive \"$ORG_ALIAS\"\nactivate_pm_bot_if_inactive() {\n local org_alias=\"$1\"\n\n local active_count\n active_count=$(pm_bot_active_version_count \"$org_alias\")\n if [[ \"$active_count\" -ge 1 ]]; then\n return 0\n fi\n\n local latest_version api_name\n latest_version=$(pm_bot_latest_version \"$org_alias\")\n if [[ -z \"$latest_version\" ]]; then\n echo \" ERROR: BotDefinition exists but has no BotVersion to activate.\"\n return 1\n fi\n\n api_name=$(pm_bot_developer_name \"$org_alias\")\n [[ -z \"$api_name\" ]] && api_name=\"SalesAgent\"\n\n echo \" Activating $api_name version $latest_version (auto-provisioned version was Inactive)...\"\n local activate_result\n activate_result=$(sf agent activate --api-name \"$api_name\" --version \"$latest_version\" --target-org \"$org_alias\" --json 2>/dev/null || echo '{\"status\":1}')\n\n if ! echo \"$activate_result\" | jq -e '.status == 0' >/dev/null 2>&1; then\n local activate_err\n activate_err=$(echo \"$activate_result\" | jq -r '.message // .result.message // .result.errors[0].message // \"unknown\"' 2>/dev/null || echo \"unknown\")\n echo \" ERROR: Agent activation failed: $activate_err\"\n return 1\n fi\n\n # Confirm the activation took effect (async on some orgs).\n local attempt\n for attempt in {1..3}; do\n active_count=$(pm_bot_active_version_count \"$org_alias\")\n [[ \"$active_count\" -ge 1 ]] && return 0\n [[ $attempt -lt 3 ]] && sleep 5\n done\n\n # sf agent activate returned success; treat as activated even if the\n # confirmation query lagged.\n return 0\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":9896,"content_sha256":"18344b8546af14e1509d24eebff8ca9d1b1df3121da18b6ffe7d3ee03681703a"},{"filename":"scripts/shared/flow-builder.sh","content":"#!/bin/bash\n# shared/flow-builder.sh — build the suggestion flow's field collection from a\n# selected set of Opportunity fields.\n#\n# The base flow template (assets/pipeline_management_flow.flow-meta.xml) ships\n# with a hardcoded AddOppFieldsToCollection assignment wiring BOTH NextStep and\n# StageName. Historically setup deployed both and then STRIPPED the unwanted one\n# (see strip_flow_field in setup-all.sh, still used for the repair path). For a\n# greenfield setup we instead build the flow with ONLY the selected fields from\n# the start — no deploy-then-strip round trip.\n#\n# This file only DEFINES a function; it makes no org calls and mutates no files\n# when sourced. build_field_collection reads the base template and writes a NEW\n# output file; it never edits the template in place.\n\n# build_field_collection \u003ccomma-separated-fields> \u003cbase-template> \u003coutput-file>\n#\n# Rewrites the AddOppFieldsToCollection assignment so its \u003cassignmentItems>\n# contain exactly the given fields (in order, de-duplicated by the caller).\n# Uses python3 for a namespace-safe edit — the same requirement as\n# strip_flow_field. Returns non-zero (without writing) if python3 is missing,\n# the template is unreadable, the assignment/fields are absent, or the write\n# fails, so callers can fall back rather than deploy a corrupt/empty flow.\n#\n# A zero-item AddOppFieldsToCollection is INVALID Flow metadata and fails to\n# deploy, so this function refuses to write when the field list is empty.\nbuild_field_collection() {\n local fields=\"$1\" base_template=\"$2\" output_file=\"$3\"\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \" [FAIL] python3 not available — cannot build flow field collection.\" >&2\n return 1\n fi\n if [[ ! -f \"$base_template\" ]]; then\n echo \" [FAIL] Base flow template not found: $base_template\" >&2\n return 1\n fi\n\n python3 - \"$base_template\" \"$output_file\" \"$fields\" \u003c\u003c'PYEOF'\nimport sys, xml.etree.ElementTree as ET\n\nbase_template, output_file, raw_fields = sys.argv[1], sys.argv[2], sys.argv[3]\n\n# Order-preserving de-dup of trimmed, non-empty field names.\nfields = []\nfor f in raw_fields.split(\",\"):\n f = f.strip()\n if f and f not in fields:\n fields.append(f)\n\nif not fields:\n sys.stderr.write(\" [FAIL] No fields provided — refusing to write an empty AddOppFieldsToCollection.\\n\")\n sys.exit(2)\n\nns = \"http://soap.sforce.com/2006/04/metadata\"\nET.register_namespace(\"\", ns)\ntry:\n tree = ET.parse(base_template)\nexcept Exception as e: # noqa: BLE001\n sys.stderr.write(\" [FAIL] Could not parse base flow template: %s\\n\" % e)\n sys.exit(3)\n\nroot = tree.getroot()\nq = lambda t: \"{%s}%s\" % (ns, t)\n\ntarget = None\nfor assign in root.findall(q(\"assignments\")):\n name_elem = assign.find(q(\"name\"))\n if name_elem is not None and name_elem.text == \"AddOppFieldsToCollection\":\n target = assign\n break\n\nif target is None:\n sys.stderr.write(\" [FAIL] AddOppFieldsToCollection assignment not found in template.\\n\")\n sys.exit(4)\n\n# Drop existing assignmentItems, then rebuild from the selected fields.\nfor item in list(target.findall(q(\"assignmentItems\"))):\n target.remove(item)\n\n# The FlowAssignment schema requires every \u003cassignmentItems> to be contiguous\n# and positioned BEFORE \u003cconnector>. A plain ET.SubElement(target, ...) appends\n# to the end of the element — AFTER \u003cconnector> when the template has one — which\n# the Metadata API rejects with \"Element assignmentItems is duplicated at this\n# location\". Find the connector's index and insert each new block just before it\n# (falling back to append when there is no connector).\nconn_idx = next((i for i, c in enumerate(list(target)) if c.tag == q(\"connector\")), None)\ninsert_at = conn_idx\nfor field in fields:\n item = ET.Element(q(\"assignmentItems\"))\n ref = ET.SubElement(item, q(\"assignToReference\"))\n ref.text = \"OpportunityFields\"\n op = ET.SubElement(item, q(\"operator\"))\n op.text = \"Add\"\n val = ET.SubElement(item, q(\"value\"))\n sv = ET.SubElement(val, q(\"stringValue\"))\n sv.text = field\n if insert_at is None:\n target.append(item)\n else:\n target.insert(insert_at, item)\n insert_at += 1\n\ntry:\n tree.write(output_file, xml_declaration=True, encoding=\"UTF-8\")\nexcept Exception as e: # noqa: BLE001\n sys.stderr.write(\" [FAIL] Could not write flow output file: %s\\n\" % e)\n sys.exit(5)\n\nsys.exit(0)\nPYEOF\n}\n\n# strip_flow_field \u003cflow-file> \u003cfield> — remove the matching \u003cassignmentItems>\n# from the AddOppFieldsToCollection assignment of an EXISTING flow file, in place.\n# This is the \"fix method\" used by the REPAIR path (a flow already deployed with\n# fields the admin no longer wants). Greenfield never strips — it builds only the\n# selected fields via build_field_collection. Requires python3 for a\n# namespace-safe edit; if it is unavailable the field is left wired rather than\n# risk corrupting the XML with a regex. Returns 0 if a field was removed, 2 if\n# the field was not present (nothing to strip), non-zero on error.\nstrip_flow_field() {\n local flow_file=\"$1\" field=\"$2\"\n if ! command -v python3 >/dev/null 2>&1; then\n echo \" [WARN] python3 not available — cannot strip '$field' from the flow; leaving it wired.\" >&2\n return 1\n fi\n python3 - \"$flow_file\" \"$field\" \u003c\u003c'PYEOF'\nimport sys, xml.etree.ElementTree as ET\nflow_file, field = sys.argv[1], sys.argv[2]\nns = \"http://soap.sforce.com/2006/04/metadata\"\nET.register_namespace(\"\", ns)\ntree = ET.parse(flow_file)\nroot = tree.getroot()\nq = lambda t: \"{%s}%s\" % (ns, t)\nremoved = 0\nfor assign in root.findall(q(\"assignments\")):\n name = assign.find(q(\"name\"))\n if name is None or name.text != \"AddOppFieldsToCollection\":\n continue\n for item in list(assign.findall(q(\"assignmentItems\"))):\n sv = item.find(\"%s/%s\" % (q(\"value\"), q(\"stringValue\")))\n if sv is not None and sv.text == field:\n assign.remove(item)\n removed += 1\nif removed:\n tree.write(flow_file, xml_declaration=True, encoding=\"UTF-8\")\nsys.exit(0 if removed else 2)\nPYEOF\n}\n\n# add_flow_field \u003cflow-file> \u003cfield> — idempotently ADD a field's \u003cassignmentItems>\n# to the AddOppFieldsToCollection assignment of an EXISTING flow file, in place.\n# The symmetric counterpart to strip_flow_field, used by the REPAIR path to wire a\n# newly-selected field into an already-deployed flow without a full rebuild (a full\n# rebuild would discard the org-specific SalesManagementAgentTemplate constant the\n# retrieved flow carries). No-op if the field is already wired. Returns 0 if added,\n# 2 if it was already present, non-zero on error.\nadd_flow_field() {\n local flow_file=\"$1\" field=\"$2\"\n if ! command -v python3 >/dev/null 2>&1; then\n echo \" [WARN] python3 not available — cannot add '$field' to the flow.\" >&2\n return 1\n fi\n python3 - \"$flow_file\" \"$field\" \u003c\u003c'PYEOF'\nimport sys, xml.etree.ElementTree as ET\nflow_file, field = sys.argv[1], sys.argv[2]\nns = \"http://soap.sforce.com/2006/04/metadata\"\nET.register_namespace(\"\", ns)\ntree = ET.parse(flow_file)\nroot = tree.getroot()\nq = lambda t: \"{%s}%s\" % (ns, t)\ntarget = None\nfor assign in root.findall(q(\"assignments\")):\n name = assign.find(q(\"name\"))\n if name is not None and name.text == \"AddOppFieldsToCollection\":\n target = assign\n break\nif target is None:\n sys.stderr.write(\" [FAIL] AddOppFieldsToCollection assignment not found.\\n\")\n sys.exit(4)\nchildren = list(target)\nlast_ai_idx = -1\nfor idx, item in enumerate(children):\n if item.tag != q(\"assignmentItems\"):\n continue\n last_ai_idx = idx\n sv = item.find(\"%s/%s\" % (q(\"value\"), q(\"stringValue\")))\n if sv is not None and sv.text == field:\n sys.exit(2) # already wired\n\n# Build the new assignmentItems block.\nitem = ET.Element(q(\"assignmentItems\"))\nref = ET.SubElement(item, q(\"assignToReference\")); ref.text = \"OpportunityFields\"\nop = ET.SubElement(item, q(\"operator\")); op.text = \"Add\"\nval = ET.SubElement(item, q(\"value\"))\nsv = ET.SubElement(val, q(\"stringValue\")); sv.text = field\n\n# The FlowAssignment schema requires every \u003cassignmentItems> to be contiguous\n# and positioned before \u003cconnector>. A plain append() lands the new block at\n# the end of the element — AFTER \u003cconnector> when one exists — which the\n# Metadata API rejects with \"Element assignmentItems is duplicated at this\n# location\". Insert immediately after the last existing assignmentItems; if\n# there are none, insert just before \u003cconnector> (else fall back to append).\nif last_ai_idx >= 0:\n target.insert(last_ai_idx + 1, item)\nelse:\n conn_idx = next((i for i, c in enumerate(children) if c.tag == q(\"connector\")), None)\n if conn_idx is not None:\n target.insert(conn_idx, item)\n else:\n target.append(item)\ntree.write(flow_file, xml_declaration=True, encoding=\"UTF-8\")\nsys.exit(0)\nPYEOF\n}\n\n# flow_wired_fields \u003cflow-file> — print the field API names currently wired into\n# AddOppFieldsToCollection, one per line (in document order). Used by the repair\n# path to diff the deployed flow against the admin's selected set.\nflow_wired_fields() {\n local flow_file=\"$1\"\n command -v python3 >/dev/null 2>&1 || return 1\n python3 - \"$flow_file\" \u003c\u003c'PYEOF'\nimport sys, xml.etree.ElementTree as ET\nns = \"http://soap.sforce.com/2006/04/metadata\"\ntree = ET.parse(sys.argv[1])\nq = lambda t: \"{%s}%s\" % (ns, t)\nfor assign in tree.getroot().findall(q(\"assignments\")):\n name = assign.find(q(\"name\"))\n if name is None or name.text != \"AddOppFieldsToCollection\":\n continue\n for item in assign.findall(q(\"assignmentItems\")):\n sv = item.find(\"%s/%s\" % (q(\"value\"), q(\"stringValue\")))\n if sv is not None and sv.text:\n print(sv.text)\nPYEOF\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":9722,"content_sha256":"0e84352c0f215731ef9076168af5c02f3fea9eff1f0d7df66ee01899f0d4ef85"},{"filename":"scripts/shared/psg.sh","content":"#!/bin/bash\n# shared/psg.sh — shared PermissionSetGroup recalculation helper.\n#\n# PermissionSetGroup.Status is a system-computed field (Updated / Updating /\n# Outdated / Failed), NOT client-writable. A component change (inserting a\n# PermissionSetGroupComponent) is what TRIGGERS the async recalculation — there\n# is no CLI recalc verb and no client-writable nudge.\n#\n# Design choice (deliberate): setup does NOT block waiting for recalculation to\n# finish. Firing the trigger is sufficient for the org to converge on its own.\n# The recalc's completion only matters later, when a user tries to VIEW\n# suggestions in the UI or the scheduled flow RUNS a generation — both of which\n# happen well after setup exits. So callers only ever read Status once, for\n# reporting, and never sleep-poll.\n#\n# This file only DEFINES a function; it makes no org calls when sourced.\n\n# psg_status \u003corg-alias> \u003cpsg-developer-name>\n#\n# Reads the named PSG's recalculation Status ONCE (no waiting). Prints one of:\n# Updated | Updating | Outdated | Failed (the raw Status value), or\n# not-found (PSG DeveloperName did not resolve), or\n# unknown (query/parse failed)\n# Always returns 0 — the caller inspects the printed value. Never sleeps.\npsg_status() {\n local org=\"$1\" psg_name=\"$2\"\n local psg_id\n psg_id=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetGroup WHERE DeveloperName = '${psg_name}'\" \\\n --target-org \"$org\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\n if [[ -z \"$psg_id\" ]]; then\n printf 'not-found'\n return 0\n fi\n\n local status\n status=$(sf data query \\\n --query \"SELECT Status FROM PermissionSetGroup WHERE Id = '${psg_id}'\" \\\n --target-org \"$org\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.records[0].Status // empty' 2>/dev/null || echo \"\")\n\n printf '%s' \"${status:-unknown}\"\n return 0\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1995,"content_sha256":"b1bc2ade165aa58f0ad039d1c2a1da197a94cd44fce217c92ee18cf7a3dfba4f"},{"filename":"scripts/shared/soap.sh","content":"#!/bin/bash\n# Shared library for SOAP Metadata API response parsing\n#\n# Functions:\n# redact_token \u003cinput> - Redacts session IDs and Bearer tokens from error messages\n# parse_soap_response \u003cxml> \u003cfield> \u003chttp_status> - Classifies SOAP responses\n#\n# Returns:\n# \"true\" - field is enabled\n# \"false\" - field is explicitly disabled or omitted (setting is off)\n# \"AUTH_ERROR:\u003cfault>\" - authentication/authorization failure\n# \"TYPE_UNAVAILABLE:\u003cfault>\" - org lacks license/edition for this feature\n# \"NETWORK_ERROR\" - network/service error (empty response, 5xx, malformed XML)\n\n# Redact session IDs and Bearer tokens from error messages\n# Args: $1 = input string\n# Returns: string with tokens replaced by [REDACTED]\n#\n# Salesforce session IDs start with the 15/18-char org id (00D...) followed by\n# '!' and a signature segment that contains '.', '=', '-', '+' in addition to\n# alphanumerics. Match greedily up to the next whitespace or XML tag boundary\n# ('\u003c') so the whole token tail is redacted, not just its leading run.\nredact_token() {\n local input=\"$1\"\n printf '%s' \"$input\" | sed 's/00D[^ \u003c]*/[REDACTED]/g; s/Bearer [^ ]*/Bearer [REDACTED]/g'\n}\n\n# Parse SOAP Metadata API response and classify the result\n# Args:\n# $1 = raw SOAP XML response\n# $2 = field name to extract (e.g., \"enableEinsteinGptPlatform\")\n# $3 = HTTP status code (e.g., \"200\", \"503\")\n# Returns: classification string (see header comments)\nparse_soap_response() {\n local xml_response=\"$1\"\n local field_name=\"$2\"\n local http_status=\"$3\"\n\n # Validate field_name to prevent regex injection in grep patterns below\n if [[ ! \"$field_name\" =~ ^[A-Za-z0-9_]+$ ]]; then\n echo \"NETWORK_ERROR\"\n return 0\n fi\n\n # Disable pipefail locally to allow grep failures without aborting\n local old_opts=\"$-\"\n set +e\n\n # Check for network/service errors first (non-2xx status or empty response)\n if [[ -z \"$xml_response\" ]] || [[ \"$http_status\" != \"200\" && \"$http_status\" != \"201\" ]]; then\n # Restore pipefail before returning\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"NETWORK_ERROR\"\n return 0\n fi\n\n # Check for SOAP Fault (auth, permission, or type errors)\n # Handle both standard and namespace-prefixed faults\n local fault_code=\"\"\n if echo \"$xml_response\" | grep -q \"\u003csoapenv:Fault>\\\\|\u003csf:Fault>\\\\|\u003cFault>\"; then\n # Extract faultcode - try multiple patterns for different namespace conventions\n fault_code=$(echo \"$xml_response\" | grep -o \"\u003cfaultcode>[^\u003c]*\u003c/faultcode>\" | sed 's/\u003c[^>]*>//g' | head -1)\n if [[ -z \"$fault_code\" ]]; then\n fault_code=$(echo \"$xml_response\" | grep -o \"\u003csf:faultcode>[^\u003c]*\u003c/sf:faultcode>\" | sed 's/\u003c[^>]*>//g' | head -1)\n fi\n if [[ -z \"$fault_code\" ]]; then\n # Handle fully-qualified fault codes like \"sf:INVALID_SESSION_ID\"\n fault_code=$(echo \"$xml_response\" | grep -oE \"faultcode>[^\u003c]*\u003c/.*faultcode\" | sed 's/\u003c[^>]*>//g' | sed 's/.*://' | head -1)\n fi\n\n if [[ -n \"$fault_code\" ]]; then\n # Extract faultstring for context (will be redacted)\n local fault_string=\"\"\n fault_string=$(echo \"$xml_response\" | grep -o \"\u003cfaultstring>[^\u003c]*\u003c/faultstring>\" | sed 's/\u003c[^>]*>//g' | head -1 || echo \"\")\n if [[ -z \"$fault_string\" ]]; then\n fault_string=$(echo \"$xml_response\" | grep -o \"\u003csf:faultstring>[^\u003c]*\u003c/sf:faultstring>\" | sed 's/\u003c[^>]*>//g' | head -1 || echo \"\")\n fi\n\n # Redact tokens from fault string\n if [[ -n \"$fault_string\" ]]; then\n fault_string=$(redact_token \"$fault_string\")\n fi\n\n # Classify by fault type\n # INVALID_TYPE = org lacks license/edition - distinct from auth failures\n if echo \"$fault_code\" | grep -qi \"INVALID_TYPE\"; then\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"TYPE_UNAVAILABLE:${fault_code}${fault_string:+:$fault_string}\"\n return 0\n fi\n\n # All other faults are auth/permission errors\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"AUTH_ERROR:${fault_code}${fault_string:+:$fault_string}\"\n return 0\n fi\n fi\n\n # Check if response is valid XML with the expected structure\n # If it's HTML or malformed XML, treat as network error\n if ! echo \"$xml_response\" | grep -q \"\u003creadMetadataResponse>\\\\|\u003cupdateMetadataResponse>\"; then\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"NETWORK_ERROR\"\n return 0\n fi\n\n # Check for closing envelope tag - if missing, XML is truncated\n if ! echo \"$xml_response\" | grep -q \"\u003c/soapenv:Envelope>\"; then\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"NETWORK_ERROR\"\n return 0\n fi\n\n # Extract field value - try to find \u003cfield>true/false\u003c/field>\n # head -1 collapses multi-record responses to the first match so two records\n # never produce a two-line value that fails the == comparisons below.\n local field_value=\"\"\n field_value=$(echo \"$xml_response\" | grep -o \"\u003c${field_name}>[^\u003c]*\u003c/${field_name}>\" | grep -o \"true\\\\|false\" | head -1 || echo \"\")\n\n if [[ \"$field_value\" == \"true\" ]]; then\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"true\"\n return 0\n fi\n\n # If field is explicitly false OR field is omitted entirely, treat as disabled.\n # Salesforce omits boolean settings fields when they're off rather than\n # emitting \u003cfield>false\u003c/field>, so an absent field inside an otherwise valid\n # readMetadataResponse is the disabled state, not an error.\n if [[ \"$field_value\" == \"false\" ]] || [[ -z \"$field_value\" ]]; then\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"false\"\n return 0\n fi\n\n # Shouldn't reach here, but if we do, treat as network error (unexpected response format)\n if [[ \"$old_opts\" =~ e ]]; then set -e; fi\n echo \"NETWORK_ERROR\"\n return 0\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5685,"content_sha256":"448d42683a4a3823bd647e4452de2b9868257536b35e57ba56384c24d637b510"},{"filename":"scripts/shared/stage-descriptions.sh","content":"#!/usr/bin/env bash\n# shared/stage-descriptions.sh\n# ---------------------------------------------------------------------------\n# OpptStageDescription creation, extracted from setup-all.sh so it can run in\n# Block A (Call 1) BEFORE the StageName prompt is deployed / activated / tested,\n# rather than as a trailing phase. Stage descriptions are the grounding source\n# for StageName suggestions, so they must exist before the StageName prompt is\n# verified.\n#\n# Sourced by setup-all.sh. Relies on the parent script's helpers (log_*, section,\n# confirm, sanitize_dev_name, track_issue) — bash resolves these at call time, so\n# they only need to be defined before run_stage_descriptions is CALLED (they are).\n#\n# Inputs (globals set by the caller): ORG_ALIAS, NON_INTERACTIVE,\n# CREATE_STAGE_DESCRIPTIONS, STAGENAME_SELECTED, SCRIPT_DIR.\n# Output (global for the Phase 8.5 summary): STAGE_DESCRIPTIONS_BLOCKED.\n# ---------------------------------------------------------------------------\n\n# Generate a context-aware description for custom stages\n# Args: $1=stage_label, $2=methodology_name, $3=record_type_name (optional)\ngenerate_custom_stage_desc() {\n local stage_label=\"$1\" meth_name=\"$2\" rt_name=\"${3:-}\"\n local stage_lower context_hint meth_hint\n\n stage_lower=$(printf '%s' \"$stage_label\" | tr '[:upper:]' '[:lower:]')\n\n if [[ $stage_lower =~ (technical|tech|solution|demo|poc|pilot|trial) ]]; then\n context_hint=\"Technical validation stage. Activities: solution demonstration, technical requirements review, feasibility assessment, stakeholder technical approval.\"\n elif [[ $stage_lower =~ (legal|contract|compliance|security|review|audit) ]]; then\n context_hint=\"Legal/compliance review stage. Activities: contract review, legal approval, security assessment, compliance validation, risk evaluation.\"\n elif [[ $stage_lower =~ (executive|c-level|leadership|sponsor|approval) ]]; then\n context_hint=\"Executive approval stage. Activities: executive presentation, C-level buy-in, budget approval, strategic alignment validation.\"\n elif [[ $stage_lower =~ (procurement|purchasing|vendor|sourcing) ]]; then\n context_hint=\"Procurement process stage. Activities: vendor evaluation, procurement approval, purchase order creation, budget allocation.\"\n elif [[ $stage_lower =~ (implementation|onboard|kickoff|setup|deploy) ]]; then\n context_hint=\"Implementation planning stage. Activities: project kickoff, resource allocation, timeline planning, onboarding preparation.\"\n elif [[ $stage_lower =~ (finance|financial|budget|pricing|cost) ]]; then\n context_hint=\"Financial validation stage. Activities: budget confirmation, ROI analysis, cost-benefit review, financial approval.\"\n else\n context_hint=\"Custom sales stage. Activities: progress opportunity through this specific phase, document key milestones and decisions.\"\n fi\n\n case \"$meth_name\" in\n MEDDIC)\n meth_hint=\"Apply MEDDIC framework: Validate Metrics (quantified impact), engage Economic Buyer, confirm Decision Criteria met, understand Decision Process steps, address Pain points, enable Champion support.\"\n ;;\n BANT)\n meth_hint=\"Apply BANT criteria: Confirm Budget available, validate Authority engaged, ensure Need remains urgent, verify Timeline realistic for this stage.\"\n ;;\n SPICED)\n meth_hint=\"Apply SPICED framework: Understand Situation context, quantify Pain severity, identify all Impacted stakeholders, validate Critical Event timing, navigate Decision process.\"\n ;;\n *)\n meth_hint=\"Document progress and next steps aligned to your sales methodology.\"\n ;;\n esac\n\n if [[ -n \"$rt_name\" ]]; then\n printf '%s' \"Custom stage for $rt_name: $stage_label. $context_hint $meth_hint Entry: Previous stage criteria met. Exit: Stage-specific requirements completed to advance.\"\n else\n printf '%s' \"Custom stage: $stage_label. $context_hint $meth_hint Entry: Previous stage criteria met. Exit: Stage-specific requirements completed to advance.\"\n fi\n}\n\n# run_stage_descriptions — relocated Phase 6 body; self-gates on STAGENAME_SELECTED.\nrun_stage_descriptions() {\n section \"Phase 4c.5: Stage Descriptions (before StageName prompt)\"\n\n# OpptStageDescription requires PM fully enabled (Tooling API only)\nDESC_QUERY=$(sf data query --query \"SELECT Id, OpportunityStageApiName, Description FROM OpptStageDescription\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null || echo '{\"status\":1}')\n\n# Detect inaccessibility (PM not fully provisioned yet)\nif echo \"$DESC_QUERY\" | grep -qi \"INVALID_TYPE\\|NOT_FOUND\\|sObject type.*not supported\"; then\n log_warn \"OpptStageDescription not accessible yet (PM provisioning may still be in progress)\"\n echo \" Stage descriptions can be configured after provisioning completes.\"\n echo \" Re-run: bash setup-all.sh $ORG_ALIAS (in 2-5 minutes)\"\n DESC_COUNT=\"0\"\n STAGE_DESCRIPTIONS_BLOCKED=\"true\"\nelse\n DESC_COUNT=$(echo \"$DESC_QUERY\" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\n DESC_COUNT=\"${DESC_COUNT:-0}\"\nfi\n\nif [[ \"${STAGE_DESCRIPTIONS_BLOCKED:-}\" == \"true\" ]]; then\n : # Already reported - skip Phase 6 entirely\nelif [[ \"$STAGENAME_SELECTED\" != \"true\" ]]; then\n # StageName was not among the up-front-selected fields, so stage descriptions\n # are irrelevant this run — they only feed StageName suggestions. Skip the\n # whole gate rather than ask an irrelevant question (3-phase requirement:\n # stage descriptions asked ONLY when StageName is selected).\n log_pass \"Stage descriptions: not needed (StageName not selected)\"\nelif [[ \"$DESC_COUNT\" -gt 0 ]]; then\n log_pass \"Stage descriptions: $DESC_COUNT configured\"\nelse\n # No stage descriptions found, and StageName IS selected.\n # CONSENT GATE (#11): stage descriptions are ONLY needed for StageName suggestions and\n # creating them may overwrite descriptions the org intentionally left blank. Creation is\n # therefore opt-in: gated on --create-stage-descriptions (non-interactive) or an explicit\n # interactive confirmation. We NEVER silently default to MEDDIC in non-interactive mode.\n log_info \"No stage descriptions configured (StageName is selected — needed for its suggestions).\"\n\n WANT_STAGE_DESCRIPTIONS=false\n if [[ \"$CREATE_STAGE_DESCRIPTIONS\" == \"true\" ]]; then\n WANT_STAGE_DESCRIPTIONS=true\n elif [[ \"$NON_INTERACTIVE\" != \"true\" ]]; then\n if confirm \"Create stage descriptions now? (only needed if you want StageName suggestions)\"; then\n WANT_STAGE_DESCRIPTIONS=true\n fi\n fi\n\n if [[ \"$WANT_STAGE_DESCRIPTIONS\" != \"true\" ]]; then\n log_pass \"Stage descriptions: skipped (not requested)\"\n if [[ \"$NON_INTERACTIVE\" == \"true\" ]]; then\n echo \" To create them, re-run with --create-stage-descriptions\"\n fi\n METHODOLOGY=\"4\" # sentinel: skip the creation branch below\n elif [[ \"$NON_INTERACTIVE\" == \"true\" ]]; then\n # Consent given via flag, but no TTY to choose a methodology — default to MEDDIC and say so.\n echo \" Creating stage descriptions with MEDDIC methodology (default for --create-stage-descriptions)...\"\n METHODOLOGY=\"1\"\n else\n echo \" Which sales methodology?\"\n echo \" 1) MEDDIC 2) BANT 3) SPICED 4) Skip\"\n read -rp \" Choice (1-4): \" METHODOLOGY\n fi\n\n if [[ \"$METHODOLOGY\" =~ ^[1-3]$ ]]; then\n # Map methodology choice to file name\n case \"$METHODOLOGY\" in\n 1) METH_NAME=\"MEDDIC\"; METH_FILE=\"meddic.txt\" ;;\n 2) METH_NAME=\"BANT\"; METH_FILE=\"bant.txt\" ;;\n 3) METH_NAME=\"SPICED\"; METH_FILE=\"spiced.txt\" ;;\n esac\n\n # Load stage descriptions from asset file\n DESCRIPTIONS_FILE=\"$SCRIPT_DIR/../assets/stage-descriptions/$METH_FILE\"\n if [[ ! -f \"$DESCRIPTIONS_FILE\" ]]; then\n log_fail \"Stage description file not found: $DESCRIPTIONS_FILE\"\n log_warn \"Skipping stage description creation.\"\n else\n log_info \"Creating stage descriptions using $METH_NAME methodology...\"\n\n # Define the 11 standard OOTB stages (for custom stage detection)\n STANDARD_STAGES=(\n \"Prospecting\"\n \"Qualification\"\n \"Needs Analysis\"\n \"Value Proposition\"\n \"Id. Decision Makers\"\n \"Perception Analysis\"\n \"Proposal/Price Quote\"\n \"Negotiation/Review\"\n \"Closed Won\"\n \"Closed Lost\"\n \"Most Likely\"\n )\n\n # Count active Opportunity record types to decide which log path to take.\n # OpptStageDescription is global-per-stage regardless — we only use this count\n # for the log message, not to fan out the write loop.\n RT_QUERY=$(sf data query --query \"SELECT COUNT() FROM RecordType WHERE SObjectType = 'Opportunity' AND IsActive = true\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\n RT_COUNT=$(echo \"$RT_QUERY\" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\n RT_COUNT=\"${RT_COUNT:-0}\"\n\n if [[ \"$RT_COUNT\" -eq 0 ]]; then\n log_warn \"No Opportunity record types found. Using global stage picklist values.\"\n # Fall back to global picklist query (no record type context)\n STAGE_QUERY=$(sf data query --query \"SELECT MasterLabel, ApiName FROM OpportunityStage WHERE IsActive=true ORDER BY SortOrder\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\n\n # Batch pre-fetch existing stage descriptions (avoid N+1 queries)\n EXISTING_DESCS=$(sf data query -q \"SELECT Id, OpportunityStageApiName, Description FROM OpptStageDescription\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null || echo '{\"result\":{\"records\":[]}}')\n\n CREATED=0\n FAILED=0 # real API create/update failures only\n NO_DESC=0 # methodology file had no line for this stage (not an API error)\n CUSTOM_SKIPPED=0\n\n while IFS= read -r STAGE_JSON; do\n [[ -z \"$STAGE_JSON\" ]] && continue\n STAGE_LABEL=$(echo \"$STAGE_JSON\" | jq -r '.MasterLabel // empty' 2>/dev/null)\n STAGE_API=$(echo \"$STAGE_JSON\" | jq -r '.ApiName // empty' 2>/dev/null)\n [[ -z \"$STAGE_LABEL\" || -z \"$STAGE_API\" ]] && continue\n\n # Check if this is a standard stage\n IS_STANDARD=false\n for STD_STAGE in \"${STANDARD_STAGES[@]}\"; do\n if [[ \"$STAGE_LABEL\" == \"$STD_STAGE\" ]]; then\n IS_STANDARD=true\n break\n fi\n done\n\n if [[ \"$IS_STANDARD\" == \"false\" ]]; then\n # Custom stage detected\n echo \"\"\n log_info \"Custom stage detected: $STAGE_LABEL\"\n\n RECOMMENDED_DESC=$(generate_custom_stage_desc \"$STAGE_LABEL\" \"$METH_NAME\")\n\n if [[ ! -t 0 ]]; then\n # Non-interactive: skip custom stages with warning\n log_warn \"Non-interactive mode: skipping custom stage '$STAGE_LABEL'\"\n CUSTOM_SKIPPED=$((CUSTOM_SKIPPED + 1))\n continue\n fi\n\n echo \" Recommended description:\"\n echo \" $RECOMMENDED_DESC\"\n echo \"\"\n read -rp \" Use this description? (y/n/edit): \" CUSTOM_CHOICE\n\n case \"$CUSTOM_CHOICE\" in\n y|Y|yes|Yes|YES)\n DESC=\"$RECOMMENDED_DESC\"\n ;;\n e|E|edit|Edit|EDIT)\n echo \" Enter custom description (single line):\"\n read -r DESC\n [[ -z \"$DESC\" ]] && { log_warn \"Empty description - skipping stage\"; continue; }\n ;;\n *)\n log_warn \"Skipping custom stage: $STAGE_LABEL\"\n CUSTOM_SKIPPED=$((CUSTOM_SKIPPED + 1))\n continue\n ;;\n esac\n else\n # Standard stage - load from file\n # Literal match on the first pipe-delimited field. A stage label can\n # contain regex metacharacters (the OOTB \"Id. Decision Makers\" has a\n # '.'), so grep \"^LABEL|\" would treat them as patterns and mis-match.\n DESC=$(awk -F'|' -v s=\"$STAGE_LABEL\" '$1==s{sub(/^[^|]*\\|/,\"\"); print; exit}' \"$DESCRIPTIONS_FILE\")\n if [[ -z \"$DESC\" ]]; then\n log_info \"No ${METH_NAME} description defined for standard stage '$STAGE_LABEL' — skipping (not an error).\"\n NO_DESC=$((NO_DESC + 1))\n continue\n fi\n fi\n\n # Create/update OpptStageDescription record. Use the sanitizer so labels like\n # \"Id. Decision Makers\" yield a VALID DeveloperName (not \"Id__Decision_Makers\").\n DEV_NAME=$(sanitize_dev_name \"$STAGE_LABEL\")\n\n # Check if description already exists for this stage\n EXISTING_ID=$(printf '%s' \"$EXISTING_DESCS\" | jq -r --arg stage \"$STAGE_API\" '.result.records[] | select(.OpportunityStageApiName == $stage) | .Id' 2>/dev/null | head -1)\n EXISTING_ID=\"${EXISTING_ID:-}\"\n\n # Idempotency: for custom stages with existing descriptions, offer to keep\n if [[ \"$IS_STANDARD\" == \"false\" && -n \"$EXISTING_ID\" && -t 0 ]]; then\n CURRENT_DESC=$(printf '%s' \"$EXISTING_DESCS\" | jq -r --arg id \"$EXISTING_ID\" '.result.records[] | select(.Id == $id) | .Description // \"\"' 2>/dev/null || echo \"\")\n if [[ -n \"$CURRENT_DESC\" ]]; then\n echo \" Current: ${CURRENT_DESC:0:100}\"\n read -rp \" Keep current description? (y/n): \" KEEP_CHOICE\n if [[ \"$KEEP_CHOICE\" =~ ^[yY] ]]; then\n CREATED=$((CREATED + 1))\n continue\n fi\n fi\n fi\n\n # Escape for SOQL/CLI: double single-quotes, strip backticks and shell metacharacters\n DESC_ESCAPED=$(printf '%s' \"$DESC\" | sed \"s/'/''/g\" | tr -d '`' | sed 's/\\$/\\\\$/g')\n\n if [[ -n \"$EXISTING_ID\" ]]; then\n # Update existing record — capture stderr+json so a failure reports WHY.\n SD_RESULT=$(sf data update record --sobject OpptStageDescription --record-id \"$EXISTING_ID\" \\\n --values \"Description='${DESC_ESCAPED}'\" \\\n --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>&1 || true)\n if echo \"$SD_RESULT\" | jq -e '.result.id // .status == 0' >/dev/null 2>&1; then\n CREATED=$((CREATED + 1))\n else\n SD_ERR=$(echo \"$SD_RESULT\" | jq -r '.message // .result.message // empty' 2>/dev/null || echo \"\")\n log_fail \"Failed to update '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}\"\n FAILED=$((FAILED + 1))\n fi\n else\n # Create new record — capture stderr+json so a failure reports WHY.\n SD_RESULT=$(sf data create record --sobject OpptStageDescription \\\n --values \"DeveloperName='${DEV_NAME}' MasterLabel='${STAGE_LABEL}' OpportunityStageApiName='${STAGE_API}' Description='${DESC_ESCAPED}'\" \\\n --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>&1 || true)\n if echo \"$SD_RESULT\" | jq -e '.result.id // empty' >/dev/null 2>&1; then\n CREATED=$((CREATED + 1))\n else\n SD_ERR=$(echo \"$SD_RESULT\" | jq -r '.message // .result.message // empty' 2>/dev/null || echo \"\")\n log_fail \"Failed to create '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}\"\n FAILED=$((FAILED + 1))\n fi\n fi\n done \u003c \u003c(echo \"$STAGE_QUERY\" | jq -c '.result.records[]?' 2>/dev/null)\n\n # Summary — report each category distinctly so real API failures are not\n # conflated with stages that simply had no methodology line (#8).\n if [[ $FAILED -gt 0 ]]; then\n log_fail \"Stage descriptions: $CREATED created/updated, $FAILED FAILED (see errors above), $NO_DESC no-description, $CUSTOM_SKIPPED custom skipped\"\n elif [[ $CREATED -gt 0 ]]; then\n log_pass \"Stage descriptions: $CREATED created/updated, $NO_DESC no-description, $CUSTOM_SKIPPED custom skipped\"\n else\n log_warn \"Stage descriptions: none created ($NO_DESC no-description, $CUSTOM_SKIPPED custom skipped)\"\n fi\n\n else\n # Record types exist. OpptStageDescription is GLOBAL-PER-STAGE (keyed only by\n # OpportunityStageApiName — there is no OpportunityRecordTypeId column on the\n # object), so each active stage is written exactly ONCE regardless of how many\n # Opportunity record types exist. Descriptions are NOT partitioned by record\n # type, so we write against the global active-stage picklist directly. (W-23356857)\n log_info \"Found $RT_COUNT opportunity record type(s). Writing global-per-stage descriptions (one per active stage)...\"\n\n TOTAL_CREATED=0\n TOTAL_FAILED=0\n TOTAL_NO_DESC=0\n TOTAL_CUSTOM_SKIPPED=0\n\n # Batch pre-fetch ALL existing stage descriptions once (avoid redundant queries per stage)\n EXISTING_DESCS=$(sf data query -q \"SELECT Id, OpportunityStageApiName, Description FROM OpptStageDescription\" --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>/dev/null || echo '{\"result\":{\"records\":[]}}')\n\n # OpptStageDescription is global-per-stage: iterate the global active-stage picklist\n # once and write one description per stage (no per-record-type partitioning).\n RT_STAGES_QUERY=$(sf data query --query \"SELECT MasterLabel, ApiName FROM OpportunityStage WHERE IsActive=true ORDER BY SortOrder\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null)\n\n while IFS= read -r STAGE_JSON; do\n [[ -z \"$STAGE_JSON\" ]] && continue\n STAGE_LABEL=$(echo \"$STAGE_JSON\" | jq -r '.MasterLabel // empty' 2>/dev/null)\n STAGE_API=$(echo \"$STAGE_JSON\" | jq -r '.ApiName // empty' 2>/dev/null)\n [[ -z \"$STAGE_LABEL\" || -z \"$STAGE_API\" ]] && continue\n\n # Check if this is a standard stage\n IS_STANDARD=false\n for STD_STAGE in \"${STANDARD_STAGES[@]}\"; do\n if [[ \"$STAGE_LABEL\" == \"$STD_STAGE\" ]]; then\n IS_STANDARD=true\n break\n fi\n done\n\n if [[ \"$IS_STANDARD\" == \"false\" ]]; then\n # Custom stage detected\n log_info \" Custom stage: $STAGE_LABEL\"\n\n RECOMMENDED_DESC=$(generate_custom_stage_desc \"$STAGE_LABEL\" \"$METH_NAME\")\n\n if [[ ! -t 0 ]]; then\n # Non-interactive: skip custom stages with warning\n log_warn \" Non-interactive mode: skipping custom stage '$STAGE_LABEL'\"\n TOTAL_CUSTOM_SKIPPED=$((TOTAL_CUSTOM_SKIPPED + 1))\n continue\n fi\n\n echo \" Recommended description:\"\n echo \" $RECOMMENDED_DESC\"\n echo \"\"\n read -rp \" Use this description? (y/n/edit): \" CUSTOM_CHOICE\n\n case \"$CUSTOM_CHOICE\" in\n y|Y|yes|Yes|YES)\n DESC=\"$RECOMMENDED_DESC\"\n ;;\n e|E|edit|Edit|EDIT)\n echo \" Enter custom description (single line):\"\n read -r DESC\n [[ -z \"$DESC\" ]] && { log_warn \" Empty description - skipping stage\"; continue; }\n ;;\n *)\n log_warn \" Skipping custom stage: $STAGE_LABEL\"\n TOTAL_CUSTOM_SKIPPED=$((TOTAL_CUSTOM_SKIPPED + 1))\n continue\n ;;\n esac\n else\n # Standard stage - load from file\n # Literal match on the first pipe-delimited field. A stage label can\n # contain regex metacharacters (the OOTB \"Id. Decision Makers\" has a\n # '.'), so grep \"^LABEL|\" would treat them as patterns and mis-match.\n DESC=$(awk -F'|' -v s=\"$STAGE_LABEL\" '$1==s{sub(/^[^|]*\\|/,\"\"); print; exit}' \"$DESCRIPTIONS_FILE\")\n if [[ -z \"$DESC\" ]]; then\n log_info \" No ${METH_NAME} description defined for standard stage '$STAGE_LABEL' — skipping (not an error).\"\n TOTAL_NO_DESC=$((TOTAL_NO_DESC + 1))\n continue\n fi\n fi\n\n # Create/update the global OpptStageDescription record (keyed by stage only).\n # Sanitizer guards against invalid DeveloperNames (e.g. double underscores).\n DEV_NAME=$(sanitize_dev_name \"$STAGE_LABEL\")\n\n # Check if a description already exists for this stage.\n EXISTING_ID=$(printf '%s' \"$EXISTING_DESCS\" | jq -r --arg stage \"$STAGE_API\" '.result.records[] | select(.OpportunityStageApiName == $stage) | .Id' 2>/dev/null | head -1)\n EXISTING_ID=\"${EXISTING_ID:-}\"\n\n # Idempotency: for custom stages with existing descriptions, offer to keep\n if [[ \"$IS_STANDARD\" == \"false\" && -n \"$EXISTING_ID\" && -t 0 ]]; then\n CURRENT_DESC=$(printf '%s' \"$EXISTING_DESCS\" | jq -r --arg id \"$EXISTING_ID\" '.result.records[] | select(.Id == $id) | .Description // \"\"' 2>/dev/null || echo \"\")\n if [[ -n \"$CURRENT_DESC\" ]]; then\n echo \" Current: ${CURRENT_DESC:0:100}\"\n read -rp \" Keep current description? (y/n): \" KEEP_CHOICE\n if [[ \"$KEEP_CHOICE\" =~ ^[yY] ]]; then\n TOTAL_CREATED=$((TOTAL_CREATED + 1))\n continue\n fi\n fi\n fi\n\n # Escape for SOQL/CLI: double single-quotes, strip backticks and shell metacharacters\n DESC_ESCAPED=$(printf '%s' \"$DESC\" | sed \"s/'/''/g\" | tr -d '`' | sed 's/\\$/\\\\$/g')\n\n if [[ -n \"$EXISTING_ID\" ]]; then\n # Update existing record — capture the error on failure.\n SD_RESULT=$(sf data update record --sobject OpptStageDescription --record-id \"$EXISTING_ID\" \\\n --values \"Description='${DESC_ESCAPED}'\" \\\n --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>&1 || true)\n if echo \"$SD_RESULT\" | jq -e '.result.id // .status == 0' >/dev/null 2>&1; then\n TOTAL_CREATED=$((TOTAL_CREATED + 1))\n else\n SD_ERR=$(echo \"$SD_RESULT\" | jq -r '.message // .result.message // empty' 2>/dev/null || echo \"\")\n log_fail \" Failed to update '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}\"\n TOTAL_FAILED=$((TOTAL_FAILED + 1))\n fi\n else\n # Create new global record — capture the error on failure.\n SD_RESULT=$(sf data create record --sobject OpptStageDescription \\\n --values \"DeveloperName='${DEV_NAME}' MasterLabel='${STAGE_LABEL}' OpportunityStageApiName='${STAGE_API}' Description='${DESC_ESCAPED}'\" \\\n --target-org \"$ORG_ALIAS\" --use-tooling-api --json 2>&1 || true)\n if echo \"$SD_RESULT\" | jq -e '.result.id // empty' >/dev/null 2>&1; then\n TOTAL_CREATED=$((TOTAL_CREATED + 1))\n else\n SD_ERR=$(echo \"$SD_RESULT\" | jq -r '.message // .result.message // empty' 2>/dev/null || echo \"\")\n log_fail \" Failed to create '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}\"\n TOTAL_FAILED=$((TOTAL_FAILED + 1))\n fi\n fi\n done \u003c \u003c(echo \"$RT_STAGES_QUERY\" | jq -c '.result.records[]?' 2>/dev/null)\n\n # Overall summary — real API failures reported distinctly from no-description skips.\n echo \"\"\n if [[ $TOTAL_FAILED -gt 0 ]]; then\n log_fail \"Stage descriptions: $TOTAL_CREATED created/updated, $TOTAL_FAILED FAILED (see errors above), ${TOTAL_NO_DESC:-0} no-description, $TOTAL_CUSTOM_SKIPPED custom skipped\"\n elif [[ $TOTAL_CREATED -gt 0 ]]; then\n log_pass \"Stage descriptions: $TOTAL_CREATED created/updated, ${TOTAL_NO_DESC:-0} no-description, $TOTAL_CUSTOM_SKIPPED custom skipped\"\n else\n log_warn \"Stage descriptions: none created (${TOTAL_NO_DESC:-0} no-description, $TOTAL_CUSTOM_SKIPPED custom skipped)\"\n fi\n fi\n fi\n else\n log_warn \"Skipped. Stage suggestions will NOT work without descriptions.\"\n fi\nfi\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":23898,"content_sha256":"835ef10b638c004ca0de6ba4c12184e38cbda8ddba604958396c54667e0ff96d"},{"filename":"scripts/shared/test-opp.sh","content":"#!/usr/bin/env bash\n#\n# test-opp.sh — shared test-opportunity + grounding-note resolution for the two\n# admin verification scripts (flow-debug-and-verify.sh and\n# verify-prompt-generation.sh).\n#\n# The skill invokes those two scripts SEPARATELY (prompt path vs. flow path), but\n# both must converge on ONE shared test Opportunity and ONE shared grounding Note\n# so a suggestion generated on one path is visible on the other and duplicates\n# never pile up. Keeping this logic in a single sourced lib is what guarantees the\n# two stay byte-for-byte identical (they drifted before this was extracted).\n#\n# Contract:\n# - Sourced, not executed. Relies on the CALLER having defined:\n# * $ORG_ALIAS (target org)\n# * log_info / log_pass / log_warn (helpers — resolved at call time)\n# - Honors optional env var $PM_GROUNDING_NOTE: an agent/LLM-composed grounding\n# note body. When set it is what grounds the AI suggestion; absent, a generic\n# literal is used so standalone runs still work (a contextless note grounds on\n# nothing and returns an empty suggestion, making a working setup look broken).\n# - resolve_test_opportunity sets the global OPP_ID (empty if none could be\n# resolved) and seeds/refreshes the grounding note on it.\n#\n# NOTE: no `set -euo pipefail` here — the caller owns shell options. Every sf/jq\n# pipeline is guarded with `|| echo \"\"` so a transient CLI/network blip can't trip\n# the caller's `set -e` and abort the whole verification run.\n\n# Canonical name for the reusable test opp. The `[PM-TEST]` prefix makes every\n# opp this tooling creates findable in one place (Opportunity WHERE Name LIKE\n# '[PM-TEST]%') so they can be spotted/cleaned up and never confused with real\n# pipeline in Flow Builder's Debug record picker or Pipeline Inspection.\nPM_TEST_OPP_NAME='[PM-TEST] Test Pipeline Verification Opp'\n# Title of the grounding note this tooling manages (used to find/refresh it).\nPM_GROUNDING_NOTE_TITLE='PM grounding note'\n# base64 of \"Test grounding data for Pipeline Management verification.\" — the\n# fallback body when the driver passes no PM_GROUNDING_NOTE.\nPM_GROUNDING_NOTE_FALLBACK_B64='VGVzdCBncm91bmRpbmcgZGF0YSBmb3IgUGlwZWxpbmUgTWFuYWdlbWVudCB2ZXJpZmljYXRpb24u'\n\n# Resolve the shared test opportunity, setting the global OPP_ID.\n# 1. Reuse the stable [PM-TEST] opp if one exists (and roll its CloseDate +30d\n# so it stays flow-eligible over repeated sessions).\n# 2. Else create it (owner = current user; Pipeline Inspection is owner-scoped,\n# so a suggestion on someone else's opp is invisible to the admin verifying).\n# 3. Else fall back to any user-owned open opp.\n# 4. Seed or refresh the grounding note against whichever opp resolved.\nresolve_test_opportunity() {\n local current_user_id close_date stage_label opp_values\n\n current_user_id=$(NO_COLOR=1 FORCE_COLOR=0 sf org display user --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.id // empty' 2>/dev/null || echo \"\")\n\n # macOS/BSD `date -v+30d`; GNU `date -d '+30 days'`. Empty => skip create/roll.\n close_date=$(date -v+30d '+%Y-%m-%d' 2>/dev/null || date -d '+30 days' '+%Y-%m-%d' 2>/dev/null || echo \"\")\n\n # 1) Reuse the stable, prefixed test opp if it exists.\n if [[ -n \"$current_user_id\" ]]; then\n OPP_ID=$(sf data query -q \"SELECT Id FROM Opportunity WHERE Name='${PM_TEST_OPP_NAME}' AND OwnerId='${current_user_id}' AND IsClosed=false AND CloseDate>=TODAY AND CloseDate\u003c=NEXT_N_DAYS:90 ORDER BY CreatedDate DESC LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' || echo \"\")\n if [[ -n \"$OPP_ID\" ]]; then\n log_pass \"Reusing existing test opportunity: $OPP_ID ($PM_TEST_OPP_NAME)\"\n # Roll CloseDate forward so the reused opp keeps meeting the flow's\n # \"CloseDate within 90 days\" eligibility filter across sessions.\n if [[ -n \"$close_date\" ]]; then\n if sf data update record --sobject Opportunity --record-id \"$OPP_ID\" \\\n --values \"CloseDate='${close_date}'\" --target-org \"$ORG_ALIAS\" --json 2>/dev/null >/dev/null; then\n log_info \"Rolled CloseDate to ${close_date}\"\n else\n log_warn \"Could not roll CloseDate on $OPP_ID (continuing)\"\n fi\n fi\n fi\n fi\n\n # 2) Create the prefixed test opp if none exists yet.\n if [[ -z \"${OPP_ID:-}\" && -n \"$close_date\" ]]; then\n stage_label=$(sf data query -q \"SELECT MasterLabel FROM OpportunityStage WHERE IsActive=true AND IsClosed=false ORDER BY SortOrder LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].MasterLabel // empty')\n # Skip creation if the stage label contains a single quote — it would break\n # --values parsing.\n if [[ -n \"$stage_label\" ]] && [[ \"$stage_label\" != *\"'\"* ]]; then\n opp_values=\"Name='${PM_TEST_OPP_NAME}' StageName='${stage_label}' CloseDate='${close_date}'\"\n [[ -n \"$current_user_id\" ]] && opp_values=\"${opp_values} OwnerId='${current_user_id}'\"\n OPP_ID=$(sf data create record --sobject Opportunity --values \"$opp_values\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.id // empty' || echo \"\")\n [[ -n \"$OPP_ID\" ]] && log_pass \"Created test opportunity: $OPP_ID ($PM_TEST_OPP_NAME)\"\n fi\n fi\n\n # 3) Fall back to a user-owned open opp (owner filter only when user is known).\n if [[ -z \"${OPP_ID:-}\" ]]; then\n if [[ -n \"$current_user_id\" ]]; then\n OPP_ID=$(sf data query -q \"SELECT Id FROM Opportunity WHERE OwnerId='${current_user_id}' AND IsClosed=false AND CloseDate>=TODAY AND CloseDate\u003c=NEXT_N_DAYS:90 ORDER BY LastModifiedDate DESC LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' || echo \"\")\n else\n OPP_ID=$(sf data query -q \"SELECT Id FROM Opportunity WHERE IsClosed=false AND CloseDate>=TODAY AND CloseDate\u003c=NEXT_N_DAYS:90 ORDER BY LastModifiedDate DESC LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' || echo \"\")\n fi\n [[ -n \"$OPP_ID\" ]] && log_pass \"Using opportunity: $OPP_ID\"\n fi\n\n # 4) Seed / refresh the grounding note against the FINALLY-resolved opp so a\n # freshly created opp, a reused opp, AND a fallback opp all end up grounded.\n [[ -n \"${OPP_ID:-}\" ]] && seed_grounding_note \"$OPP_ID\"\n}\n\n# Ensure the shared grounding note exists (and matches the driver's note) on the\n# given opp. Idempotent: seeds when absent, refreshes only when the composed note\n# differs from what's stored, and otherwise leaves the existing note untouched.\nseed_grounding_note() {\n local opp_id=\"$1\"\n local note_b64 existing_doc_id existing_content note_id\n\n # base64 line-wraps at 76 cols on macOS/BSD; `tr -d '\\n'` keeps it single-line\n # (GNU's `-w0` isn't portable).\n if [[ -n \"${PM_GROUNDING_NOTE:-}\" ]]; then\n note_b64=$(printf '%s' \"$PM_GROUNDING_NOTE\" | base64 | tr -d '\\n')\n else\n note_b64=\"$PM_GROUNDING_NOTE_FALLBACK_B64\"\n fi\n\n # Find a grounding note this tooling previously seeded on the opp. ContentNote's\n # own 069-prefix Id IS the ContentDocumentId, so the link's ContentDocumentId is\n # the note's Id. `|| echo \"\"` keeps a query blip from tripping the caller's set -e.\n existing_doc_id=$(sf data query -q \"SELECT ContentDocumentId FROM ContentDocumentLink WHERE LinkedEntityId='${opp_id}' AND ContentDocument.Title='${PM_GROUNDING_NOTE_TITLE}' ORDER BY ContentDocument.CreatedDate DESC LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.records[0].ContentDocumentId // empty' || echo \"\")\n\n if [[ -n \"$existing_doc_id\" ]]; then\n # A grounding note already exists. Refresh it ONLY when the driver passed a\n # note whose content differs from what's stored — otherwise reuse it so we\n # don't churn the note (and its ContentVersion history) every run.\n # ContentNote.Content is a base64Url BLOB field: SOQL returns the REST *path*\n # to the blob, not the bytes, so we fetch the decoded body via that endpoint\n # and compare it to the raw PM_GROUNDING_NOTE plaintext (not base64).\n if [[ -n \"${PM_GROUNDING_NOTE:-}\" ]]; then\n existing_content=$(sf api request rest \"/services/data/v64.0/sobjects/ContentNote/${existing_doc_id}/Content\" \\\n --target-org \"$ORG_ALIAS\" 2>/dev/null || echo \"\")\n if [[ -n \"$existing_content\" && \"$existing_content\" != \"$PM_GROUNDING_NOTE\" ]]; then\n # ContentNote.Content isn't reliably updatable in place (it's\n # ContentVersion-backed), so replace by delete + recreate: dropping the\n # ContentDocument cascades its link and versions.\n sf data delete record --sobject ContentDocument --record-id \"$existing_doc_id\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null >/dev/null || true\n existing_doc_id=\"\"\n log_info \"Grounding note changed — refreshing it on $opp_id\"\n else\n log_info \"Reusing existing grounding note on $opp_id\"\n fi\n else\n log_info \"Reusing existing grounding note on $opp_id\"\n fi\n fi\n\n # Seed when none exists (fresh opp) or the stale one was just dropped above.\n if [[ -z \"$existing_doc_id\" ]]; then\n note_id=$(sf data create record --sobject ContentNote \\\n --values \"Title='${PM_GROUNDING_NOTE_TITLE}' Content='${note_b64}'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.id // empty' || echo \"\")\n if [[ -n \"$note_id\" ]]; then\n sf data create record --sobject ContentDocumentLink \\\n --values \"ContentDocumentId='${note_id}' LinkedEntityId='${opp_id}' ShareType='V'\" \\\n --target-org \"$ORG_ALIAS\" --json 2>/dev/null >/dev/null || true\n log_info \"Seeded ContentNote for grounding on test opportunity\"\n else\n log_warn \"Could not seed grounding ContentNote (Notes may not be enabled on this org) — suggestion quality may be lower\"\n fi\n fi\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":9865,"content_sha256":"db0e6cc44072c63f48656cd7b2f2691658b8c5e83fb3173c22e0ff47c53b6971"},{"filename":"scripts/verify-all.sh","content":"#!/bin/bash\n\n# Comprehensive verification of Pipeline Management configuration\n#\n# Checks all components and reports status. Run after setup to confirm\n# everything is working, or to diagnose a partially configured org.\n#\n# Usage: ./verify-all.sh \u003corg-alias>\n\nset -euo pipefail\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\n# Source shared libraries\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"$SCRIPT_DIR/shared/soap.sh\"\nsource \"$SCRIPT_DIR/shared/agent-detection.sh\"\n\nORG_ALIAS=\"${1:-}\"\n\nif [[ -z \"$ORG_ALIAS\" ]]; then\n echo \"Error: Missing org alias\"\n echo \"Usage: $0 \u003corg-alias>\"\n exit 1\nfi\n\n# Validate org alias (prevent shell injection via metacharacters)\nif [[ ! \"$ORG_ALIAS\" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then\n echo \"ERROR: Invalid org alias. Only alphanumeric, dots, hyphens, and underscores allowed (must start with alphanumeric).\"\n exit 1\nfi\n\necho \"============================================\"\necho \" PIPELINE MANAGEMENT CONFIGURATION STATUS\"\necho \"============================================\"\necho \"\"\necho \"Org: $ORG_ALIAS\"\necho \"Date: $(date '+%Y-%m-%d %H:%M:%S')\"\necho \"\"\n\nPASS=0\nWARN=0\nFAIL=0\n\ncheck_pass() { echo \" [PASS] $1\"; PASS=$((PASS + 1)); }\ncheck_warn() { echo \" [WARN] $1\"; WARN=$((WARN + 1)); }\ncheck_fail() { echo \" [FAIL] $1\"; FAIL=$((FAIL + 1)); }\ncheck_error() { echo \" [ERROR] $1\"; echo \" $2\"; FAIL=$((FAIL + 1)); }\n\n# --- Prerequisites ---\necho \"--- Prerequisites ---\"\n\n# Get auth details for SOAP checks\nINSTANCE_URL=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.instanceUrl // empty' 2>/dev/null || echo \"\")\n\n# Get access token — newer CLI versions redact it in `sf org display --json`\n# so we use `sf org auth show-access-token --json` which always returns the real token\nACCESS_TOKEN=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.accessToken // empty' 2>/dev/null || echo \"\")\n\nif [[ -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" || \"$ACCESS_TOKEN\" == *\"REDACTED\"* ]]; then\n ACCESS_TOKEN=$(echo \"y\" | sf org auth show-access-token --target-org \"$ORG_ALIAS\" --no-prompt --json 2>/dev/null | jq -r '.result.accessToken // empty' 2>/dev/null || echo \"\")\nfi\n\nif [[ -z \"$ACCESS_TOKEN\" || \"$ACCESS_TOKEN\" == \"null\" ]]; then\n echo \" [FAIL] Cannot retrieve access token. Is the org authenticated?\"\n echo \" Run: sf org login web --instance-url \u003curl> --alias $ORG_ALIAS\"\n exit 1\nfi\n\n# Check Pipeline Management (SOAP only)\nPM_RESPONSE=$(curl -s -w \"\\n%{http_code}\" \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\n \u003cmet:type>SalesDealAgentSettings\u003c/met:type>\n \u003cmet:fullNames>SalesDealAgent\u003c/met:fullNames>\n \u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\nPM_HTTP_STATUS=$(echo \"$PM_RESPONSE\" | tail -1)\nPM_XML=$(echo \"$PM_RESPONSE\" | sed '$d')\nPM_STATUS=$(parse_soap_response \"$PM_XML\" \"enableDealAgent\" \"$PM_HTTP_STATUS\")\n\ncase \"$PM_STATUS\" in\n true)\n check_pass \"Pipeline Management: enabled\"\n ;;\n false)\n check_fail \"Pipeline Management: DISABLED\"\n echo \" → Enable via: Setup → Sales → Pipeline Management → Turn On\"\n ;;\n AUTH_ERROR:*)\n check_error \"Pipeline Management: Authentication failed\" \"${PM_STATUS#AUTH_ERROR:}\"\n echo \" → Fix: sf org login web --instance-url $INSTANCE_URL --alias $ORG_ALIAS\"\n ;;\n TYPE_UNAVAILABLE:*)\n check_error \"Pipeline Management: Not available in this org\" \"${PM_STATUS#TYPE_UNAVAILABLE:}\"\n echo \" → Check: org edition and enabled features in Setup\"\n ;;\n NETWORK_ERROR)\n check_error \"Pipeline Management: Cannot verify (network/service error)\" \"Check instance URL and connectivity\"\n ;;\n *)\n check_fail \"Pipeline Management: Unknown status ($PM_STATUS)\"\n ;;\nesac\n\n# Check Einstein GenAI\nGENAI_RESPONSE=$(curl -s -w \"\\n%{http_code}\" \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\n \u003cmet:type>EinsteinGptSettings\u003c/met:type>\n \u003cmet:fullNames>EinsteinGpt\u003c/met:fullNames>\n \u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\nGENAI_HTTP_STATUS=$(echo \"$GENAI_RESPONSE\" | tail -1)\nGENAI_XML=$(echo \"$GENAI_RESPONSE\" | sed '$d')\nGENAI_STATUS=$(parse_soap_response \"$GENAI_XML\" \"enableEinsteinGptPlatform\" \"$GENAI_HTTP_STATUS\")\n\ncase \"$GENAI_STATUS\" in\n true)\n check_pass \"Einstein Generative AI: enabled\"\n ;;\n false)\n check_fail \"Einstein Generative AI: DISABLED\"\n echo \" → Enable via: Setup → Einstein → Einstein Generative AI → Turn On\"\n ;;\n AUTH_ERROR:*)\n check_error \"Einstein Generative AI: Authentication failed\" \"${GENAI_STATUS#AUTH_ERROR:}\"\n echo \" → Fix: sf org login web --instance-url $INSTANCE_URL --alias $ORG_ALIAS\"\n ;;\n NETWORK_ERROR)\n check_error \"Einstein Generative AI: Cannot verify (network/service error)\" \"Check instance URL and connectivity\"\n ;;\n *)\n check_fail \"Einstein Generative AI: Unknown status ($GENAI_STATUS)\"\n ;;\nesac\n\n# Check Agentforce Agent\nCOPILOT_RESPONSE=$(curl -s -w \"\\n%{http_code}\" \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\n \u003cmet:type>EinsteinCopilotSettings\u003c/met:type>\n \u003cmet:fullNames>EinsteinCopilot\u003c/met:fullNames>\n \u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\nCOPILOT_HTTP_STATUS=$(echo \"$COPILOT_RESPONSE\" | tail -1)\nCOPILOT_XML=$(echo \"$COPILOT_RESPONSE\" | sed '$d')\nCOPILOT_STATUS=$(parse_soap_response \"$COPILOT_XML\" \"enableEinsteinGptCopilot\" \"$COPILOT_HTTP_STATUS\")\n\ncase \"$COPILOT_STATUS\" in\n true)\n check_pass \"Agentforce Agent: enabled\"\n ;;\n false)\n check_fail \"Agentforce Agent: DISABLED\"\n echo \" → Enable via: Setup → Einstein → Agentforce Agent → Turn On\"\n ;;\n AUTH_ERROR:*)\n check_error \"Agentforce Agent: Authentication failed\" \"${COPILOT_STATUS#AUTH_ERROR:}\"\n echo \" → Fix: sf org login web --instance-url $INSTANCE_URL --alias $ORG_ALIAS\"\n ;;\n NETWORK_ERROR)\n check_error \"Agentforce Agent: Cannot verify (network/service error)\" \"Check instance URL and connectivity\"\n ;;\n *)\n check_fail \"Agentforce Agent: Unknown status ($COPILOT_STATUS)\"\n ;;\nesac\n\n# Check Agentforce Studio / Agent Platform (CRITICAL — AgentPlatformSettings\n# .enableAgentPlatform gates the Sales Deal Agent; core rejects Deal Agent\n# enablement when this OrgPreference is off, so a disabled state here explains\n# an otherwise-opaque Deal Agent activation failure).\nAGENT_PLATFORM_RESPONSE=$(curl -s -w \"\\n%{http_code}\" \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\n \u003cmet:type>AgentPlatformSettings\u003c/met:type>\n \u003cmet:fullNames>AgentPlatform\u003c/met:fullNames>\n \u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\nAGENT_PLATFORM_HTTP_STATUS=$(echo \"$AGENT_PLATFORM_RESPONSE\" | tail -1)\nAGENT_PLATFORM_XML=$(echo \"$AGENT_PLATFORM_RESPONSE\" | sed '$d')\nAGENT_PLATFORM_STATUS=$(parse_soap_response \"$AGENT_PLATFORM_XML\" \"enableAgentPlatform\" \"$AGENT_PLATFORM_HTTP_STATUS\")\n\ncase \"$AGENT_PLATFORM_STATUS\" in\n true)\n check_pass \"Agentforce Studio (Agent Platform): enabled\"\n ;;\n false)\n check_fail \"Agentforce Studio (Agent Platform): DISABLED\"\n echo \" → Enable via: Setup → Agentforce Studio → Turn On (gates Deal Agent)\"\n ;;\n AUTH_ERROR:*)\n check_error \"Agentforce Studio (Agent Platform): Authentication failed\" \"${AGENT_PLATFORM_STATUS#AUTH_ERROR:}\"\n echo \" → Fix: sf org login web --instance-url $INSTANCE_URL --alias $ORG_ALIAS\"\n ;;\n NETWORK_ERROR)\n check_error \"Agentforce Studio (Agent Platform): Cannot verify (network/service error)\" \"Check instance URL and connectivity\"\n ;;\n *)\n check_fail \"Agentforce Studio (Agent Platform): Unknown status ($AGENT_PLATFORM_STATUS)\"\n ;;\nesac\n\n# Check Opportunity Team (CRITICAL — PM flow uses OpportunityTeamMember; without\n# it, flow deployment fails with an OppTeamMemRec error)\nOPP_TEAM_RESPONSE=$(curl -s -w \"\\n%{http_code}\" \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\n \u003cmet:type>OpportunitySettings\u003c/met:type>\n \u003cmet:fullNames>Opportunity\u003c/met:fullNames>\n \u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\nOPP_TEAM_HTTP_STATUS=$(echo \"$OPP_TEAM_RESPONSE\" | tail -1)\nOPP_TEAM_XML=$(echo \"$OPP_TEAM_RESPONSE\" | sed '$d')\nOPP_TEAM_STATUS=$(parse_soap_response \"$OPP_TEAM_XML\" \"enableOpportunityTeam\" \"$OPP_TEAM_HTTP_STATUS\")\n\ncase \"$OPP_TEAM_STATUS\" in\n true)\n check_pass \"Opportunity Team: enabled\"\n ;;\n false)\n check_fail \"Opportunity Team: DISABLED (flow deploy fails with OppTeamMemRec error)\"\n echo \" → Enable via: Setup → Opportunity → Opportunity Team Settings → Turn On\"\n ;;\n AUTH_ERROR:*)\n check_error \"Opportunity Team: Authentication failed\" \"${OPP_TEAM_STATUS#AUTH_ERROR:}\"\n echo \" → Fix: sf org login web --instance-url $INSTANCE_URL --alias $ORG_ALIAS\"\n ;;\n NETWORK_ERROR)\n check_error \"Opportunity Team: Cannot verify (network/service error)\" \"Check instance URL and connectivity\"\n ;;\n *)\n check_fail \"Opportunity Team: Unknown status ($OPP_TEAM_STATUS)\"\n ;;\nesac\n\n# Check Pipeline Inspection\nPI_RESPONSE=$(curl -s -w \"\\n%{http_code}\" \"${INSTANCE_URL}/services/Soap/m/64.0\" \\\n -H \"Content-Type: text/xml; charset=UTF-8\" \\\n -H \"SOAPAction: readMetadata\" \\\n -d \"\u003c?xml version='1.0' encoding='utf-8'?>\n\u003csoapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>\n \u003csoapenv:Header>\u003cmet:SessionHeader>\u003cmet:sessionId>${ACCESS_TOKEN}\u003c/met:sessionId>\u003c/met:SessionHeader>\u003c/soapenv:Header>\n \u003csoapenv:Body>\u003cmet:readMetadata>\n \u003cmet:type>OpportunitySettings\u003c/met:type>\n \u003cmet:fullNames>Opportunity\u003c/met:fullNames>\n \u003c/met:readMetadata>\u003c/soapenv:Body>\n\u003c/soapenv:Envelope>\" 2>/dev/null)\nPI_HTTP_STATUS=$(echo \"$PI_RESPONSE\" | tail -1)\nPI_XML=$(echo \"$PI_RESPONSE\" | sed '$d')\nPI_STATUS=$(parse_soap_response \"$PI_XML\" \"enablePipelineInspection\" \"$PI_HTTP_STATUS\")\n\ncase \"$PI_STATUS\" in\n true)\n check_pass \"Pipeline Inspection: enabled\"\n ;;\n false)\n check_warn \"Pipeline Inspection: DISABLED (suggestions won't show in UI)\"\n ;;\n AUTH_ERROR:*)\n check_error \"Pipeline Inspection: Authentication failed\" \"${PI_STATUS#AUTH_ERROR:}\"\n echo \" → Fix: sf org login web --instance-url $INSTANCE_URL --alias $ORG_ALIAS\"\n ;;\n NETWORK_ERROR)\n check_error \"Pipeline Inspection: Cannot verify (network/service error)\" \"Check instance URL and connectivity\"\n ;;\n *)\n check_warn \"Pipeline Inspection: Unknown status ($PI_STATUS)\"\n ;;\nesac\n\necho \"\"\n\n# --- Agent ---\necho \"--- Agent ---\"\n\nAGENT_USER=$(sf data query \\\n --query \"SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo \"\")\n\nif [[ -n \"$AGENT_USER\" ]]; then\n check_pass \"Agent user: $AGENT_USER\"\nelse\n check_fail \"Agent user: NOT FOUND\"\nfi\n\n# Detect the PM bot by AgentTemplate — the same field the scheduled flow\n# uses — instead of a hardcoded DeveloperName.\nBOT_DEF=$(pm_bot_developer_name \"$ORG_ALIAS\")\n\nif [[ -n \"$BOT_DEF\" ]]; then\n check_pass \"BotDefinition: $BOT_DEF (matched by AgentTemplate)\"\nelse\n check_fail \"BotDefinition: NOT FOUND — no bot with a PM AgentTemplate (run create-agent.sh)\"\nfi\n\necho \"\"\n\n# --- Permission Set Groups ---\necho \"--- Permission Set Groups ---\"\n\nPSG_COUNT=$(sf data query \\\n --query \"SELECT Id, DeveloperName FROM PermissionSetGroup WHERE DeveloperName IN ('SalesManagementUserPsg','SalesManagementAgentUserPsg')\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\nPSG_COUNT=\"${PSG_COUNT:-0}\"\n\n# These two PSGs ship with the Agentforce-for-Sales license, so their presence is a\n# license check — NOT a Pipeline Management enablement indicator. The real functional\n# checks are the agent-user PSG assignment and user PSG assignments below.\nif [[ \"$PSG_COUNT\" -ge 2 ]]; then\n check_pass \"Permission Set Groups (license): both present ($PSG_COUNT/2)\"\nelse\n check_warn \"Permission Set Groups (license): only $PSG_COUNT/2 found — org may lack the Agentforce-for-Sales license\"\nfi\n\n# Check if agent user has PSG\nif [[ -n \"$AGENT_USER\" ]]; then\n AGENT_USER_ID=$(sf data query \\\n --query \"SELECT Id FROM User WHERE Username LIKE '%salesmanagementagentuser%'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\n if [[ -n \"$AGENT_USER_ID\" ]]; then\n AGENT_PSG=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetAssignment WHERE AssigneeId = '${AGENT_USER_ID}' AND PermissionSetGroup.DeveloperName = 'SalesManagementAgentUserPsg'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\n AGENT_PSG=\"${AGENT_PSG:-0}\"\n\n if [[ \"$AGENT_PSG\" -ge 1 ]]; then\n check_pass \"Agent PSG assignment: assigned\"\n else\n check_warn \"Agent PSG assignment: NOT assigned to agent user\"\n fi\n fi\nfi\n\n# Count users with SalesManagementUserPsg\nUSER_PSG_COUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM PermissionSetAssignment WHERE PermissionSetGroup.DeveloperName = 'SalesManagementUserPsg'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\nUSER_PSG_COUNT=\"${USER_PSG_COUNT:-0}\"\n\nif [[ \"$USER_PSG_COUNT\" -ge 1 ]]; then\n check_pass \"User PSG assignments: $USER_PSG_COUNT user(s) assigned\"\nelse\n check_warn \"User PSG assignments: 0 users have SalesManagementUserPsg\"\nfi\n\necho \"\"\n\n# --- Agent Access (W-23242378) ---\n# Users holding SalesManagementUserPsg can only launch the agent if it is added\n# to \"Agent Access\" on a permission set. This is done via the custom permset\n# Sales_Agent_Access (SetupEntityAccess -> BotDefinition) linked into the PSG.\necho \"--- Agent Access ---\"\n\nACCESS_PS_ID=$(sf data query \\\n --query \"SELECT Id FROM PermissionSet WHERE Name = 'Sales_Agent_Access'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\nif [[ -z \"$ACCESS_PS_ID\" ]]; then\n check_warn \"Agent Access permset: 'Sales_Agent_Access' not found (run define-agent-access.sh)\"\nelse\n check_pass \"Agent Access permset: Sales_Agent_Access\"\n\n # Is the agent (BotDefinition, matched by AgentTemplate) granted on that permset?\n # Query by AgentTemplate only — the `Id IN (SELECT ... FROM BotVersion)` subquery\n # was removed in de77d520 because it hangs 60+ seconds on some orgs. Active-version\n # verification is reported separately by the BotDefinition check above (line ~270).\n if [[ -n \"$BOT_DEF\" ]]; then\n BOT_DEF_ID=$(sf data query \\\n --query \"SELECT Id FROM BotDefinition WHERE AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) ORDER BY LastModifiedDate DESC LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\n if [[ -n \"$BOT_DEF_ID\" ]]; then\n ACCESS_COUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM SetupEntityAccess WHERE ParentId = '${ACCESS_PS_ID}' AND SetupEntityId = '${BOT_DEF_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\n ACCESS_COUNT=\"${ACCESS_COUNT:-0}\"\n\n if [[ \"$ACCESS_COUNT\" -ge 1 ]]; then\n check_pass \"Agent Access grant: $BOT_DEF added to Sales_Agent_Access\"\n else\n check_fail \"Agent Access grant: agent NOT in Sales_Agent_Access (users cannot launch agent)\"\n fi\n fi\n else\n check_warn \"Agent Access grant: BotDefinition absent — cannot verify grant\"\n fi\n\n # Is the custom permset linked into the managed PSG?\n ACCESS_PSG_ID=$(sf data query \\\n --query \"SELECT Id FROM PermissionSetGroup WHERE DeveloperName = 'SalesManagementUserPsg'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo \"\")\n\n if [[ -n \"$ACCESS_PSG_ID\" ]]; then\n COMPONENT_COUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM PermissionSetGroupComponent WHERE PermissionSetGroupId = '${ACCESS_PSG_ID}' AND PermissionSetId = '${ACCESS_PS_ID}'\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\n COMPONENT_COUNT=\"${COMPONENT_COUNT:-0}\"\n\n if [[ \"$COMPONENT_COUNT\" -ge 1 ]]; then\n check_pass \"Agent Access link: Sales_Agent_Access is a component of SalesManagementUserPsg\"\n else\n check_warn \"Agent Access link: Sales_Agent_Access not linked into SalesManagementUserPsg (may be assigned directly)\"\n fi\n fi\nfi\n\necho \"\"\n\n# --- Schedule-Triggered Flow ---\necho \"--- Schedule-Triggered Flow ---\"\n\n# Check by SourceTemplateId first\nFLOW_QUERY=$(sf data query \\\n --query \"SELECT Id, ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null)\n\nFLOW_NAME=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0].ApiName // empty' 2>/dev/null | head -1 || echo \"\")\nFLOW_ACTIVE=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0].IsActive // empty' 2>/dev/null | head -1 || echo \"\")\n\n# Fallback: check by naming convention\nif [[ -z \"$FLOW_NAME\" ]]; then\n FLOW_QUERY=$(sf data query \\\n --query \"SELECT Id, ApiName, IsActive FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=false\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null)\n\n FLOW_NAME=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0].ApiName // empty' 2>/dev/null | head -1 || echo \"\")\n FLOW_ACTIVE=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0].IsActive // empty' 2>/dev/null | head -1 || echo \"\")\nfi\n\n# Fallback: check by exact deployed name\nif [[ -z \"$FLOW_NAME\" ]]; then\n FLOW_QUERY=$(sf data query \\\n --query \"SELECT Id, ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null)\n\n FLOW_NAME=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0].ApiName // empty' 2>/dev/null | head -1 || echo \"\")\n FLOW_ACTIVE=$(echo \"$FLOW_QUERY\" | jq -r '.result.records[0].IsActive // empty' 2>/dev/null | head -1 || echo \"\")\nfi\n\nif [[ -z \"$FLOW_NAME\" ]]; then\n check_fail \"Schedule flow: NOT FOUND (must clone via Setup UI 'Save As')\"\nelif [[ \"$FLOW_ACTIVE\" == \"true\" ]]; then\n check_pass \"Schedule flow: $FLOW_NAME (ACTIVE)\"\nelse\n check_warn \"Schedule flow: $FLOW_NAME (INACTIVE — needs activation)\"\nfi\n\n# Check template exists\nTEMPLATE_EXISTS=$(sf data query \\\n --query \"SELECT Id, ApiName FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=true\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.records[0].ApiName // empty' 2>/dev/null || echo \"\")\n\nif [[ -n \"$TEMPLATE_EXISTS\" ]]; then\n check_pass \"Template flow: present\"\nelse\n check_fail \"Template flow: NOT provisioned (Pipeline Management may not be fully enabled)\"\nfi\n\necho \"\"\n\n# --- Grounding Flows (managed sales_pipe_mgmt) ---\n# Deactivated grounding flows silently kill all suggestion generation.\necho \"--- Grounding Flows ---\"\n\nGROUNDING_QUERY=$(sf data query \\\n --query \"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE NamespacePrefix='sales_pipe_mgmt' AND ApiName IN ('GetOppGroundingData','GetRcmdConvTscp')\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null)\n\nfor GROUNDING_FLOW in GetOppGroundingData GetRcmdConvTscp; do\n GROUNDING_ACTIVE=$(echo \"$GROUNDING_QUERY\" | jq -r --arg n \"$GROUNDING_FLOW\" '.result.records[] | select(.ApiName==$n) | .IsActive' 2>/dev/null | head -1)\n if [[ -z \"$GROUNDING_ACTIVE\" ]]; then\n check_fail \"Grounding flow $GROUNDING_FLOW: NOT FOUND (sales_pipe_mgmt namespace not provisioned)\"\n elif [[ \"$GROUNDING_ACTIVE\" == \"true\" ]]; then\n check_pass \"Grounding flow $GROUNDING_FLOW: ACTIVE\"\n else\n check_fail \"Grounding flow $GROUNDING_FLOW: INACTIVE (suggestions will not generate)\"\n fi\ndone\n\necho \"\"\n\n# --- Stage Descriptions ---\necho \"--- Stage Descriptions ---\"\n\nSTAGE_COUNT=$(sf data query \\\n --query \"SELECT MasterLabel FROM OpportunityStage WHERE IsActive = true\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\nSTAGE_COUNT=\"${STAGE_COUNT:-0}\"\n\nDESC_RESULT=$(sf data query \\\n --query \"SELECT Id FROM OpptStageDescription\" \\\n --target-org \"$ORG_ALIAS\" \\\n --use-tooling-api \\\n --json 2>/dev/null || echo '{\"status\":1}')\n\n# OpptStageDescription may be inaccessible if PM isn't fully enabled\nif echo \"$DESC_RESULT\" | grep -qi \"INVALID_TYPE\\|NOT_FOUND\\|sObject type.*not supported\"; then\n DESC_COUNT=\"0\"\n check_warn \"Stage descriptions: OpptStageDescription not accessible (Pipeline Management may not be fully enabled)\"\nelse\n DESC_COUNT=$(echo \"$DESC_RESULT\" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\n DESC_COUNT=\"${DESC_COUNT:-0}\"\n\n if [[ \"$DESC_COUNT\" -ge \"$STAGE_COUNT\" && \"$DESC_COUNT\" -gt 0 ]]; then\n check_pass \"Stage descriptions: $DESC_COUNT/$STAGE_COUNT stages described\"\n elif [[ \"$DESC_COUNT\" -gt 0 ]]; then\n check_warn \"Stage descriptions: $DESC_COUNT/$STAGE_COUNT stages (some missing — stage suggestions may fail)\"\n else\n check_fail \"Stage descriptions: NONE configured (stage suggestions will not work)\"\n fi\nfi\n\necho \"\"\n\n# --- Data Sources ---\necho \"--- Data Sources ---\"\n\nNOTE_COUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM ContentNote WHERE CreatedDate = LAST_N_DAYS:30\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\nNOTE_COUNT=\"${NOTE_COUNT:-0}\"\n\nif [[ \"$NOTE_COUNT\" -gt 0 ]]; then\n check_pass \"Recent notes (30d): $NOTE_COUNT\"\nelse\n check_warn \"Recent notes (30d): 0 (agent needs notes to generate suggestions)\"\nfi\n\nOPP_COUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM Opportunity WHERE IsClosed = false\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\nOPP_COUNT=\"${OPP_COUNT:-0}\"\n\nif [[ \"$OPP_COUNT\" -gt 0 ]]; then\n check_pass \"Open opportunities: $OPP_COUNT\"\nelse\n check_warn \"Open opportunities: 0 (agent needs open opps to generate suggestions)\"\nfi\n\nEMAIL_COUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM EmailMessage WHERE CreatedDate = LAST_N_DAYS:30\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo \"0\")\nEMAIL_COUNT=\"${EMAIL_COUNT:-0}\"\n\nif [[ \"$EMAIL_COUNT\" -gt 0 ]]; then\n check_pass \"Recent emails (30d): $EMAIL_COUNT\"\nelse\n check_warn \"Recent emails (30d): 0 (optional — agent can use notes instead)\"\nfi\n\necho \"\"\n\n# --- Summary ---\necho \"============================================\"\necho \" SUMMARY\"\necho \"============================================\"\necho \"\"\necho \" PASS: $PASS\"\necho \" WARN: $WARN\"\necho \" FAIL: $FAIL\"\necho \"\"\n\nif [[ \"$FAIL\" -eq 0 && \"$WARN\" -eq 0 ]]; then\n echo \" Status: FULLY CONFIGURED\"\n echo \"\"\n echo \" Pipeline Management is fully operational.\"\n echo \" Suggestions will generate on the next scheduled flow run.\"\nelif [[ \"$FAIL\" -eq 0 ]]; then\n echo \" Status: CONFIGURED WITH WARNINGS\"\n echo \"\"\n echo \" Core functionality is in place. Review warnings above.\"\nelse\n echo \" Status: INCOMPLETE — $FAIL issue(s) must be resolved\"\n echo \"\"\n echo \" Fix the FAIL items above before Pipeline Management will function.\"\n echo \"\"\n echo \" Common fixes:\"\n [[ \"$OPP_TEAM_STATUS\" != \"true\" ]] && echo \" - Enable Opportunity Team: ./enable-prerequisites.sh $ORG_ALIAS\"\n [[ \"$PM_STATUS\" != \"true\" ]] && echo \" - Enable Pipeline Management: ./enable-deal-agent.sh $ORG_ALIAS\"\n [[ -z \"$AGENT_USER\" ]] && echo \" - Create agent: ./create-agent.sh $ORG_ALIAS\"\n [[ -z \"$FLOW_NAME\" ]] && echo \" - Clone flow: Setup UI → Flows → OppSuggGenSchFlow → Save As\"\n [[ \"${DESC_COUNT:-0}\" -eq 0 ]] && echo \" - Add stage descriptions: see references/opportunity-stages.md\"\n\n # Exit with error status when FAIL > 0 (a PM BotDefinition is now REQUIRED)\n exit 1\nfi\n\necho \"\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":26581,"content_sha256":"7a29aa726ed893e39c4c41fadf8941ea3b1417ace734db0eedfd3c7216af652f"},{"filename":"scripts/verify-prompt-generation.sh","content":"#!/usr/bin/env bash\n#\n# verify-prompt-generation.sh — Verify a field's prompt template generates suggestions\n#\n# Usage:\n# bash verify-prompt-generation.sh \u003corg-alias> \u003cfield> [opportunity-id]\n#\n# Examples:\n# bash verify-prompt-generation.sh my-org NextStep\n# bash verify-prompt-generation.sh my-org Risk__c 006xx000000ABCD\n#\n# ADMIN VERIFICATION TOOL: Tests the field-completion prompt template BEHIND A FIELD\n# by calling the Einstein /generations API directly. Validates the JSON output\n# structure and shows the generated suggestion, helping admins confirm their prompts\n# work. The \u003cfield> argument mirrors the docs and setup-all.sh's echoed guidance:\n# pass the Opportunity field API name (NextStep, StageName, or a custom __c field) —\n# NOT a raw template developer name. The field is resolved to the managed/derived\n# template name here, identically to setup-all.sh Phase 4e and add-field-suggestion.sh,\n# so the namespaced managed template is always queried (a bare, un-namespaced managed\n# name returns ENTITY_IS_DELETED). A fully-qualified template dev name is still\n# accepted as-is for power users.\n#\nset -euo pipefail\n\n# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.\n# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).\nexport NO_COLOR=1\nexport FORCE_COLOR=0\n\n# ============================================================\n# Arguments\n# ============================================================\nORG_ALIAS=\"${1:-}\"\nFIELD_ARG=\"${2:-}\"\nOPP_ID=\"${3:-}\"\n# Must carry the leading \"v\" — the REST path is /services/data/v64.0/... and a\n# bare \"67.0\" produces /services/data/67.0/... which 404s. Same 64.0 API version\n# as add-field-suggestion.sh (which also stores it with the leading \"v\") and\n# setup-all.sh (which stores a bare \"64.0\" and prepends \"v\" at each call site).\nAPI_VERSION=\"v64.0\"\n\nif [[ -z \"$ORG_ALIAS\" || -z \"$FIELD_ARG\" ]]; then\n cat >&2 \u003c\u003cEOF\nUsage: $0 \u003corg-alias> \u003cfield> [opportunity-id]\n\nADMIN VERIFICATION: Verifies the prompt template behind an Opportunity field\ngenerates correct field suggestions by calling the Einstein API directly and\nvalidating the JSON response structure.\n\nArguments:\n org-alias Salesforce org alias or username\n field Opportunity field API name (e.g., NextStep, StageName, Risk__c).\n Resolved to its managed/derived prompt template automatically.\n A fully-qualified template dev name is also accepted as-is.\n opportunity-id (Optional) Opportunity ID. If omitted, a test Opportunity\n owned by the current user is created (and a grounding Note\n seeded); otherwise falls back to a user-owned open Opportunity\n (CloseDate within 90 days).\n\nExamples:\n $0 connected-seller-home NextStep\n $0 connected-seller-home Risk__c 006SB00000J73XjYAJ\n\nOutput:\n - Calls /einstein/prompt-templates/{template}/generations\n - Validates JSON structure (FieldName, SuggestedNewValue, Reasoning, etc.)\n - Reports success/failure with suggested value preview\nEOF\n exit 1\nfi\n\n# ------------------------------------------------------------\n# Resolve the field to the template dev name the /generations endpoint expects.\n# Kept inline (not sourced) and IDENTICAL to setup-all.sh Phase 4e + the\n# OOTB_MANAGED_TEMPLATE map / dev-name derivation in add-field-suggestion.sh:\n# - NextStep -> sales_pipe_mgmt__RecommendNextStepforOpp (managed)\n# - StageName -> sales_pipe_mgmt__RecommendStageforOpp (managed picklist template)\n# - custom -> Recommend\u003cField w/o __c, alnum-only>forOpp\n# A value that is already a template dev name (namespaced, or Recommend...forOpp)\n# is used verbatim so power users can still pass a raw template name.\nresolve_template_name() {\n local f=\"$1\"\n # Passthrough only well-formed template dev names (anchored regex, not a glob):\n # a namespaced managed name, or Recommend\u003calnum>forOpp. A permissive glob would\n # accept malformed input like \"RecommendZ for Opp\" and defer failure to a cryptic\n # API error; the anchor makes bad names fall through to the case map / API instead.\n if [[ \"$f\" =~ ^sales_pipe_mgmt__[A-Za-z0-9_]+$ || \"$f\" =~ ^Recommend[A-Za-z0-9]+forOpp$ ]]; then\n printf '%s' \"$f\"; return 0\n fi\n case \"$f\" in\n NextStep) printf 'sales_pipe_mgmt__RecommendNextStepforOpp' ;;\n StageName) printf 'sales_pipe_mgmt__RecommendStageforOpp' ;;\n *) printf 'Recommend%sforOpp' \"$(printf '%s' \"$f\" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')\" ;;\n esac\n}\nTEMPLATE_DEV_NAME=\"$(resolve_template_name \"$FIELD_ARG\")\"\n\n# ============================================================\n# Helper Functions\n# ============================================================\nlog_info() { echo \" [....] $*\"; }\nlog_pass() { echo \" [PASS] $*\"; }\nlog_fail() { echo \" [FAIL] $*\" >&2; }\nlog_warn() { echo \" [WARN] $*\"; }\n\ndie() {\n echo \"Error: $*\" >&2\n exit 1\n}\n\n# Shared test-opp + grounding-note resolution (kept identical to\n# flow-debug-and-verify.sh via this one sourced shared library). Defines\n# resolve_test_opportunity, which sets OPP_ID and seeds/refreshes the note.\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"$SCRIPT_DIR/shared/test-opp.sh\"\n\n# ============================================================\n# Setup\n# ============================================================\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" Pipeline Management Prompt Template Test\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\necho \"Org: $ORG_ALIAS\"\necho \"Field: $FIELD_ARG\"\necho \"Template: $TEMPLATE_DEV_NAME\"\necho \"Opportunity: ${OPP_ID:-\u003ccreate test opp>}\"\necho \"\"\n\n# ============================================================\n# Get Access Token\n# ============================================================\nlog_info \"Authenticating to org...\"\n\n# Use sf org auth show-access-token for CLI v2.x (hides tokens in org display)\nACCESS_TOKEN=$(sf org auth show-access-token --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.accessToken // empty')\nINSTANCE_URL=$(sf org display --target-org \"$ORG_ALIAS\" --json 2>/dev/null | jq -r '.result.instanceUrl // empty')\n\nif [[ -z \"$ACCESS_TOKEN\" || -z \"$INSTANCE_URL\" ]]; then\n die \"Could not retrieve access token or instance URL. Verify org alias '$ORG_ALIAS' exists.\"\nfi\n\nlog_pass \"Connected to $INSTANCE_URL\"\necho \"\"\n\n# ============================================================\n# Resolve the target Opportunity (create a user-owned test opp when omitted)\n# ============================================================\n# Pipeline Inspection is owner-scoped, so a suggestion on an opp owned by someone\n# else is invisible to the admin verifying. Prefer creating a fresh test opp owned\n# by the current user; fall back to a user-owned open opp; skip gracefully if none.\nif [[ -z \"$OPP_ID\" ]]; then\n log_info \"No opportunity given — resolving a user-owned test opportunity...\"\n\n # resolve_test_opportunity (shared lib) reuses/creates the stable [PM-TEST] opp\n # owned by the current user, falls back to a user-owned open opp, and\n # seeds/refreshes the grounding note on whichever resolved — the SAME opp + note\n # the flow-verification path uses (both scripts share this one lib).\n resolve_test_opportunity\n\n if [[ -z \"$OPP_ID\" ]]; then\n log_warn \"No eligible opportunity found for the current user.\"\n echo \" To verify: create an open Opportunity owned by you with CloseDate within 90 days,\"\n echo \" then re-run: $0 $ORG_ALIAS $FIELD_ARG \u003copportunity-id>\"\n echo \"\"\n log_info \"Skipping output verification — no opportunity available\"\n exit 0\n fi\n echo \"\"\nfi\n\n# ============================================================\n# Resolve Template Developer Name to API Name/ID\n# ============================================================\nlog_info \"Looking up prompt template '$TEMPLATE_DEV_NAME'...\"\n\n# The sf CLI wraps the REST API and handles name resolution\n# We'll use sf api request which accepts the developer name directly\necho \"\"\n\n# ============================================================\n# Call Einstein Prompt Template API\n# ============================================================\nlog_info \"Calling Einstein /generations API...\"\n\nRESPONSE_FILE=$(mktemp)\n\n# Use sf api request which handles authentication and name resolution.\n# GUARD the call with `|| CLI_EXIT=$?`: under `set -e` an unguarded non-zero exit\n# would abort the script immediately, making the `$?` capture and the error block\n# below dead code. Capturing the exit status keeps the diagnostic branch live.\nCLI_EXIT=0\nsf api request rest \"/services/data/${API_VERSION}/einstein/prompt-templates/${TEMPLATE_DEV_NAME}/generations\" \\\n --method POST \\\n --target-org \"$ORG_ALIAS\" \\\n --body \"{\\\"isPreview\\\":false,\\\"inputParams\\\":{\\\"valueMap\\\":{\\\"Input:Opportunity\\\":{\\\"value\\\":{\\\"id\\\":\\\"${OPP_ID}\\\"}}}},\\\"additionalConfig\\\":{\\\"numGenerations\\\":1,\\\"temperature\\\":0,\\\"applicationName\\\":\\\"PromptTemplateGenerationsInvocable\\\"}}\" \\\n > \"$RESPONSE_FILE\" 2>&1 || CLI_EXIT=$?\n\n# Check the CLI exit status BEFORE filtering. Two reasons:\n# 1. Under `set -e`, an unguarded sed failure below would abort the script\n# before this check runs, turning the diagnostic branch into dead code.\n# 2. On failure the raw response usually holds a non-JSON CLI error that the\n# JSON-only filter would strip — show it verbatim instead of an empty file.\nif [[ \"$CLI_EXIT\" -ne 0 ]]; then\n log_fail \"API call failed (sf CLI exit code: $CLI_EXIT)\"\n echo \"\"\n echo \"Response:\"\n cat \"$RESPONSE_FILE\"\n rm -f \"$RESPONSE_FILE\"\n exit 1\nfi\n\n# Filter out CLI warnings from response (keep only lines starting with { or [)\nsed -n '/^[{\\[]/,/^[}\\]]/p' \"$RESPONSE_FILE\" > \"${RESPONSE_FILE}.clean\"\nmv \"${RESPONSE_FILE}.clean\" \"$RESPONSE_FILE\"\n\n# Check if response file is empty or contains error\nif [[ ! -s \"$RESPONSE_FILE\" ]]; then\n log_fail \"Empty response from API\"\n rm -f \"$RESPONSE_FILE\"\n exit 1\nfi\n\n# Check if response contains error\nif jq -e '.[0].errorCode' \"$RESPONSE_FILE\" >/dev/null 2>&1; then\n ERROR_CODE=$(jq -r '.[0].errorCode' \"$RESPONSE_FILE\")\n ERROR_MSG=$(jq -r '.[0].message' \"$RESPONSE_FILE\")\n log_fail \"API returned error: $ERROR_CODE - $ERROR_MSG\"\n rm -f \"$RESPONSE_FILE\"\n exit 1\nfi\n\nlog_pass \"API call succeeded\"\necho \"\"\n\n# ============================================================\n# Extract and Validate JSON\n# ============================================================\nlog_info \"Parsing response...\"\n\n# Extract the generation text (which itself is JSON)\nGENERATION_TEXT=$(cat \"$RESPONSE_FILE\" | jq -r '.generations[0].text // empty')\n\nif [[ -z \"$GENERATION_TEXT\" ]]; then\n log_fail \"No generation text in response\"\n echo \"\"\n echo \"Full Response:\"\n cat \"$RESPONSE_FILE\" | jq '.'\n rm -f \"$RESPONSE_FILE\"\n exit 1\nfi\n\n# Parse the inner JSON\nPARSED_JSON=$(echo \"$GENERATION_TEXT\" | jq '.' 2>/dev/null || echo \"\")\n\nif [[ -z \"$PARSED_JSON\" ]]; then\n log_fail \"Generation text is not valid JSON\"\n echo \"\"\n echo \"Generation Text:\"\n echo \"$GENERATION_TEXT\"\n rm -f \"$RESPONSE_FILE\"\n exit 1\nfi\n\nlog_pass \"JSON parsing successful\"\necho \"\"\n\n# ============================================================\n# Validate Required Fields\n# ============================================================\nlog_info \"Validating JSON structure...\"\n\nREQUIRED_FIELDS=(\"FieldName\" \"SuggestedNewValue\" \"OriginalValue\" \"Reasoning\" \"DueDate\" \"Sources\")\nMISSING_FIELDS=()\n\nfor field in \"${REQUIRED_FIELDS[@]}\"; do\n if ! echo \"$PARSED_JSON\" | jq -e \".$field\" >/dev/null 2>&1; then\n MISSING_FIELDS+=(\"$field\")\n fi\ndone\n\nif [[ ${#MISSING_FIELDS[@]} -gt 0 ]]; then\n log_fail \"Missing required fields: ${MISSING_FIELDS[*]}\"\n echo \"\"\n echo \"Generated JSON:\"\n echo \"$PARSED_JSON\" | jq '.'\n rm -f \"$RESPONSE_FILE\"\n exit 1\nfi\n\nlog_pass \"All required fields present\"\n\n# Validate Sources is an array\nSOURCES_COUNT=$(echo \"$PARSED_JSON\" | jq '.Sources | length // 0')\nif [[ \"$SOURCES_COUNT\" -eq 0 ]]; then\n log_warn \"Sources array is empty (no citations)\"\nelse\n log_pass \"Sources array contains $SOURCES_COUNT citation(s)\"\nfi\n\necho \"\"\n\n# ============================================================\n# Display Results\n# ============================================================\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" ✅ TEST PASSED - AI Suggestion Generated\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\n\nFIELD_NAME=$(echo \"$PARSED_JSON\" | jq -r '.FieldName')\nSUGGESTED_VALUE=$(echo \"$PARSED_JSON\" | jq -r '.SuggestedNewValue')\nREASONING=$(echo \"$PARSED_JSON\" | jq -r '.Reasoning')\nDUE_DATE=$(echo \"$PARSED_JSON\" | jq -r '.DueDate')\n\necho \"Field: $FIELD_NAME\"\necho \"Suggested Value: $SUGGESTED_VALUE\"\necho \"Reasoning: $REASONING\"\necho \"Due Date: $DUE_DATE\"\necho \"Sources: $SOURCES_COUNT citation(s)\"\necho \"\"\n\n# Character length check\nCHAR_COUNT=${#SUGGESTED_VALUE}\necho \"Length Check: $CHAR_COUNT characters\"\n\nif [[ $CHAR_COUNT -gt 255 ]]; then\n log_warn \"Suggested value exceeds 255 characters (may be truncated on field write)\"\nfi\n\necho \"\"\necho \"Full JSON:\"\necho \"$PARSED_JSON\" | jq '.'\necho \"\"\n\n# Save response for inspection\nSAVE_PATH=\"/tmp/prompt-test-${TEMPLATE_DEV_NAME}-$(date +%Y%m%d-%H%M%S).json\"\necho \"$PARSED_JSON\" | jq '.' > \"$SAVE_PATH\"\necho \"💾 Full response saved to: $SAVE_PATH\"\n\nrm -f \"$RESPONSE_FILE\"\n\necho \"\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \" Test Complete\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":14179,"content_sha256":"5b80c3e3e5aca1062db8e78af1554bb3f002c15b46c7fe40fdea8f958c24d328"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Agentforce Pipeline Management Configuration","type":"text"}]},{"type":"paragraph","content":[{"text":"Configure Agentforce Pipeline Management end-to-end in a Salesforce org. This skill handles both greenfield orgs (zero configuration) and partially configured orgs that need repair or completion.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scope","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In scope","type":"text","marks":[{"type":"strong"}]},{"text":": Enabling Pipeline Management, configuring the Sales Management agent, activating/customizing flows (Process Field Update Suggestions, Get Opportunity Grounding Data, Get AI Recommendations from Call Transcripts), prompt template configuration, permission set assignment, data source setup, opportunity stage descriptions, autonomous field update configuration, and partial-org repair.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Out of scope","type":"text","marks":[{"type":"strong"}]},{"text":": Building custom agents from scratch (use ","type":"text"},{"text":"agentforce-generate","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Einstein Conversation Insights or Einstein Activity Capture initial provisioning (those are separate products). Slack app installation. Custom Apex triggers on Opportunity Team Member.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"You must verify (scripts cannot check or change these)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Edition","type":"text","marks":[{"type":"strong"}]},{"text":": Enterprise, Performance, Unlimited, or Developer Edition with Agentforce for Sales add-on (or Agentforce 1 Sales Edition). Setup ","type":"text"},{"text":"probes","type":"text","marks":[{"type":"strong"}]},{"text":" this up front (Step 0) — it fails fast with a license/edition message on an org that can't run Pipeline Management, before asking anything — but edition/license provisioning itself is still admin-owned.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User permissions","type":"text","marks":[{"type":"strong"}]},{"text":": The executing user needs ","type":"text"},{"text":"View Setup","type":"text","marks":[{"type":"code_inline"}]},{"text":" AND (","type":"text"},{"text":"Modify All Data","type":"text","marks":[{"type":"code_inline"}]},{"text":" OR ","type":"text"},{"text":"Customize Application","type":"text","marks":[{"type":"code_inline"}]},{"text":"), ","type":"text"},{"text":"Manage AI Agents","type":"text","marks":[{"type":"code_inline"}]},{"text":" AND (","type":"text"},{"text":"Manage Agentforce Employee Agents","type":"text","marks":[{"type":"code_inline"}]},{"text":" OR ","type":"text"},{"text":"Customize Application","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and ","type":"text"},{"text":"Assign Permission Sets","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Enabled automatically by ","type":"text"},{"text":"scripts/setup-all.sh","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"The following settings are toggled by setup automatically — do not enable them manually before running the script.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Einstein Generative AI","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"EinsteinGptSettings.enableEinsteinGptPlatform","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":"; must be on before Agentforce Agent.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agentforce Agent","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"EinsteinCopilotSettings.enableEinsteinGptCopilot","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":"; depends on Einstein Generative AI.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agentforce Studio / Agent Platform","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"AgentPlatformSettings.enableAgentPlatform","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":"; depends on Agentforce Agent and gates the Deal Agent. Core rejects ","type":"text"},{"text":"SalesDealAgentSettings.enableDealAgent","type":"text","marks":[{"type":"code_inline"}]},{"text":" when this is off, surfacing as an opaque Deal Agent activation failure.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enhanced Notes","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"EnhancedNotesSettings.enableEnhancedNotes","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":"; Pipeline Management uses ContentNote as a primary data source.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Opportunity Team","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"OpportunitySettings.enableOpportunityTeam","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":"; the suggestion flow uses OpportunityTeamMember and will fail deployment without it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pipeline Inspection","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"OpportunitySettings.enablePipelineInspection","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":"; provides the UI where reps view and accept/dismiss suggestions. Suggestions still generate without it, but users have nowhere to see them.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enhanced Email","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"EmailAdministrationSettings.enableEnhancedEmailEnabled","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"optional but recommended","type":"text","marks":[{"type":"strong"}]},{"text":"; only needed for email-body indexing when Einstein Activity Capture is in use.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pipeline Management","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"SalesDealAgentSettings.enableDealAgent","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":"; the feature itself.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Clarifying Questions","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Run the license preflight FIRST — before asking any of these questions.","type":"text","marks":[{"type":"strong"}]},{"text":" As your very first action (right after resolving the org alias), run ","type":"text"},{"text":"bash scripts/setup-all.sh \u003corg-alias> --check-license","type":"text","marks":[{"type":"code_inline"}]},{"text":". It runs only auth + the capability gate, asks nothing, and changes nothing. ","type":"text"},{"text":"If it exits non-zero","type":"text","marks":[{"type":"strong"}]},{"text":", relay the printed license/edition blocker to the user and ","type":"text"},{"text":"STOP","type":"text","marks":[{"type":"strong"}]},{"text":" — do not ask any clarifying question and make no changes. Only when it exits 0 (org is capable — greenfield or already provisioned) proceed to the questions below.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Once the preflight passes, determine:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Greenfield or repair?","type":"text","marks":[{"type":"strong"}]},{"text":" Is this a new org that has never had Pipeline Management, or one with partial configuration?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data sources","type":"text","marks":[{"type":"strong"}]},{"text":": Which data sources should inform the agent?","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Notes (enabled by default)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Emails (requires Einstein Activity Capture)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Voice/Video calls (requires Einstein Conversation Insights)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enhanced Email (for email body indexing — requires ","type":"text"},{"text":"EmailAdministrationSettings.enableEnhancedEmailEnabled","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Autonomous updates","type":"text","marks":[{"type":"strong"}]},{"text":": Should the agent update opportunity fields autonomously, or only suggest updates for user review?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Which fields should the agent manage? (ask this FIRST, up front — setup is field-selection-driven).","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" builds the flow with ","type":"text"},{"text":"only the fields you select","type":"text","marks":[{"type":"strong"}]},{"text":", from the start — there is no deploy-both-then-strip step. Ask: ","type":"text"},{"text":"\"Which Opportunity fields should the agent suggest values for? The OOTB options are Next Step (","type":"text","marks":[{"type":"em"}]},{"text":"NextStep","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":") and Opportunity Stage (","type":"text","marks":[{"type":"em"}]},{"text":"StageName","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":"); you can also add custom text fields (e.g. a Competitor Analysis field).\"","type":"text","marks":[{"type":"em"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"At least 1 field is REQUIRED","type":"text","marks":[{"type":"strong"}]},{"text":" — setup aborts (exit 1) if zero fields are selected. There is ","type":"text"},{"text":"no","type":"text","marks":[{"type":"strong"}]},{"text":" default to ","type":"text"},{"text":"NextStep","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hard cap: 5 fields total","type":"text","marks":[{"type":"strong"}]},{"text":"; OOTB fields count toward it. Selecting more aborts before any change.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pass the set with ","type":"text"},{"text":"--fields \"NextStep,StageName,Risk__c\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" in non-interactive runs, or type it at the interactive prompt.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Per-field prompt customization (all fields EXCEPT ","type":"text","marks":[{"type":"strong"}]},{"text":"StageName","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":").","type":"text","marks":[{"type":"strong"}]},{"text":" For each selected non-","type":"text"},{"text":"StageName","type":"text","marks":[{"type":"code_inline"}]},{"text":" field, setup collects an optional ","type":"text"},{"text":"goal","type":"text","marks":[{"type":"strong"}]},{"text":" (\"You must think about…\") and ","type":"text"},{"text":"instruction","type":"text","marks":[{"type":"strong"}]},{"text":" (extraction guidance):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NextStep","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" ships with a curated OOTB managed prompt. Setup only overrides it when you supply a goal/instruction (interactive: it asks whether to customize; non-interactive: pass ","type":"text"},{"text":"--field-goal \"NextStep:…\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--field-instruction \"NextStep:…\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Skip customization to keep the OOTB prompt.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Custom fields","type":"text","marks":[{"type":"strong"}]},{"text":" always get a goal/instruction (your text, or the script's sensible defaults).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"StageName","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is NOT customizable","type":"text","marks":[{"type":"strong"}]},{"text":" — it is a managed picklist template with no override support; setup never prompts for it.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Custom field eligibility","type":"text","marks":[{"type":"strong"}]},{"text":": Only standard or custom ","type":"text"},{"text":"text","type":"text","marks":[{"type":"strong"}]},{"text":" fields on Opportunity are supported — plain Text (","type":"text"},{"text":"type=string","type":"text","marks":[{"type":"code_inline"}]},{"text":") or Text Area ≤ 255 (","type":"text"},{"text":"type=textarea","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"htmlFormatted=false","type":"text","marks":[{"type":"code_inline"}]},{"text":"); length ≤ 255; not Long Text Area (length > 255), Rich Text Area (","type":"text"},{"text":"htmlFormatted=true","type":"text","marks":[{"type":"code_inline"}]},{"text":"), picklist, or formula. The suggestion is capped at the field's own length. Fields selected in question 4 are wired during ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"additional","type":"text","marks":[{"type":"strong"}]},{"text":" fields added ","type":"text"},{"text":"after","type":"text","marks":[{"type":"em"}]},{"text":" setup use ","type":"text"},{"text":"scripts/add-field-suggestion.sh \u003corg-alias> \u003cFieldApiName>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (one per field; see Phase 4.3). Both paths enforce the 5-total cap — ","type":"text"},{"text":"add-field-suggestion.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" refuses to wire a 6th field.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Opportunity stages","type":"text","marks":[{"type":"strong"}]},{"text":": Opportunity stages are picklist values, and different Opportunity record types may expose different stage subsets. Are standard or custom opportunity stages in use? Do their descriptions need to be defined/updated? Note: ","type":"text"},{"text":"OpptStageDescription","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"global-per-stage","type":"text","marks":[{"type":"strong"}]},{"text":" — keyed only by ","type":"text"},{"text":"OpportunityStageApiName","type":"text","marks":[{"type":"code_inline"}]},{"text":", with no per-record-type column — so a description is written ","type":"text"},{"text":"once per active stage","type":"text","marks":[{"type":"strong"}]},{"text":" and applies across every record type that exposes that stage. Setup works the same whether or not the org has Opportunity record types.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Admin Communication Guidelines","type":"text"}]},{"type":"paragraph","content":[{"text":"CRITICAL","type":"text","marks":[{"type":"strong"}]},{"text":": This skill serves admin users, not developers. Follow these rules:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run all bash commands in background","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"run_in_background: true","type":"text","marks":[{"type":"code_inline"}]},{"text":") - this includes the main setup commands AND any log-tailing for progress monitoring","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Monitor progress without showing commands","type":"text","marks":[{"type":"strong"}]},{"text":" - You can tail logs to provide progress updates, but run tail commands in background too. Parse the output and show only friendly updates like \"✓ Opportunity Team: enabled\". Never show the tail command itself or raw log lines.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Investigate errors silently","type":"text","marks":[{"type":"strong"}]},{"text":" - When something fails, do your diagnosis (check data, run queries, fix environment issues) in background without narrating each step. Only surface the conclusion and action: \"I need to create a test opportunity first. Let me do that...\" NOT \"The --json output is corrupted... let me check... ANSI color codes... let me fix...\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Narrate in plain language","type":"text","marks":[{"type":"strong"}]},{"text":": Say \"Setting up the platform...\" NOT \"Running setup-all.sh Phase 1.5\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hide technical details","type":"text","marks":[{"type":"strong"}]},{"text":": No SOAP responses, curl commands, jq parsing, phase numbers, or script paths unless user asks to debug","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check exit codes","type":"text","marks":[{"type":"strong"}]},{"text":": Always parse success/failure and translate technical errors to admin-friendly messages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set time expectations","type":"text","marks":[{"type":"strong"}]},{"text":": \"This takes ~3 minutes...\" prevents \"is it frozen?\" questions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Handle errors gracefully","type":"text","marks":[{"type":"strong"}]},{"text":": Auto-retry transient failures; explain and offer choices for real blockers","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/admin-communication.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for detailed error translation patterns, examples, and recovery strategies.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow","type":"text"}]},{"type":"paragraph","content":[{"text":"IMPORTANT — Primary Entry Point","type":"text","marks":[{"type":"strong"}]},{"text":": For end-to-end setup, drive ","type":"text"},{"text":"scripts/setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":". It handles all prerequisites, enablement, flow deployment, permissions, and verification in the correct dependency order. Only use individual scripts when diagnosing or repairing specific components after ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" has run.","type":"text"}]},{"type":"paragraph","content":[{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a ","type":"text"},{"text":"3-phase, field-selection-driven","type":"text","marks":[{"type":"strong"}]},{"text":" setup: (1) platform enablement + agent user + PSG assignment; (2) field selection → prompt template creation/activation → stage descriptions (only if ","type":"text"},{"text":"StageName","type":"text","marks":[{"type":"code_inline"}]},{"text":" selected) → prompt verification; (3) flow build (selected fields only), flow activation, agent activation, PSG recalc. It is ","type":"text"},{"text":"safe by default","type":"text","marks":[{"type":"strong"}]},{"text":": suggestions-only mode, no stage-description creation unless asked, and the flow does not go live until the prompts are in place.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Canonical agent path — two calls with an interactive tune-loop between them","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/canonical-agent-path.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full two-call pattern with code examples, agent rules, narration templates, and internal step map.","type":"text"}]},{"type":"paragraph","content":[{"text":"Summary","type":"text","marks":[{"type":"strong"}]},{"text":": Split setup into Call 1 (","type":"text"},{"text":"--through-phase prompts","type":"text","marks":[{"type":"code_inline"}]},{"text":") and Call 2 (","type":"text"},{"text":"--from-phase flow","type":"text","marks":[{"type":"code_inline"}]},{"text":") with a tune-loop between them where you drive prompt approval per field. Call 2 is MANDATORY — without it no flow exists and no suggestions generate.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Unattended / CI path (single call, no tune-loop)","type":"text"}]},{"type":"paragraph","content":[{"text":"For fully unattended runs (cron/CI, or when the user explicitly declines prompt review):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash setup-all.sh \u003corg-alias> --fields \"NextStep,StageName,Risk__c\" \\\n [--field-goal \"NextStep:\u003ctext>\"] [--field-instruction \"NextStep:\u003ctext>\"] \\\n [--autonomous] [--create-stage-descriptions] [--skip-prompt-verification] \\\n --non-interactive [--users \"[email protected],[email protected]\"]","type":"text"}]},{"type":"paragraph","content":[{"text":"In single-pass mode the verification step still prints each field's suggestion (informational). Relay them to the user.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Flags","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/flags.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for complete flag documentation. Key flags:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--check-license","type":"text","marks":[{"type":"code_inline"}]},{"text":" — preflight only (exit 0 = capable, 1 = blocker). Run FIRST.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--through-phase prompts","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--from-phase flow","type":"text","marks":[{"type":"code_inline"}]},{"text":" — split-run endpoints (mutually exclusive)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--fields \"NextStep,StageName\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — required field set (at least 1, max 5)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--autonomous","type":"text","marks":[{"type":"code_inline"}]},{"text":" — enable auto-apply mode (off by default)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--non-interactive","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--yes","type":"text","marks":[{"type":"code_inline"}]},{"text":" — unattended mode (requires ","type":"text"},{"text":"--fields","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--users \"[email protected],[email protected]\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — explicit user list for PSG assignment (email-validated)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The phases below explain the decision logic for manual/repair scenarios. See ","type":"text"},{"text":"references/setup-order.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for step-by-step details.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Automation Summary","type":"text"}]},{"type":"paragraph","content":[{"text":"Every configuration step and whether it's CLI-automatable (and how) or UI-only lives in ","type":"text"},{"text":"references/automation-matrix.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". In short: prerequisites, enablement, flow clone/activation, agent publish/activation, permissions, field suggestions, and stage descriptions are all CLI-automatable (","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" does them in dependency order); only Agent Analytics is UI-only.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 0 — Authentication and Org Assessment","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Authenticate","type":"text","marks":[{"type":"strong"}]},{"text":" — See ","type":"text"},{"text":"references/auth-and-cli.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Always use ","type":"text"},{"text":"2>/dev/null","type":"text","marks":[{"type":"code_inline"}]},{"text":" on ","type":"text"},{"text":"sf --json | jq","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Assess org state","type":"text","marks":[{"type":"strong"}]},{"text":" — Branching signals (NOT PSG existence — PSGs ship with the license even in unconfigured orgs):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enablement","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"SalesDealAgentSettings.enableDealAgent","type":"text","marks":[{"type":"code_inline"}]},{"text":" via SOAP readMetadata","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setup-has-run","type":"text","marks":[{"type":"strong"}]},{"text":" (any of): (a) agent user holds ","type":"text"},{"text":"SalesManagementAgentUserPsg","type":"text","marks":[{"type":"code_inline"}]},{"text":", (b) flow ","type":"text"},{"text":"Process_Field_Update_Suggestions","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists (by ApiName, not label), (c) ","type":"text"},{"text":"BotDefinition","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"AgentTemplate IN ('SalesMgmt__NGASalesAgent','SalesMgmt__SalesAgent')","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text","marks":[{"type":"strong"}]},{"text":"scripts/setup-all.sh \u003corg-alias>","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — handles branching automatically:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No enablement + no signals → greenfield (Phase 1)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Partial signals → repair (Phase 3)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"All present → verification only","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 1 — Enable Prerequisites and Pipeline Management","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text","marks":[{"type":"strong"}]},{"text":"references/setup-order.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" for complete scripts.","type":"text","marks":[{"type":"strong"}]},{"text":" Enable in dependency order: Einstein GenAI → Agentforce Agent → Agent Platform → Enhanced Notes → Opportunity Team → Pipeline Management → Pipeline Inspection. Then deploy the flow (","type":"text"},{"text":"scripts/create-flow.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":") and verify agent architecture (","type":"text"},{"text":"references/agent-creation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]},{"type":"paragraph","content":[{"text":"What auto-creates on enablement","type":"text","marks":[{"type":"strong"}]},{"text":": agent user + PSGs (auto-assigned). ","type":"text"},{"text":"What you must create","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"BotDefinition:SalesAgent","type":"text","marks":[{"type":"code_inline"}]},{"text":" (via authoring-bundle publish) and the schedule-triggered flow (via template deploy).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 2 — Assign Permissions","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Define Agent Access","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"bash scripts/define-agent-access.sh \u003corg-alias>","type":"text","marks":[{"type":"code_inline"}]},{"text":" — creates custom permset, links into both PSGs. See ","type":"text"},{"text":"references/agent-creation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → \"Agent Access\".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Assign PSG to target users","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"HARD RULE — never bulk-assign.","type":"text","marks":[{"type":"strong"}]},{"text":" Grant ","type":"text"},{"text":"SalesManagementUserPsg","type":"text","marks":[{"type":"code_inline"}]},{"text":" only to: (1) the running user, and (2) explicit ","type":"text"},{"text":"--users","type":"text","marks":[{"type":"code_inline"}]},{"text":" list. No enumerate-and-grant.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 3 — Repair Mode (Partially Configured Org)","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/repair-diagnostics.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full checklist. Common issues: flow deactivated → Tooling API ","type":"text"},{"text":"PATCH","type":"text","marks":[{"type":"code_inline"}]},{"text":"; agent not active → ","type":"text"},{"text":"sf agent activate","type":"text","marks":[{"type":"code_inline"}]},{"text":"; prompt template inactive → version round-trip redeploy; agent missing → SOAP toggle off/on.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 4 — Customize (Admin Decisions Required)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Define Opportunity Stage Descriptions","type":"text","marks":[{"type":"strong"}]},{"text":" — Use the \"propose and correct\" pattern from ","type":"text"},{"text":"references/opportunity-stages.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Query active stages, present defaults from reference file, apply user corrections, bulk-create via Tooling API.","type":"text"}]},{"type":"paragraph","content":[{"text":"IMPORTANT","type":"text","marks":[{"type":"strong"}]},{"text":": Stage descriptions MAY be auto-provisioned when Pipeline Management is enabled (test orgs showed MEDDIC descriptions pre-populated). ","type":"text"},{"text":"Always CHECK for existing descriptions BEFORE attempting to create them","type":"text","marks":[{"type":"strong"}]},{"text":". If descriptions already exist, UPDATE rather than CREATE to avoid duplicates.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Configure additional data sources","type":"text","marks":[{"type":"strong"}]},{"text":" — See ","type":"text"},{"text":"references/data-sources.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for decision table (Einstein Conversation Insights, Einstein Activity Capture, Inbox).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add custom opportunity field suggestions (AFTER setup)","type":"text","marks":[{"type":"strong"}]},{"text":" — Fields chosen at setup time are wired by ","type":"text"},{"text":"setup-all.sh --fields","type":"text","marks":[{"type":"code_inline"}]},{"text":". To add a ","type":"text"},{"text":"new","type":"text","marks":[{"type":"strong"}]},{"text":" field to an already-configured org, run ","type":"text"},{"text":"scripts/add-field-suggestion.sh \u003corg-alias> \u003cFieldApiName>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (e.g. a ","type":"text"},{"text":"Risk__c","type":"text","marks":[{"type":"code_inline"}]},{"text":" field). The script validates the field, fills the canonical template (","type":"text"},{"text":"assets/field-completion-template.genAiPromptTemplate-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":"), deploys and activates it via the version round-trip, and idempotently wires the field into the live ","type":"text"},{"text":"Process_Field_Update_Suggestions","type":"text","marks":[{"type":"code_inline"}]},{"text":" flow (respecting the 5-field cap). Optional flags: ","type":"text"},{"text":"--label","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--goal","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--instruction","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the two field-specific prompt lines), ","type":"text"},{"text":"--verify-with-note","type":"text","marks":[{"type":"code_inline"}]},{"text":" (seed a Note + run a synchronous generation), ","type":"text"},{"text":"--opp \u003cId>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--skip-flow","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--force","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"references/field-completion-prompts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for mechanics and ","type":"text"},{"text":"references/field-completion-prompt-template.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for writing the goal/instruction lines.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Naming note","type":"text","marks":[{"type":"strong"}]},{"text":": the prompt-template ","type":"text"},{"text":"\u003ctype>","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"einstein_gpt__fieldCompletion","type":"text","marks":[{"type":"code_inline"}]},{"text":" (used in all metadata — never change it). The ","type":"text"},{"text":"Setup UI","type":"text","marks":[{"type":"strong"}]},{"text":" (Prompt Builder) labels this category ","type":"text"},{"text":"\"Field Generation\"","type":"text","marks":[{"type":"strong"}]},{"text":" — same thing. Admins should look for \"Field Generation\", not \"Field Completion\".","type":"text"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Configure autonomous updates","type":"text","marks":[{"type":"strong"}]},{"text":" — Autonomous mode is a single ","type":"text"},{"text":"org-wide","type":"text","marks":[{"type":"strong"}]},{"text":" toggle (","type":"text"},{"text":"SalesDealAgentSettings.enableDealAgentAutoApproveAllTasks","type":"text","marks":[{"type":"code_inline"}]},{"text":"), not a per-field setting: when ON, the agent auto-applies all field suggestions without review; when OFF (the default), every suggestion waits for human approval. It is opt-in via ","type":"text"},{"text":"setup-all.sh --autonomous","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"references/autonomous-updates.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the toggle and safety considerations.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enable Agent Analytics","type":"text","marks":[{"type":"strong"}]},{"text":" — Setup → Einstein Feedback and Monitoring → Agent Analytics (UI-only).","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Understanding the Flows","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/flow-clone-from-template.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for complete flow architecture, clone approach, and technical constraints.","type":"text"}]},{"type":"paragraph","content":[{"text":"Key facts","type":"text","marks":[{"type":"strong"}]},{"text":": Pipeline Management uses a schedule-triggered flow (","type":"text"},{"text":"Process_Field_Update_Suggestions","type":"text","marks":[{"type":"code_inline"}]},{"text":") cloned from the managed template. Detection is by ApiName (not label). The flow calls ","type":"text"},{"text":"getOrExecFieldUpdtSuggestion","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the selected fields — adding a field requires both activating its template AND wiring it into the flow's collection (handled atomically by ","type":"text"},{"text":"add-field-suggestion.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"). ","type":"text"},{"text":"No suggestions generate until this flow is active.","type":"text","marks":[{"type":"strong"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Considerations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pipeline Management REQUIRES a standalone ","type":"text","marks":[{"type":"strong"}]},{"text":"BotDefinition:SalesAgent","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — a missing BotDefinition blocks setup. Auto-provisioning creates it in most editions, but where it doesn't, ","type":"text"},{"text":"publish_and_activate_agent()","type":"text","marks":[{"type":"code_inline"}]},{"text":" (in ","type":"text"},{"text":"shared/agent-bundle-publish.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":", called by ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"create-agent.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":") publishes the bundle from ","type":"text"},{"text":"assets/sales_management_agent.agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" and activates it. Do NOT rely on the ","type":"text"},{"text":"EmployeeCopilotPlanner","type":"text","marks":[{"type":"code_inline"}]},{"text":" fallback — users need the interactive chat surface.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Flow runs only when agent is ","type":"text"},{"text":"active","type":"text","marks":[{"type":"strong"}]},{"text":" with ","type":"text"},{"text":"Read/Write","type":"text","marks":[{"type":"strong"}]},{"text":" on opportunities; agent joins as opportunity team member (watch for Apex triggers on OpportunityTeamMember)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Daily limit: 8,000 LLM requests / ~4,000 opportunities. Suggestions expire after 30 days","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stage template reads ","type":"text"},{"text":"OpptStageDescription","type":"text","marks":[{"type":"code_inline"}]},{"text":" via ","type":"text"},{"text":"GetOpportunityStageDetailsInvocableAction","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"stage suggestions fail","type":"text","marks":[{"type":"strong"}]},{"text":" if any active stage lacks a description","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do NOT use CLI Metadata deploy","type":"text","marks":[{"type":"strong"}]},{"text":" for DealAgent enablement — SOAP API v64.0 only (CLI has silent failure mode)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do NOT enable ","type":"text","marks":[{"type":"strong"}]},{"text":"BotSettings","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":"/","type":"text","marks":[{"type":"strong"}]},{"text":"enableBots","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — legacy messaging bots, unrelated, fails with \"Legal Terms\" error","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pipeline Management does NOT use a managed package — components provisioned directly on enablement","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Toggle not visible in Setup → verify Sales Cloud EE+ license and Agentforce SKU","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always use ","type":"text"},{"text":"2>/dev/null","type":"text","marks":[{"type":"code_inline"}]},{"text":" on ","type":"text"},{"text":"sf ... --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" piped to jq (strips CLI warnings that corrupt JSON)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/setup-order.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Complete copy-paste setup sequence with all scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/soap-api-enablement.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — SOAP API patterns for all prerequisites","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/flow-clone-from-template.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Flow clone approach with ApiName-based detection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/agent-creation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Agent creation (auto-provisioning + authoring-bundle publish; the SOAP toggle only re-provisions the agent user and PSGs, not the BotDefinition)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/field-completion-prompts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Adding custom field suggestions end-to-end (verified v67 pattern, the three requirements, ","type":"text"},{"text":"add-field-suggestion.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" mechanics, verification)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/field-completion-prompt-template.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Writing the field-specific goal/instruction lines the script consumes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/opportunity-stages.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Stage descriptions + methodologies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/repair-diagnostics.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Partially configured org handling","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/auth-and-cli.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Auth methods, CLI compatibility","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/data-sources.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Decision table for EAC/ECI/Notes/Enhanced Email","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/autonomous-updates.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Auto-apply mode toggle and safety considerations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/metadata-inventory.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Complete inventory of provisioned metadata components","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/automation-matrix.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Which setup steps are CLI-automatable (and how) vs UI-only","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/admin-communication.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Admin-friendly narration patterns: error-translation table, silent-investigation examples, and per-step narration templates for driving setup conversationally","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Examples","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"examples/custom-prompt-instructions.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Custom prompt instruction patterns for methodology alignment and update aggressiveness control","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Standalone Scripts","type":"text"}]},{"type":"paragraph","content":[{"text":"Executable scripts for each setup phase (run from ","type":"text"},{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory with ","type":"text"},{"text":"sfdx-project.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" in CWD):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"Master orchestration","type":"text","marks":[{"type":"strong"}]},{"text":": 3-phase, field-selection-driven end-to-end setup (auth + enablement + agent user/PSG → field selection + prompt templates + stage descriptions + prompt-verification gate → flow build/activate + agent activate + PSG recalc). Requires ","type":"text"},{"text":"--fields","type":"text","marks":[{"type":"code_inline"}]},{"text":" (aborts if none); interactive by default; safe defaults (suggestions-only, no stage-description creation) with ","type":"text"},{"text":"--field-goal","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"--field-instruction","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--autonomous","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--create-stage-descriptions","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--skip-prompt-verification","type":"text","marks":[{"type":"code_inline"}]},{"text":" opt-in flags and ","type":"text"},{"text":"--non-interactive","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"--yes","type":"text","marks":[{"type":"code_inline"}]},{"text":" for unattended runs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/shared/flow-builder.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — flow field-collection helpers sourced by ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"build_field_collection","type":"text","marks":[{"type":"code_inline"}]},{"text":" (greenfield — writes a flow wiring only the selected fields), plus ","type":"text"},{"text":"strip_flow_field","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add_flow_field","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"flow_wired_fields","type":"text","marks":[{"type":"code_inline"}]},{"text":" (repair — reconcile an already-deployed flow's fields in place)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/shared/stage-descriptions.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Phase 4c.5 stage-description creation sourced by ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"; exports ","type":"text"},{"text":"run_stage_descriptions()","type":"text","marks":[{"type":"code_inline"}]},{"text":" (self-gates on ","type":"text"},{"text":"STAGENAME_SELECTED","type":"text","marks":[{"type":"code_inline"}]},{"text":"; sets ","type":"text"},{"text":"STAGE_DESCRIPTIONS_BLOCKED","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the Phase 8.5 summary)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/shared/test-opp.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — shared test-opportunity + grounding-note resolution sourced by both verify scripts (","type":"text"},{"text":"flow-debug-and-verify.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"verify-prompt-generation.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"); exports ","type":"text"},{"text":"resolve_test_opportunity","type":"text","marks":[{"type":"code_inline"}]},{"text":" (reuse/create/fallback the ","type":"text"},{"text":"[PM-TEST]","type":"text","marks":[{"type":"code_inline"}]},{"text":" test opp, roll CloseDate, seed/refresh the grounding note) and ","type":"text"},{"text":"seed_grounding_note","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/retrieve-settings.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Audit current org settings state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/enable-prerequisites.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Enable prerequisite ","type":"text"},{"text":"settings only","type":"text","marks":[{"type":"strong"}]},{"text":" (Einstein GenAI, Agentforce, Enhanced Notes, Opportunity Team) via SOAP — no fields/flow/agent. ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" runs this same enablement inline; use the standalone script only for surgical, non-destructive prerequisite toggling","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/enable-deal-agent.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Enable Pipeline Management via SOAP API (standalone helper; ","type":"text"},{"text":"setup-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" enables PM inline — use this only for isolated manual toggling)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/create-flow.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Detect, deploy from template, or activate suggestion flow (falls back to UI guidance)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/create-agent.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Verify/create/activate agent (auto-provisioning + authoring-bundle publish; SOAP toggle only re-provisions agent user/PSGs, not the BotDefinition); also defines Agent Access on completion","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/define-agent-access.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Define Agent Access so both users holding ","type":"text"},{"text":"SalesManagementUserPsg","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the autonomous agent user holding ","type":"text"},{"text":"SalesManagementAgentUserPsg","type":"text","marks":[{"type":"code_inline"}]},{"text":" can launch/run the agent (custom permset + ","type":"text"},{"text":"SetupEntityAccess","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"PermissionSetGroupComponent","type":"text","marks":[{"type":"code_inline"}]},{"text":" into both PSGs + recalc)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/deploy-settings.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Deploy non-SOAP settings (Notes, Email, Opportunity)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/add-field-suggestion.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Add an AI field-completion suggestion for any eligible Opportunity text field (validate → fill canonical template → deploy + activate via version round-trip → wire flow; ","type":"text"},{"text":"--verify-with-note","type":"text","marks":[{"type":"code_inline"}]},{"text":" for synchronous generation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/verify-all.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Comprehensive configuration status check (read-only)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Admin Verification Tools","type":"text"}]},{"type":"paragraph","content":[{"text":"The following scripts help admins verify that Pipeline Management is working correctly after setup. Use these to confirm field suggestions are generating as expected.","type":"text"}]},{"type":"paragraph","content":[{"text":"Before running either verify script, compose a realistic grounding note and pass it via the ","type":"text","marks":[{"type":"strong"}]},{"text":"PM_GROUNDING_NOTE","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" env var.","type":"text","marks":[{"type":"strong"}]},{"text":" The AI only generates a suggestion when the opportunity's Note carries real sales context — a generic placeholder grounds on nothing and returns empty, making a working setup look broken. Write a few sentences of a plausible sales call/meeting reflecting the configured ","type":"text"},{"text":"--fields","type":"text","marks":[{"type":"code_inline"}]},{"text":" and any ","type":"text"},{"text":"--field-goal","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"--field-instruction","type":"text","marks":[{"type":"code_inline"}]},{"text":" (e.g. concrete next steps and deadlines for ","type":"text"},{"text":"NextStep","type":"text","marks":[{"type":"code_inline"}]},{"text":"; buyer, budget, pain, and timeline signals for ","type":"text"},{"text":"StageName","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Do not use a rigid template — tailor it to what was configured. Example: ","type":"text"},{"text":"PM_GROUNDING_NOTE=\"Discovery call with the VP of Sales: confirmed a $250K budget and a Q3 go-live; agreed next steps are a technical deep-dive with IT and an ROI analysis.\" bash scripts/flow-debug-and-verify.sh \u003corg>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Absent the env var, the scripts fall back to a generic note.","type":"text"}]},{"type":"paragraph","content":[{"text":"Both scripts are invoked separately by the skill but converge on the SAME test opportunity and the SAME grounding note.","type":"text","marks":[{"type":"strong"}]},{"text":" They both find-or-create a user-owned opp named ","type":"text"},{"text":"[PM-TEST] Test Pipeline Verification Opp","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the ","type":"text"},{"text":"[PM-TEST]","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix makes every opp this tooling creates findable via ","type":"text"},{"text":"Opportunity WHERE Name LIKE '[PM-TEST]%'","type":"text","marks":[{"type":"code_inline"}]},{"text":"). On reuse they roll the opp's CloseDate +30 days so it stays flow-eligible, and refresh the grounding note only when the ","type":"text"},{"text":"PM_GROUNDING_NOTE","type":"text","marks":[{"type":"code_inline"}]},{"text":" you pass differs from the stored one (otherwise the existing note is left untouched). Pass the same ","type":"text"},{"text":"PM_GROUNDING_NOTE","type":"text","marks":[{"type":"code_inline"}]},{"text":" value to both invocations so the note is consistent regardless of run order.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/flow-debug-and-verify.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Guided two-phase verification of the scheduled suggestion flow.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"flow-debug-start","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"bash scripts/flow-debug-and-verify.sh \u003corg> [opp-id]","type":"text","marks":[{"type":"code_inline"}]},{"text":") reuses a stable user-owned test opportunity named ","type":"text"},{"text":"[PM-TEST] Test Pipeline Verification Opp","type":"text","marks":[{"type":"code_inline"}]},{"text":" if one is still eligible (so repeated runs don't pile up duplicate opps), otherwise creates it, falls back to any user-owned open opp if creation fails, seeds/refreshes a grounding Note on whichever opp resolves, and prints the flow's ","type":"text"},{"text":"Debug","type":"text","marks":[{"type":"strong"}]},{"text":" URL, the opportunity to select, and the exact ","type":"text"},{"text":"verify","type":"text","marks":[{"type":"code_inline"}]},{"text":" command to run afterward — including a trailing baseline count (the pre-run suggestion count). It uses a user-owned opp so the resulting suggestion is visible in the admin's owner-scoped Pipeline Inspection. If no opportunity is available it prints an actionable message and exits 0 without verifying. Relay the URL + opportunity to the user and wait for them to run Debug in Flow Builder against that opportunity — this is the only on-demand path that binds a real ","type":"text"},{"text":"$Record","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a scheduled record-triggered flow (leave rollback mode UNCHECKED so the DML persists).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"verify","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"bash scripts/flow-debug-and-verify.sh \u003corg> \u003copp-id> verify [baseline]","type":"text","marks":[{"type":"code_inline"}]},{"text":") runs after the user confirms the debug run finished: it confirms the flow reached its action (agent user added to the Opportunity Team) and polls (~4 min) for the generated ","type":"text"},{"text":"AiGenActionItem","type":"text","marks":[{"type":"code_inline"}]},{"text":" suggestions. ","type":"text"},{"text":"Pass the baseline that flow-debug-start printed","type":"text","marks":[{"type":"strong"}]},{"text":" — it is captured ","type":"text"},{"text":"before","type":"text","marks":[{"type":"em"}]},{"text":" the debug run, so the delta is exact; omitting it falls back to weaker absolute detection. The wait between phases happens in the conversation, not inside the script.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/verify-prompt-generation.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Test the prompt template behind a field by calling the Einstein ","type":"text"},{"text":"/generations","type":"text","marks":[{"type":"code_inline"}]},{"text":" API directly. Pass the ","type":"text"},{"text":"field","type":"text","marks":[{"type":"strong"}]},{"text":" API name (","type":"text"},{"text":"bash scripts/verify-prompt-generation.sh \u003corg> \u003cfield> [opp-id]","type":"text","marks":[{"type":"code_inline"}]},{"text":" — e.g. ","type":"text"},{"text":"NextStep","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Risk__c","type":"text","marks":[{"type":"code_inline"}]},{"text":"); it resolves the field to its managed/derived template (a bare, un-namespaced managed name returns ","type":"text"},{"text":"ENTITY_IS_DELETED","type":"text","marks":[{"type":"code_inline"}]},{"text":") and uses the same user-owned opp selection strategy as ","type":"text"},{"text":"flow-debug-and-verify.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (both share one sourced lib: reuse the shared ","type":"text"},{"text":"[PM-TEST] Test Pipeline Verification Opp","type":"text","marks":[{"type":"code_inline"}]},{"text":" → create it → owner-scoped fallback → graceful skip) — the only difference on skip is that no Einstein endpoint call is made. Shows the actual suggestion text generated, helping admins validate custom prompt instructions.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Agent Assets","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/sales_management_agent.agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Agent Script definition for the Sales Management Agent (used when auto-provisioning fails)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/sales_management_agent.bundle-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" — AiAuthoringBundle metadata (deploy with ","type":"text"},{"text":"sf agent publish authoring-bundle --api-name SalesAgent","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/field-completion-template.genAiPromptTemplate-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Canonical ","type":"text"},{"text":"einstein_gpt__fieldCompletion","type":"text","marks":[{"type":"code_inline"}]},{"text":" prompt template with ","type":"text"},{"text":"@@PLACEHOLDERS@@","type":"text","marks":[{"type":"code_inline"}]},{"text":", filled in by ","type":"text"},{"text":"scripts/add-field-suggestion.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (do not deploy directly)","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"sales-agentforce-pipeline-management-configure","author":"@skillopedia","source":{"stars":1139,"repo_name":"sf-skills","origin_url":"https://github.com/forcedotcom/sf-skills/blob/HEAD/skills/sales-agentforce-pipeline-management-configure/SKILL.md","repo_owner":"forcedotcom","body_sha256":"a15d678b1427fa7b6bada166515ed2a7214761fb80692c3a86a9d46f0e061319","cluster_key":"f9f67f268cf98b511c33840ffe2e4ebca67f898d95d351e5f551092253b1fe00","clean_bundle":{"format":"clean-skill-bundle-v1","source":"forcedotcom/sf-skills/skills/sales-agentforce-pipeline-management-configure/SKILL.md","text_bytes":649240,"source_commit":"a8a18ce1e4ba12e69b4073eab98ce31bfbd8271e","skills_sh_rank":6706,"attachment_count":45,"text_attachments":45,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/sales-agentforce-pipeline-management-configure/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"business-sales-marketing","category_label":"Business"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"business-sales-marketing","metadata":{"domains":["Sales","Agentforce"],"version":"1.0","cliTools":[{"tool":["curl"],"semver":">=7.0.0"},{"tool":["jq"],"semver":">=1.6.0"},{"tool":["python3"],"semver":">=3.10.0"},{"tool":["sf"],"semver":">=2.139.6"}],"minApiVersion":"67.0","relatedSkills":["agentforce-generate","platform-tracing-agentforce-configure"]},"import_tag":"skills-sh-top20k-2026-11","description":"Use to configure, set up, or repair the Sales Management agent and Agentforce Pipeline Management in a Salesforce org. Automates metadata creation for flows, prompt templates, permission sets, and data source configuration. TRIGGER when: user wants to enable Pipeline Management, configure Sales pipeline features, set up the Sales Management agent for opportunity field updates (including autonomous updates), connect enabled data sources like Einstein Conversation Insights or Einstein Activity Capture, customize opportunity stage descriptions, configure post-meeting suggestions, verify or audit configuration status, fix partially configured orgs, or troubleshoot Pipeline Management metadata issues. DO NOT TRIGGER when: user wants to build a custom agent (use agentforce-generate), configure general Agentforce tracing (use platform-tracing-agentforce-configure), work with non-Sales agents, or enable Einstein Conversation Insights or Einstein Activity Capture from scratch (provisioning is out of scope)."}},"renderedAt":1793666756597}

Agentforce Pipeline Management Configuration Configure Agentforce Pipeline Management end-to-end in a Salesforce org. This skill handles both greenfield orgs (zero configuration) and partially configured orgs that need repair or completion. Scope - In scope : Enabling Pipeline Management, configuring the Sales Management agent, activating/customizing flows (Process Field Update Suggestions, Get Opportunity Grounding Data, Get AI Recommendations from Call Transcripts), prompt template configuration, permission set assignment, data source setup, opportunity stage descriptions, autonomous field…