@salesforce/afv-skills 1.31.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-bot-upgrade/SKILL.md +217 -0
  3. package/skills/agentforce-bot-upgrade/references/extraction-blueprint.md +242 -0
  4. package/skills/agentforce-bot-upgrade/references/generate-agent-spec.md +128 -0
  5. package/skills/agentforce-bot-upgrade/references/handoff-output-format.md +208 -0
  6. package/skills/agentforce-bot-upgrade/references/input-contract.md +101 -0
  7. package/skills/agentforce-bot-upgrade/references/mapping-rules.md +113 -0
  8. package/skills/agentforce-bot-upgrade/references/planner-workflow-reference.md +78 -0
  9. package/skills/agentforce-bot-upgrade/references/post-conversion-enhancements-reference.md +67 -0
  10. package/skills/agentforce-bot-upgrade/references/quality-checklist.md +33 -0
  11. package/skills/agentforce-bot-upgrade/references/sf-cli-bot-reference.md +116 -0
  12. package/skills/dx-devops-work-item-manage/SKILL.md +270 -0
  13. package/skills/dx-devops-work-item-manage/examples/common-workflows.md +479 -0
  14. package/skills/dx-devops-work-item-manage/references/cli-commands.md +476 -0
  15. package/skills/dx-org-manage/SKILL.md +8 -5
  16. package/skills/dx-org-trial-expiration-check/SKILL.md +239 -0
  17. package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +588 -0
  18. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +12 -9
  19. package/skills/experience-ui-bundle-custom-app-generate/SKILL.md +29 -5
  20. package/skills/experience-ui-bundle-custom-app-generate/references/configure-metadata-custom-application.md +1 -1
  21. package/skills/experience-ui-bundle-custom-app-generate/scripts/resolve-uibundle-path.sh +30 -0
  22. package/skills/experience-ui-bundle-features-generate/SKILL.md +86 -11
  23. package/skills/experience-ui-bundle-features-generate/references/conflict-resolution-schema.json +20 -0
  24. package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +14 -0
  25. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +6 -1
  26. package/skills/experience-ui-bundle-frontend-generate/references/component.md +34 -1
  27. package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -1
  28. package/skills/experience-ui-bundle-project-generate/SKILL.md +111 -0
  29. package/skills/experience-ui-bundle-project-generate/scripts/flatten-project.mjs +33 -0
  30. package/skills/platform-custom-lightning-type-generate/SKILL.md +9 -14
  31. package/skills/platform-custom-lightning-type-generate/references/widget-rendition.md +27 -111
  32. package/skills/platform-lightning-type-widget-coordinate/SKILL.md +214 -0
  33. package/skills/platform-lightning-type-widget-coordinate/examples/existing-lightning-type-with-widget-prompt.md +80 -0
  34. package/skills/platform-lightning-type-widget-coordinate/examples/new-lightning-type-with-widget-prompt.md +75 -0
  35. package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +67 -0
  36. package/skills/platform-lightning-type-widget-coordinate/references/lightning-type-discovery.md +124 -0
  37. package/skills/platform-lightning-type-widget-coordinate/references/validation-gates.md +130 -0
  38. package/skills/platform-sharing-owd-configure/SKILL.md +142 -0
  39. package/skills/platform-sharing-owd-configure/examples/get_owd_output.md +38 -0
  40. package/skills/platform-sharing-owd-configure/examples/update_owd_output.md +44 -0
  41. package/skills/platform-sharing-owd-configure/references/access_levels.md +49 -0
  42. package/skills/platform-sharing-owd-configure/references/metadata_api_approach.md +69 -0
  43. package/skills/platform-sharing-rules-generate/SKILL.md +89 -19
  44. package/skills/platform-sharing-rules-generate/examples/test-cases.md +381 -0
  45. package/skills/platform-widget-generate/SKILL.md +241 -0
  46. package/skills/platform-widget-generate/examples/conditional.json +73 -0
  47. package/skills/platform-widget-generate/examples/list-with-foreach.json +66 -0
  48. package/skills/platform-widget-generate/examples/single-object.json +64 -0
  49. package/skills/platform-widget-generate/references/schema-from-lightning-type.md +76 -0
  50. package/skills/platform-widget-generate/references/widget-bundle-layout.md +116 -0
  51. package/skills/platform-widget-generate/references/widget-meta-directives.md +130 -0
  52. package/skills/sales-agentforce-pipeline-management-configure/SKILL.md +268 -0
  53. package/skills/sales-agentforce-pipeline-management-configure/assets/field-completion-template.genAiPromptTemplate-meta.xml +141 -0
  54. package/skills/sales-agentforce-pipeline-management-configure/assets/pipeline_management_flow.flow-meta.xml +548 -0
  55. package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.agent +2938 -0
  56. package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.bundle-meta.xml +4 -0
  57. package/skills/sales-agentforce-pipeline-management-configure/assets/sfdx-project.json +12 -0
  58. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/bant.txt +11 -0
  59. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/meddic.txt +11 -0
  60. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/spiced.txt +11 -0
  61. package/skills/sales-agentforce-pipeline-management-configure/examples/custom-prompt-instructions.md +317 -0
  62. package/skills/sales-agentforce-pipeline-management-configure/references/admin-communication.md +355 -0
  63. package/skills/sales-agentforce-pipeline-management-configure/references/agent-creation.md +383 -0
  64. package/skills/sales-agentforce-pipeline-management-configure/references/auth-and-cli.md +146 -0
  65. package/skills/sales-agentforce-pipeline-management-configure/references/automation-matrix.md +23 -0
  66. package/skills/sales-agentforce-pipeline-management-configure/references/autonomous-updates.md +215 -0
  67. package/skills/sales-agentforce-pipeline-management-configure/references/canonical-agent-path.md +75 -0
  68. package/skills/sales-agentforce-pipeline-management-configure/references/data-sources.md +155 -0
  69. package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompt-template.md +120 -0
  70. package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompts.md +149 -0
  71. package/skills/sales-agentforce-pipeline-management-configure/references/flags.md +13 -0
  72. package/skills/sales-agentforce-pipeline-management-configure/references/flow-clone-from-template.md +447 -0
  73. package/skills/sales-agentforce-pipeline-management-configure/references/metadata-inventory.md +330 -0
  74. package/skills/sales-agentforce-pipeline-management-configure/references/opportunity-stages.md +246 -0
  75. package/skills/sales-agentforce-pipeline-management-configure/references/repair-diagnostics.md +265 -0
  76. package/skills/sales-agentforce-pipeline-management-configure/references/setup-order.md +263 -0
  77. package/skills/sales-agentforce-pipeline-management-configure/references/soap-api-enablement.md +528 -0
  78. package/skills/sales-agentforce-pipeline-management-configure/scripts/CHANGES.md +180 -0
  79. package/skills/sales-agentforce-pipeline-management-configure/scripts/README.md +56 -0
  80. package/skills/sales-agentforce-pipeline-management-configure/scripts/add-field-suggestion.sh +679 -0
  81. package/skills/sales-agentforce-pipeline-management-configure/scripts/create-agent.sh +302 -0
  82. package/skills/sales-agentforce-pipeline-management-configure/scripts/create-flow.sh +250 -0
  83. package/skills/sales-agentforce-pipeline-management-configure/scripts/define-agent-access.sh +430 -0
  84. package/skills/sales-agentforce-pipeline-management-configure/scripts/deploy-settings.sh +77 -0
  85. package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-deal-agent.sh +241 -0
  86. package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-prerequisites.sh +538 -0
  87. package/skills/sales-agentforce-pipeline-management-configure/scripts/flow-debug-and-verify.sh +385 -0
  88. package/skills/sales-agentforce-pipeline-management-configure/scripts/retrieve-settings.sh +131 -0
  89. package/skills/sales-agentforce-pipeline-management-configure/scripts/setup-all.sh +2791 -0
  90. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-bundle-publish.sh +189 -0
  91. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-detection.sh +239 -0
  92. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/flow-builder.sh +238 -0
  93. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/psg.sh +48 -0
  94. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/soap.sh +140 -0
  95. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/stage-descriptions.sh +448 -0
  96. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/test-opp.sh +166 -0
  97. package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-all.sh +636 -0
  98. package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-prompt-generation.sh +346 -0
@@ -0,0 +1,2791 @@
1
+ #!/bin/bash
2
+ # Master orchestration script for Pipeline Management end-to-end setup
3
+ # Handles both greenfield and partially-configured orgs.
4
+ #
5
+ # Architecture: Each phase uses a "harness loop" pattern:
6
+ # 1. Check if already done (skip if yes)
7
+ # 2. Try primary approach (SOAP Metadata API)
8
+ # 3. If primary fails, try fallback (CLI retrieve/modify/deploy)
9
+ # 4. If fallback fails, try tertiary (Tooling API / REST)
10
+ # 5. VERIFY the result via readMetadata before proceeding
11
+ # 6. Only report BLOCKER after ALL approaches exhausted
12
+ #
13
+ # Working Directory Note:
14
+ # Agent bundle publishing uses isolated temp directories with automatic cleanup.
15
+ # Settings/flow deployment operations may create force-app/ in the current directory
16
+ # when SOAP/Tooling API approaches fail and CLI deploy is attempted as fallback.
17
+ #
18
+ # Usage: ./setup-all.sh <org-alias> [flags]
19
+ #
20
+ # Flags (all optional; SAFE by default):
21
+ # --autonomous Enable autonomous field updates (agent auto-applies
22
+ # without human review). OFF by default — suggestions only.
23
+ # --create-stage-descriptions Create sales-methodology stage descriptions (needed only
24
+ # for StageName suggestions). OFF by default.
25
+ # --non-interactive | --yes Run without prompts. Combined with the flags above this
26
+ # gives fully unattended setup; without them the run stays
27
+ # on the safe (suggestion-only, no stage-desc) defaults.
28
+ # --users "a@x.com,b@x.com" Explicit comma-separated usernames to also grant the
29
+ # SalesManagementUserPsg (in addition to the running user).
30
+ # This is the ONLY non-interactive way to add users — the
31
+ # script NEVER queries all/standard users and bulk-assigns.
32
+
33
+ set -euo pipefail
34
+
35
+ # Require bash 4.0+ for associative arrays (macOS ships bash 3.2 by default)
36
+ if [[ ${BASH_VERSINFO[0]} -lt 4 ]]; then
37
+ echo "ERROR: bash 4.0+ required. Your version: $BASH_VERSION"
38
+ echo "On macOS, install via: brew install bash"
39
+ echo "Then run: /opt/homebrew/bin/bash $0 $*"
40
+ exit 1
41
+ fi
42
+
43
+ # Set consistent locale to prevent jq parsing issues with special characters
44
+ export LC_ALL=en_US.UTF-8
45
+ export LANG=en_US.UTF-8
46
+
47
+ # sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.
48
+ # 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).
49
+ # Exported here so they propagate to sourced libs and child bash subprocesses.
50
+ export NO_COLOR=1
51
+ export FORCE_COLOR=0
52
+
53
+ ORG_ALIAS="${1:-}"
54
+ # Validate org alias (prevent shell injection via metacharacters)
55
+ if [[ -n "$ORG_ALIAS" && ! "$ORG_ALIAS" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
56
+ echo "ERROR: Invalid org alias. Only alphanumeric, dots, hyphens, and underscores allowed (must start with alphanumeric)."
57
+ exit 1
58
+ fi
59
+
60
+ # --- Consent flags (SAFE by default) ---
61
+ # Autonomous updates and stage-description creation are BOTH opt-in. In non-interactive
62
+ # mode we never enable them implicitly; the operator must pass the explicit flag.
63
+ ENABLE_AUTONOMOUS=false # --autonomous
64
+ CREATE_STAGE_DESCRIPTIONS=false # --create-stage-descriptions
65
+ NON_INTERACTIVE=false # --non-interactive | --yes (also implied when stdin is not a TTY)
66
+ USERS_LIST="" # --users "a@x.com,b@x.com" — explicit PSG grantees (never bulk)
67
+
68
+ # --- Field selection (3-phase setup) ---
69
+ # Which Opportunity fields the suggestion flow should manage. Chosen UP FRONT
70
+ # (before the flow is built) so the flow is built with ONLY these fields — no
71
+ # deploy-then-strip. At least one field is REQUIRED; setup ABORTS if none are
72
+ # selected (there is no silent default). OOTB fields (NextStep, StageName) count
73
+ # toward the 5-field cap. Per-field goal/instruction customization is collected
74
+ # into FIELD_PROMPTS keyed "<Field>_goal" / "<Field>_instruction".
75
+ SELECTED_FIELDS="" # --fields "NextStep,StageName,Risk__c"
76
+ SKIP_PROMPT_VERIFICATION=false # --skip-prompt-verification (bypass the Phase 2e gate)
77
+ declare -A FIELD_PROMPTS # per-field goal/instruction, from --field-goal/--field-instruction
78
+ readonly MAX_SUGGESTION_FIELDS=5
79
+
80
+ # --- Phase split (agent-driven interactive tune-loop) ---
81
+ # The full run is one long non-interactive pass, which (a) can't pause for the
82
+ # agent to tune each field's prompt with the user and (b) can exceed the harness
83
+ # tool timeout. --through-phase / --from-phase cut the run at the prompt boundary
84
+ # so the AGENT owns the interactive tune-loop BETWEEN two calls:
85
+ # Call 1: --through-phase prompts → enable, provision, deploy+activate templates,
86
+ # then STOP before the flow is built (context phases 0/1/1.7 always run).
87
+ # (agent tunes each field via add-field-suggestion.sh --verify-with-note --note …)
88
+ # Call 2: --from-phase flow → build+activate flow, agent, PSG, final verify.
89
+ # Both calls are idempotent, so this is a resumable checkpoint, not new state.
90
+ # Recognized cut points: "prompts" (end of Phase 4d) and "flow" (start of Phase 5).
91
+ RUN_FROM_PHASE="" # --from-phase flow (skip everything before Phase 5)
92
+ RUN_THROUGH_PHASE="" # --through-phase prompts (stop after templates, before flow)
93
+
94
+ # --- License preflight (agent's first action, before any clarifying question) ---
95
+ # --check-license runs ONLY auth + the Phase 0.5 capability gate, then exits. It
96
+ # takes no --fields (so it can run before the field questions the gate must
97
+ # precede), asks nothing, and mutates nothing. The SKILL drives this FIRST so an
98
+ # unlicensed org is blocked before the user is asked anything.
99
+ CHECK_LICENSE_ONLY=false # --check-license
100
+
101
+ # Parse flags after the positional org alias. `shift` past $1 first if present.
102
+ if [[ $# -ge 1 ]]; then shift || true; fi
103
+ while [[ $# -gt 0 ]]; do
104
+ case "$1" in
105
+ --autonomous) ENABLE_AUTONOMOUS=true; shift ;;
106
+ --create-stage-descriptions) CREATE_STAGE_DESCRIPTIONS=true; shift ;;
107
+ --non-interactive|--yes) NON_INTERACTIVE=true; shift ;;
108
+ --check-license) CHECK_LICENSE_ONLY=true; shift ;;
109
+ --skip-prompt-verification) SKIP_PROMPT_VERIFICATION=true; shift ;;
110
+ --users) { [[ $# -ge 2 ]] && [[ "$2" != --* ]]; } || { echo "ERROR: --users requires a value (comma-separated usernames), e.g. --users \"a@x.com,b@x.com\""; exit 1; }
111
+ IFS=',' read -ra _TMP_USERS <<< "$2"
112
+ for _u in "${_TMP_USERS[@]}"; do
113
+ _u=$(echo "$_u" | xargs)
114
+ [[ "$_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; }
115
+ done
116
+ USERS_LIST="$2"; shift 2 ;;
117
+ --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 ;;
118
+ --field-goal) { [[ $# -ge 2 ]] && [[ "$2" == *:* ]]; } || { echo "ERROR: --field-goal requires \"FieldApiName:goal text\""; exit 1; }; FIELD_PROMPTS["${2%%:*}_goal"]="${2#*:}"; shift 2 ;;
119
+ --field-instruction) { [[ $# -ge 2 ]] && [[ "$2" == *:* ]]; } || { echo "ERROR: --field-instruction requires \"FieldApiName:instruction text\""; exit 1; }; FIELD_PROMPTS["${2%%:*}_instruction"]="${2#*:}"; shift 2 ;;
120
+ --through-phase) { [[ $# -ge 2 ]] && [[ "$2" != --* ]]; } || { echo "ERROR: --through-phase requires a value (currently: prompts)"; exit 1; }; RUN_THROUGH_PHASE="$2"; shift 2 ;;
121
+ --from-phase) { [[ $# -ge 2 ]] && [[ "$2" != --* ]]; } || { echo "ERROR: --from-phase requires a value (currently: flow)"; exit 1; }; RUN_FROM_PHASE="$2"; shift 2 ;;
122
+ *) echo "ERROR: Unknown option: $1"; echo "Usage: $0 <org-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 \"a@x.com,b@x.com\"]"; exit 1 ;;
123
+ esac
124
+ done
125
+
126
+ # --- Validate phase-split flags ---
127
+ # Only "prompts" (--through-phase) and "flow" (--from-phase) are recognized. They
128
+ # are the two ends of the split; passing both, or an unknown name, is rejected so a
129
+ # typo never silently runs the whole thing.
130
+ if [[ -n "$RUN_THROUGH_PHASE" && "$RUN_THROUGH_PHASE" != "prompts" ]]; then
131
+ echo "ERROR: --through-phase supports only 'prompts' (got '$RUN_THROUGH_PHASE')."; exit 1
132
+ fi
133
+ if [[ -n "$RUN_FROM_PHASE" && "$RUN_FROM_PHASE" != "flow" ]]; then
134
+ echo "ERROR: --from-phase supports only 'flow' (got '$RUN_FROM_PHASE')."; exit 1
135
+ fi
136
+ if [[ -n "$RUN_THROUGH_PHASE" && -n "$RUN_FROM_PHASE" ]]; then
137
+ echo "ERROR: --through-phase and --from-phase are mutually exclusive (they are the two halves of a split run)."; exit 1
138
+ fi
139
+
140
+ # A non-TTY stdin is treated as non-interactive regardless of flags, so prompts never hang.
141
+ if [[ ! -t 0 ]]; then NON_INTERACTIVE=true; fi
142
+
143
+ # The license preflight asks nothing and mutates nothing — force non-interactive
144
+ # so the Phase 0 Setup Options block (guarded below) is skipped entirely.
145
+ if [[ "$CHECK_LICENSE_ONLY" == "true" ]]; then NON_INTERACTIVE=true; fi
146
+
147
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
148
+
149
+ # Source shared libraries
150
+ source "$SCRIPT_DIR/shared/agent-bundle-publish.sh"
151
+ source "$SCRIPT_DIR/shared/soap.sh"
152
+ source "$SCRIPT_DIR/shared/psg.sh"
153
+ source "$SCRIPT_DIR/shared/flow-builder.sh"
154
+ source "$SCRIPT_DIR/shared/stage-descriptions.sh"
155
+
156
+ trap 'echo ""; echo "Setup interrupted."; exit 130' INT
157
+
158
+ # --- Configuration Constants ---
159
+ readonly API_VERSION="64.0"
160
+ readonly CURL_TIMEOUT=30
161
+ readonly SLEEP_SHORT=2
162
+ readonly SLEEP_MEDIUM=3
163
+ readonly SLEEP_LONG=5
164
+ readonly SLEEP_TOGGLE_OFF=10
165
+ readonly SLEEP_TOGGLE_ON=20
166
+
167
+ # --- Helpers ---
168
+ log_pass() { echo " [PASS] $1"; }
169
+ log_warn() { echo " [WARN] $1"; }
170
+ log_fail() { echo " [FAIL] $1"; }
171
+ log_info() { echo " [....] $1"; }
172
+ log_try() { echo " [TRY ] $1"; }
173
+ section() { echo ""; echo "=== $1 ==="; echo ""; }
174
+
175
+ # with_timeout <seconds> <cmd...> — run a command under a wall-clock bound so a
176
+ # hung network call (e.g. the /generations REST request, which has no built-in
177
+ # --max-time) can't stall the whole run past the harness's tool timeout. Uses
178
+ # coreutils `timeout` (or `gtimeout`) when available; on a stock macOS that has
179
+ # neither, it degrades to running the command unwrapped rather than failing.
180
+ _TIMEOUT_BIN="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)"
181
+ with_timeout() {
182
+ local secs="$1"; shift
183
+ if [[ -n "$_TIMEOUT_BIN" ]]; then
184
+ "$_TIMEOUT_BIN" "${secs}s" "$@"
185
+ else
186
+ "$@"
187
+ fi
188
+ }
189
+
190
+ # confirm <prompt> — yes/no gate. In non-interactive mode returns the SAFE default
191
+ # (decline: return 1) without prompting. Only ever use as `if confirm ...; then`,
192
+ # never as a bare last statement (a nonzero return would trip `set -e`).
193
+ confirm() {
194
+ local prompt="$1"
195
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
196
+ return 1
197
+ fi
198
+ local reply=""
199
+ read -rp " $prompt [y/N]: " reply || reply=""
200
+ [[ "$reply" =~ ^[Yy] ]]
201
+ }
202
+
203
+ # schedule_flow_active <org-alias> — resolve the Pipeline Management suggestion flow
204
+ # using the SAME SourceTemplateId-first resolver as Phase 5 (NOT a fuzzy label match),
205
+ # and print its IsActive state as "true" | "false" | "none" (no flow found). This keeps
206
+ # the summary consistent with detection elsewhere (#6): the old summary read used a
207
+ # label LIKE '%OppSugg%' query that could miss the deployed flow and report it as
208
+ # "Not Found" even right after a successful deploy.
209
+ schedule_flow_active() {
210
+ local org="$1" api_name="" q
211
+ for q in \
212
+ "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false" \
213
+ "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=false" \
214
+ "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false"; do
215
+ local rec
216
+ rec=$(sf data query --query "$q" --target-org "$org" --json 2>/dev/null \
217
+ | 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 "")
218
+ if [[ -n "$rec" ]]; then printf '%s' "$rec"; return 0; fi
219
+ done
220
+ printf 'none'
221
+ }
222
+
223
+ # sanitize_dev_name <raw> [prefix] — derive a VALID Salesforce DeveloperName.
224
+ # DeveloperName must be alphanumeric/underscore, start with a letter, and must NOT
225
+ # contain consecutive underscores or a trailing underscore. The naive
226
+ # `tr ' ' '_' | sed 's/\./_/g'` turned "Id. Decision Makers" into "Id__Decision_Makers"
227
+ # (double underscore) which the platform REJECTS — the silent cause behind #8.
228
+ # This collapses runs of underscores, trims leading/trailing underscores, and
229
+ # ensures a leading letter.
230
+ sanitize_dev_name() {
231
+ local raw="$1" prefix="${2:-}" name
232
+ name=$(printf '%s' "${prefix:+${prefix}_}${raw}" \
233
+ | tr ' ' '_' \
234
+ | sed 's/[^A-Za-z0-9_]/_/g' \
235
+ | sed -E 's/_+/_/g' \
236
+ | sed -E 's/^_+//; s/_+$//')
237
+ # DeveloperName must start with a letter.
238
+ if [[ ! "$name" =~ ^[A-Za-z] ]]; then name="X_${name}"; fi
239
+ printf '%s' "$name"
240
+ }
241
+
242
+ # Safe jq extraction - strips CLI warnings, handles parse failures gracefully
243
+ safe_jq() {
244
+ local json="$1" expr="$2" default="${3:-}"
245
+ [[ -z "$json" ]] && { printf '%s' "$default"; return; }
246
+ local cleaned
247
+ # Strip ANSI color escapes (sf --json can emit them into stdout), THEN find JSON start.
248
+ cleaned=$(printf '%s' "$json" | sed -E 's/'$'\x1b''\[[0-9;]*m//g' | sed -n '/^[[:space:]]*[{[]/,$p')
249
+ printf '%s' "$cleaned" | jq -r "$expr" 2>/dev/null || printf '%s' "$default"
250
+ }
251
+
252
+ safe_jq_int() {
253
+ local result
254
+ result=$(safe_jq "$1" "$2" "0")
255
+ [[ "$result" =~ ^[0-9]+$ ]] && echo "$result" || echo "0"
256
+ }
257
+
258
+ # Progress indicators for long-running operations (write to stderr to avoid polluting piped output)
259
+ show_progress() {
260
+ [[ -t 2 ]] || return 0
261
+ local msg="${1:-Working...}"
262
+ printf '\r [....] %s...' "$msg" >&2
263
+ }
264
+
265
+ clear_progress() {
266
+ [[ -t 2 ]] || return 0
267
+ printf '\r\033[K' >&2
268
+ }
269
+
270
+ # Sanitize display strings (remove control chars, truncate)
271
+ sanitize_display() {
272
+ local input="$1" max_len="${2:-80}"
273
+ printf '%s' "$input" | tr -d '\000-\037' | cut -c1-"$max_len"
274
+ }
275
+
276
+ # Sanitize a value for safe SOQL interpolation (alphanumeric, underscores, dots, spaces, slashes)
277
+ sanitize_soql_value() {
278
+ local input="$1"
279
+ printf '%s' "$input" | tr -cd 'A-Za-z0-9_./ -'
280
+ }
281
+
282
+ # redact_token() is provided by shared/soap.sh (sourced above). The shared version
283
+ # fixes session-id tail under-redaction (W-23215519). Do not redefine it here.
284
+
285
+ # Helper: Get license count with error handling
286
+ # Returns: license count or "ERROR" if query fails
287
+ get_license_count() {
288
+ local license_name="$1"
289
+ local result
290
+ result=$(sf data query --query "SELECT TotalLicenses FROM UserLicense WHERE Name = '$license_name'" \
291
+ --target-org "$ORG_ALIAS" --json 2>&1)
292
+
293
+ if echo "$result" | grep -qi "ERROR"; then
294
+ echo "ERROR"
295
+ else
296
+ safe_jq "$result" '.result.records[0].TotalLicenses // 0' "0"
297
+ fi
298
+ }
299
+
300
+ # --- State tracking for before/after summary ---
301
+ declare -A SETUP_STATE_BEFORE
302
+ declare -A SETUP_STATE_AFTER
303
+ declare -A SETUP_CHANGES
304
+ declare -a SETUP_ISSUES=()
305
+ declare -a SETUP_DEPENDENCIES=()
306
+
307
+ track_before() {
308
+ local KEY=$1
309
+ local VALUE=$2
310
+ SETUP_STATE_BEFORE["$KEY"]="$VALUE"
311
+ }
312
+
313
+ track_after() {
314
+ local KEY=$1
315
+ local VALUE=$2
316
+ SETUP_STATE_AFTER["$KEY"]="$VALUE"
317
+
318
+ # Determine change status
319
+ local BEFORE="${SETUP_STATE_BEFORE[$KEY]:-N/A}"
320
+ if [[ "$BEFORE" == "N/A" ]]; then
321
+ SETUP_CHANGES["$KEY"]="🆕 CREATED"
322
+ elif [[ "$BEFORE" != "$VALUE" ]]; then
323
+ SETUP_CHANGES["$KEY"]="✏️ CHANGED"
324
+ else
325
+ SETUP_CHANGES["$KEY"]="✅ NO CHANGE"
326
+ fi
327
+ }
328
+
329
+ track_issue() {
330
+ local SEVERITY=$1
331
+ local DESCRIPTION=$2
332
+ local RESOLUTION=$3
333
+ SETUP_ISSUES+=("$SEVERITY|$DESCRIPTION|$RESOLUTION")
334
+ }
335
+
336
+ track_dependency() {
337
+ local PHASE=$1
338
+ local DEPENDS_ON=$2
339
+ SETUP_DEPENDENCIES+=("$PHASE|$DEPENDS_ON")
340
+ }
341
+
342
+
343
+ if [[ -z "$ORG_ALIAS" ]]; then
344
+ echo "Usage: $0 <org-alias> [--autonomous] [--create-stage-descriptions] [--non-interactive|--yes]"
345
+ exit 1
346
+ fi
347
+
348
+ # ============================================================
349
+ # Phase 0: Consent preamble
350
+ # ============================================================
351
+ # Pipeline Management setup is provisioning-heavy. Two behaviours change what the
352
+ # agent does to the org and are therefore gated on explicit consent:
353
+ # 1. Autonomous field updates — the agent auto-applies suggestions with no human review.
354
+ # 2. Stage-description creation — only needed for StageName suggestions; may overwrite
355
+ # or create descriptions the org intentionally left blank.
356
+ # Interactive runs are asked here; non-interactive runs use the SAFE resolved defaults
357
+ # (opt-in only via --autonomous / --create-stage-descriptions).
358
+ # The --check-license preflight skips this block wholesale — it decides nothing
359
+ # about provisioning behaviour and must ask the user nothing.
360
+ if [[ "$CHECK_LICENSE_ONLY" != "true" ]]; then
361
+ section "Phase 0: Setup Options"
362
+
363
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
364
+ log_info "Non-interactive mode — using resolved options:"
365
+ else
366
+ echo " Before provisioning, confirm two optional behaviours (both default to OFF/safe):"
367
+ echo ""
368
+ # Autonomous updates: opt-in. Flag pre-answers; otherwise ask.
369
+ if [[ "$ENABLE_AUTONOMOUS" != "true" ]]; then
370
+ if confirm "Let the agent update fields AUTONOMOUSLY (auto-apply without human review)? Default is suggestions-only"; then
371
+ ENABLE_AUTONOMOUS=true
372
+ fi
373
+ fi
374
+ # Stage descriptions: opt-in, and only relevant if the user wants StageName suggestions.
375
+ if [[ "$CREATE_STAGE_DESCRIPTIONS" != "true" ]]; then
376
+ if confirm "Create sales-methodology stage descriptions (only needed for StageName suggestions)?"; then
377
+ CREATE_STAGE_DESCRIPTIONS=true
378
+ fi
379
+ fi
380
+ echo ""
381
+ fi
382
+
383
+ echo " • Autonomous field updates: $([[ "$ENABLE_AUTONOMOUS" == "true" ]] && echo "ENABLED (auto-apply)" || echo "off (suggestions only)")"
384
+ echo " • Stage-description creation: $([[ "$CREATE_STAGE_DESCRIPTIONS" == "true" ]] && echo "ENABLED" || echo "off")"
385
+ fi # END Phase 0 Setup Options (skipped under --check-license)
386
+
387
+ # ============================================================
388
+ # Phase 1: Authentication
389
+ # ============================================================
390
+ section "Phase 1: Authentication"
391
+
392
+ # Filter CLI warnings that pollute JSON output (causes jq parse errors)
393
+ # Use buffering pattern to avoid grep eating newlines that jq needs
394
+ ORG_DISPLAY_OUTPUT=$(sf org display --target-org "$ORG_ALIAS" --json 2>&1)
395
+ INSTANCE_URL=$(safe_jq "$ORG_DISPLAY_OUTPUT" '.result.instanceUrl // empty' "")
396
+ if [[ -z "$INSTANCE_URL" ]]; then
397
+ log_fail "Cannot connect to org '$ORG_ALIAS'. Run: sf org login web --alias $ORG_ALIAS"
398
+ exit 1
399
+ fi
400
+ # Enforce HTTPS on instance URL (prevent MITM with access tokens)
401
+ if [[ -n "$INSTANCE_URL" && ! "$INSTANCE_URL" =~ ^https:// ]]; then
402
+ log_fail "Instance URL is not HTTPS: $INSTANCE_URL"
403
+ log_fail "Refusing to send access tokens over insecure connection."
404
+ exit 1
405
+ fi
406
+
407
+ # Extract access token with same warning filtering
408
+ ACCESS_TOKEN=$(safe_jq "$ORG_DISPLAY_OUTPUT" '.result.accessToken // empty' "")
409
+ if [[ -z "$ACCESS_TOKEN" || "$ACCESS_TOKEN" == "null" || "$ACCESS_TOKEN" == *"REDACTED"* ]]; then
410
+ TOKEN_OUTPUT=$(echo "y" | sf org auth show-access-token --target-org "$ORG_ALIAS" --no-prompt --json 2>&1)
411
+ ACCESS_TOKEN=$(safe_jq "$TOKEN_OUTPUT" '.result.accessToken // empty' "")
412
+ fi
413
+
414
+ if [[ -z "$ACCESS_TOKEN" || "$ACCESS_TOKEN" == "null" ]]; then
415
+ log_fail "Cannot extract access token. Re-authenticate: sf org login web --alias $ORG_ALIAS"
416
+ exit 1
417
+ fi
418
+
419
+ log_pass "Connected to $(sanitize_display "$INSTANCE_URL" 80)"
420
+
421
+ # Detect test/trial/scratch orgs and warn about potential API restrictions
422
+ if [[ -n "$INSTANCE_URL" ]]; then
423
+ if [[ "$INSTANCE_URL" =~ (test[0-9]*\.|\.pc-rnd\.|scratch|sandbox|-dev-ed\.|\.trailhead\.|--) ]]; then
424
+ log_warn "TEST/TRIAL ORG DETECTED: $INSTANCE_URL"
425
+ echo ""
426
+ echo " Note: Test, trial, and sandbox orgs may have SOAP API restrictions."
427
+ echo " This script will use alternative approaches (REST API, CLI) where possible."
428
+ echo " Some features may require manual UI enablement if API access is limited."
429
+ echo ""
430
+ fi
431
+ fi
432
+
433
+ # --- API Helpers ---
434
+ # NOTE: These helpers are defined here — right after Phase 1 Authentication — so
435
+ # they are available to the Step 0 capability gate (Phase 0.5, below) that must run
436
+ # BEFORE any interactive question. They depend only on INSTANCE_URL/ACCESS_TOKEN
437
+ # (set in Phase 1), API_VERSION/CURL_TIMEOUT (readonly, top), and
438
+ # show_progress/clear_progress (top-level) — nothing from Phase 1.7 onward. They
439
+ # are also used by BLOCK B (soap_read/soap_extract_bool in the AFTER snapshot,
440
+ # sed_inplace in the flow reconciliation, ensure_sfdx_project before flow/agent
441
+ # deploys); on a --from-phase flow run BLOCK A is skipped entirely, so defining
442
+ # them at this top level keeps them in scope for both blocks and Step 0.
443
+
444
+ # SOAP readMetadata — returns raw XML response
445
+ soap_read() {
446
+ local TYPE="$1" NAME="$2"
447
+ curl -s --max-time $CURL_TIMEOUT "${INSTANCE_URL}/services/Soap/m/${API_VERSION}" \
448
+ -H "Content-Type: text/xml; charset=UTF-8" \
449
+ -H "SOAPAction: readMetadata" \
450
+ -d "<?xml version='1.0' encoding='utf-8'?>
451
+ <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>
452
+ <soapenv:Header><met:SessionHeader><met:sessionId>${ACCESS_TOKEN}</met:sessionId></met:SessionHeader></soapenv:Header>
453
+ <soapenv:Body><met:readMetadata><met:type>${TYPE}</met:type><met:fullNames>${NAME}</met:fullNames></met:readMetadata></soapenv:Body>
454
+ </soapenv:Envelope>" 2>/dev/null
455
+ }
456
+
457
+ # Classify a readMetadata response via the shared shared/soap.sh parser.
458
+ # Distinguishes a genuine "false" from an auth/SOAP fault or a network/service
459
+ # error (the W-23215519 fix) instead of collapsing all three to empty.
460
+ # soap_read() does not capture an HTTP status, so "200" is passed as the
461
+ # nominal status; the parser still detects empty bodies, SOAP faults, HTML
462
+ # error pages, and truncated XML from the response content itself.
463
+ # Args: $1 = readMetadata XML, $2 = field name
464
+ # Echoes: "true" | "false" | "AUTH_ERROR:..." | "TYPE_UNAVAILABLE:..." | "NETWORK_ERROR"
465
+ soap_classify() {
466
+ local XML="$1" FIELD="$2"
467
+ parse_soap_response "$XML" "$FIELD" "200"
468
+ }
469
+
470
+ # SOAP updateMetadata — returns raw XML response
471
+ soap_update() {
472
+ local BODY="$1"
473
+ show_progress "SOAP Metadata API call"
474
+ local response
475
+ response=$(curl -s --max-time $CURL_TIMEOUT "${INSTANCE_URL}/services/Soap/m/${API_VERSION}" \
476
+ -H "Content-Type: text/xml; charset=UTF-8" \
477
+ -H "SOAPAction: update" \
478
+ -d "<?xml version='1.0' encoding='utf-8'?>
479
+ <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>
480
+ <soapenv:Header><met:SessionHeader><met:sessionId>${ACCESS_TOKEN}</met:sessionId></met:SessionHeader></soapenv:Header>
481
+ <soapenv:Body><met:updateMetadata>${BODY}</met:updateMetadata></soapenv:Body>
482
+ </soapenv:Envelope>" 2>/dev/null)
483
+ clear_progress
484
+ printf '%s' "$response"
485
+ }
486
+
487
+ # Extract a boolean field value from SOAP XML response.
488
+ # Delegates to the shared shared/soap.sh parser for robust extraction (handles
489
+ # truncated XML, HTML error pages, multi-record responses) but preserves this
490
+ # function's original contract — "true", "false", or "" — so the ~15 call sites
491
+ # that only test `== "true"` keep their exact behavior. Auth/type/network
492
+ # classifications collapse to "" here (same as the old grep failure); callers
493
+ # that need to distinguish them use soap_classify directly (see the gate).
494
+ soap_extract_bool() {
495
+ local XML="$1" FIELD="$2"
496
+ local result
497
+ result=$(parse_soap_response "$XML" "$FIELD" "200")
498
+ case "$result" in
499
+ true) echo "true" ;;
500
+ false) echo "false" ;;
501
+ *) echo "" ;;
502
+ esac
503
+ }
504
+
505
+ # Cross-platform sed -i
506
+ sed_inplace() {
507
+ if [[ "$OSTYPE" == "darwin"* ]]; then
508
+ sed -i '' "$@"
509
+ else
510
+ sed -i "$@"
511
+ fi
512
+ }
513
+
514
+ # Ensure sfdx-project.json exists for CLI operations
515
+ ensure_sfdx_project() {
516
+ if [[ ! -f "sfdx-project.json" ]]; then
517
+ cat > sfdx-project.json << 'SFDX'
518
+ {"packageDirectories": [{"path": "force-app", "default": true}], "sourceApiVersion": "67.0"}
519
+ SFDX
520
+ fi
521
+ }
522
+
523
+ # Print the "org can't enable Pipeline Management" blocker body (license list,
524
+ # edition guidance, Setup URL). Shared by the Step 0 gate (Phase 0.5) and the
525
+ # Phase 2.5 backstop so the message is authored once. Callers own the log_fail
526
+ # banner and the exit; this only prints the explanatory body.
527
+ # Args: $1 = Einstein Agent license count, $2 = Agentforce-for-Sales license count
528
+ # (either may be a number, "unknown", or empty)
529
+ print_license_blocker() {
530
+ local ea_count="${1:-unknown}" sales_count="${2:-unknown}"
531
+ echo ""
532
+ echo " BLOCKER: This org cannot enable Pipeline Management programmatically."
533
+ echo ""
534
+ echo " Reason: The SalesDealAgentSettings metadata type is not provisioned."
535
+ echo " This typically means the org is missing required licenses."
536
+ echo ""
537
+ echo " Current org licenses:"
538
+ echo " • Einstein Agent licenses: $ea_count"
539
+ echo " • Agentforce for Sales licenses: $sales_count"
540
+ echo ""
541
+ echo " Pipeline Management requires:"
542
+ echo " ✓ Agentforce for Sales add-on license, OR"
543
+ echo " ✓ Sales Cloud with Einstein capabilities, OR"
544
+ echo " ✓ Agentforce 1 Sales Edition"
545
+ echo ""
546
+ echo " Actions:"
547
+ echo " 1. Contact your Salesforce account team to provision Agentforce for Sales licenses"
548
+ echo " 2. Verify org edition supports Pipeline Management (Enterprise Edition or higher)"
549
+ echo " 3. Try manual enablement via Setup UI (if available):"
550
+ echo " ${INSTANCE_URL}/lightning/setup/PipelineManagement/home"
551
+ echo " (Note: Assumes Lightning Experience. For Classic, use Setup menu.)"
552
+ echo ""
553
+ }
554
+
555
+ # ============================================================
556
+ # Phase 0.5: License & Capability Gate (Step 0)
557
+ # ============================================================
558
+ # Block an ineligible org UP FRONT — before any interactive question (Phase 1.7
559
+ # field selection) or mutation. The authoritative signal is the same one the
560
+ # Phase 2.5 backstop uses: probe the SalesDealAgentSettings metadata type over
561
+ # SOAP and hard-block ONLY on INVALID_TYPE (soap_classify => TYPE_UNAVAILABLE).
562
+ # License/edition COUNTS are informational context, never a block, because
563
+ # UserLicense can read 0 while PM still works via base Einstein Agent
564
+ # entitlements. An already-provisioned org has the type (probe => true/false),
565
+ # so it passes and idempotent re-runs stay green. Runs on BOTH the single-shot
566
+ # and --from-phase flow paths — it's read-only and fast.
567
+ section "Phase 0.5: License & Capability Gate"
568
+
569
+ STEP0_CAP=$(soap_classify "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
570
+
571
+ # Informational context (non-blocking): edition + license counts.
572
+ STEP0_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")
573
+ STEP0_EA_COUNT=$(get_license_count "Einstein Agent")
574
+ [[ "$STEP0_EA_COUNT" == "ERROR" ]] && STEP0_EA_COUNT="unknown"
575
+ STEP0_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')
576
+
577
+ echo " Org edition: $STEP0_EDITION"
578
+ echo " Einstein Agent licenses: $STEP0_EA_COUNT"
579
+ echo " Agentforce for Sales / Sales Cloud Einstein / Agentforce 1 licenses: $STEP0_SALES_COUNT"
580
+ echo ""
581
+
582
+ case "$STEP0_CAP" in
583
+ true|false)
584
+ log_pass "GATE: Pipeline Management capability confirmed (SalesDealAgentSettings available) ✓"
585
+ ;;
586
+ TYPE_UNAVAILABLE:*)
587
+ log_fail "GATE: This org is not licensed for Pipeline Management"
588
+ print_license_blocker "$STEP0_EA_COUNT" "$STEP0_SALES_COUNT"
589
+ echo " Stopping now — before any configuration questions — so nothing is changed on an org that cannot run Pipeline Management."
590
+ echo ""
591
+ exit 1
592
+ ;;
593
+ AUTH_ERROR:*)
594
+ log_warn "GATE: Couldn't confirm capability over SOAP yet (SOAP API access issue)."
595
+ echo " This is a separate, fixable prerequisite — it's re-checked and explained"
596
+ echo " at the SOAP prerequisites step (Phase 1.5). Proceeding."
597
+ echo ""
598
+ ;;
599
+ NETWORK_ERROR|*)
600
+ log_warn "GATE: Capability probe inconclusive (transient network/service error). Proceeding; the Phase 2.5 backstop will re-check."
601
+ echo ""
602
+ ;;
603
+ esac
604
+
605
+ # Non-blocking permission reminder — scripts cannot check these; the executing
606
+ # user must hold them for setup to complete (see SKILL.md "You must verify").
607
+ echo " Reminder — the executing user needs these permissions (not script-checkable):"
608
+ echo " • View Setup AND (Modify All Data OR Customize Application)"
609
+ echo " • Manage AI Agents AND (Manage Agentforce Employee Agents OR Customize Application)"
610
+ echo " • Assign Permission Sets"
611
+ echo ""
612
+
613
+ # --- License preflight stops here (--check-license) ---
614
+ # We've run auth + the Phase 0.5 gate. If the org were unlicensed the gate would
615
+ # already have printed the blocker and exit 1'd; reaching here means it's capable
616
+ # (or the probe was inconclusive — a non-blocking AUTH/NETWORK warning). Exit 0 so
617
+ # the driving agent can proceed to ask its clarifying questions. No mutation ran.
618
+ if [[ "$CHECK_LICENSE_ONLY" == "true" ]]; then
619
+ log_pass "Preflight passed — org is capable of Pipeline Management. No changes made."
620
+ echo " Proceed to configuration (field selection, prompt tuning, then setup)."
621
+ echo ""
622
+ exit 0
623
+ fi
624
+
625
+ # ============================================================
626
+ # Phase 1.7: Field Selection (up front, before any provisioning)
627
+ # ============================================================
628
+ # 3-phase setup, part 1 of the "field" story: decide WHICH Opportunity fields the
629
+ # suggestion flow will manage BEFORE we build/deploy the flow, so the flow is built
630
+ # with only those fields (no deploy-then-strip). Done here — right after auth,
631
+ # before the heavy provisioning phases — so an empty/oversized selection fails fast
632
+ # rather than after minutes of enablement work.
633
+ #
634
+ # Rules:
635
+ # - At least 1 field REQUIRED; setup ABORTS on an empty selection (no default).
636
+ # - Hard cap MAX_SUGGESTION_FIELDS (OOTB NextStep/StageName count toward it).
637
+ # - StageName is a picklist: NOT prompt-customizable, and it needs stage
638
+ # descriptions (Phase 6) — flagged via STAGENAME_SELECTED.
639
+ # - Per non-StageName field we collect an optional goal/instruction for the
640
+ # prompt template (NextStep -> OOTB override; custom fields -> new template).
641
+ section "Phase 1.7: Field Selection"
642
+
643
+ if [[ -z "$SELECTED_FIELDS" ]]; then
644
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
645
+ log_fail "No fields selected. Pipeline Management needs at least one field to manage."
646
+ echo " Pass --fields with a comma-separated list of Opportunity field API names."
647
+ echo " OOTB fields: NextStep (Next Step), StageName (Opportunity Stage)."
648
+ echo " Example: $0 $ORG_ALIAS --fields \"NextStep\" --non-interactive"
649
+ exit 1
650
+ fi
651
+ echo "Which Opportunity fields should the agent generate suggestions for?"
652
+ echo " • NextStep — Next Step (OOTB; works out of the box)"
653
+ echo " • StageName — Opportunity Stage (OOTB; requires stage descriptions)"
654
+ echo " • <API name> — any eligible custom text field (Text or Text Area <=255)"
655
+ echo ""
656
+ echo " Enter a comma-separated list (at least 1, at most $MAX_SUGGESTION_FIELDS). Setup aborts if empty."
657
+ read -rp " Fields: " SELECTED_FIELDS || SELECTED_FIELDS=""
658
+ fi
659
+
660
+ # Normalize: trim whitespace around each entry, drop empties, de-dup (order-preserving).
661
+ declare -a FIELD_ARRAY=()
662
+ IFS=',' read -ra _raw_fields <<< "$SELECTED_FIELDS"
663
+ for _f in "${_raw_fields[@]}"; do
664
+ _f="$(echo "$_f" | xargs)" # trim
665
+ [[ -z "$_f" ]] && continue
666
+ _dup=false
667
+ for _seen in "${FIELD_ARRAY[@]:-}"; do [[ "$_seen" == "$_f" ]] && { _dup=true; break; }; done
668
+ [[ "$_dup" == "true" ]] || FIELD_ARRAY+=("$_f")
669
+ done
670
+
671
+ if [[ ${#FIELD_ARRAY[@]} -eq 0 ]]; then
672
+ log_fail "No fields selected. Setup aborted — Pipeline Management requires at least one field."
673
+ echo " Re-run and select at least one field (e.g., NextStep)."
674
+ exit 1
675
+ fi
676
+ if [[ ${#FIELD_ARRAY[@]} -gt $MAX_SUGGESTION_FIELDS ]]; then
677
+ log_fail "Too many fields selected (${#FIELD_ARRAY[@]}). Maximum is $MAX_SUGGESTION_FIELDS (OOTB fields count toward the cap)."
678
+ exit 1
679
+ fi
680
+
681
+ # Rebuild the canonical comma list from the normalized array so downstream
682
+ # consumers (flow builder, summary) see the de-duped, trimmed set.
683
+ SELECTED_FIELDS="$(IFS=,; echo "${FIELD_ARRAY[*]}")"
684
+
685
+ STAGENAME_SELECTED=false
686
+ for _f in "${FIELD_ARRAY[@]}"; do [[ "$_f" == "StageName" ]] && STAGENAME_SELECTED=true; done
687
+
688
+ log_pass "Fields selected (${#FIELD_ARRAY[@]}): $SELECTED_FIELDS"
689
+ [[ "$STAGENAME_SELECTED" == "true" ]] && log_info "StageName selected — stage descriptions will be needed (Phase 6)."
690
+
691
+ # Collect prompt customization for each non-StageName field. StageName is a
692
+ # picklist and is never customizable. Interactive only — in non-interactive mode
693
+ # the goal/instruction come from --field-goal / --field-instruction (or defaults
694
+ # inside add-field-suggestion.sh). NextStep with no customization stays on its
695
+ # curated OOTB managed prompt.
696
+ if [[ "$NON_INTERACTIVE" != "true" ]]; then
697
+ for _f in "${FIELD_ARRAY[@]}"; do
698
+ [[ "$_f" == "StageName" ]] && continue
699
+ if [[ "$_f" == "NextStep" ]]; then
700
+ confirm "Customize the AI prompt for NextStep? (N = keep the OOTB prompt)" || continue
701
+ else
702
+ echo ""
703
+ echo "Configure the AI prompt for custom field '$_f':"
704
+ fi
705
+ read -rp " Goal (completes 'You must think about ...'): " _goal || _goal=""
706
+ read -rp " Instruction (one line of extraction guidance): " _instr || _instr=""
707
+ [[ -n "$_goal" ]] && FIELD_PROMPTS["${_f}_goal"]="$_goal"
708
+ [[ -n "$_instr" ]] && FIELD_PROMPTS["${_f}_instruction"]="$_instr"
709
+ done
710
+ fi
711
+ echo ""
712
+
713
+ # ============================================================
714
+ # Pre-Block-A: Snapshot org state for accurate Configuration Summary
715
+ # (W-23277501 — status column must reflect before-vs-after, not hardcoded
716
+ # greenfield literals, so idempotent reruns report UNCHANGED)
717
+ # ============================================================
718
+ # This is READ-ONLY org state and MUST run on BOTH the single-shot and the
719
+ # --from-phase flow paths, so it lives ABOVE the Block A `if`. It was previously
720
+ # inside Block A, which left BEFORE_* unbound on a --from-phase flow run and
721
+ # crashed the Phase 8 before/after table under `set -u`. All helpers it calls
722
+ # (soap_read, soap_extract_bool, schedule_flow_active, sanitize_display) are
723
+ # top-level, so this is safe here.
724
+ # Cache one SOAP read so both settings are extracted from the same document
725
+ SOAP_SETTINGS_BEFORE=$(soap_read "SalesDealAgentSettings" "SalesDealAgent" 2>/dev/null || echo "")
726
+ BEFORE_PM=$(soap_extract_bool "$SOAP_SETTINGS_BEFORE" "enableDealAgent" 2>/dev/null || echo "false")
727
+ BEFORE_AUTO=$(soap_extract_bool "$SOAP_SETTINGS_BEFORE" "enableDealAgentAutoApproveAllTasks" 2>/dev/null || echo "false")
728
+ BEFORE_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")
729
+ BEFORE_AGENT_USER=$(sanitize_display "$BEFORE_AGENT_USER" 60)
730
+ BEFORE_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")
731
+ BEFORE_PSG_COUNT=$((${BEFORE_PSG_COUNT:-0} + 0))
732
+ # Normalize jq boolean output — treat literal null / missing / non-true as "none"
733
+ BEFORE_FLOW_ACTIVE=$(schedule_flow_active "$ORG_ALIAS")
734
+ # OpptStageDescription does not exist until Pipeline Management is enabled, so on
735
+ # a greenfield org this query fails with INVALID_TYPE. Under `set -o pipefail` the
736
+ # failed pipeline leaves jq's "0" on stdout AND (with a trailing `|| echo "0"`)
737
+ # would append a second "0", yielding a two-line value that crashes the arithmetic
738
+ # below with "0\n0". Swallow the failure with `|| true` and let jq's `// 0` supply
739
+ # the default — this preserves the BEFORE snapshot (W-23277501) on greenfield orgs.
740
+ BEFORE_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)
741
+ BEFORE_STAGE_DESC_COUNT=$((${BEFORE_STAGE_DESC_COUNT:-0} + 0))
742
+
743
+ # ============================================================
744
+ # BLOCK A — Provisioning & Prompts (Phase 1.5 → 4e)
745
+ # ============================================================
746
+ # Everything from SOAP prereqs through prompt-template deploy + the verification
747
+ # gate. On a --from-phase flow run this entire block is SKIPPED: the org was
748
+ # already enabled/provisioned and the templates deployed+tuned in the prior
749
+ # --through-phase prompts call, so we jump straight to building the flow (Phase 5)
750
+ # from the agent-approved --fields set. The context phases above (0/1/1.7) always
751
+ # run, so SELECTED_FIELDS / FIELD_ARRAY / STAGENAME_SELECTED are populated either way.
752
+ if [[ "$RUN_FROM_PHASE" == "flow" ]]; then
753
+ section "Provisioning & Prompts (Phase 1.5–4e)"
754
+ log_info "Skipped (--from-phase flow): org already enabled and prompt templates deployed in the prior --through-phase prompts run."
755
+ log_info "Building the flow from the approved field set: $SELECTED_FIELDS"
756
+ else
757
+
758
+ # ============================================================
759
+ # Phase 1.5: SOAP API Prerequisites Verification
760
+ # ============================================================
761
+ section "Phase 1.5: SOAP API Prerequisites"
762
+
763
+ echo "This script requires SOAP API access to enable Pipeline Management settings."
764
+ echo "Testing SOAP API connectivity..."
765
+ echo ""
766
+
767
+ # Test SOAP API with a simple read operation
768
+ show_progress "Testing SOAP API connectivity"
769
+ SOAP_TEST=$(curl -s --max-time $CURL_TIMEOUT "${INSTANCE_URL}/services/Soap/m/${API_VERSION}" \
770
+ -H "Content-Type: text/xml; charset=UTF-8" \
771
+ -H "SOAPAction: readMetadata" \
772
+ -d "<?xml version='1.0' encoding='utf-8'?>
773
+ <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>
774
+ <soapenv:Header><met:SessionHeader><met:sessionId>${ACCESS_TOKEN}</met:sessionId></met:SessionHeader></soapenv:Header>
775
+ <soapenv:Body><met:readMetadata><met:type>EinsteinGptSettings</met:type><met:fullNames>EinsteinGpt</met:fullNames></met:readMetadata></soapenv:Body>
776
+ </soapenv:Envelope>" 2>&1)
777
+
778
+ clear_progress
779
+
780
+ # Check for INVALID_LOGIN error (most common SOAP access issue)
781
+ if echo "$SOAP_TEST" | grep -q "INVALID_LOGIN"; then
782
+ log_fail "SOAP API login failed"
783
+ echo ""
784
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
785
+ echo " BLOCKER: SOAP API Access Not Enabled"
786
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
787
+ echo ""
788
+ echo "This script uses the SOAP Metadata API to enable Pipeline Management"
789
+ echo "settings. Your org or user account does not have SOAP API access enabled."
790
+ echo ""
791
+ echo "Error received: INVALID_LOGIN"
792
+ echo ""
793
+ echo "SOAP API requires two prerequisites:"
794
+ echo ""
795
+ echo "1️⃣ ORG-LEVEL SETTING: Enable SOAP API Login"
796
+ echo ""
797
+ echo " a. Navigate to: ${INSTANCE_URL}/lightning/setup/UserInterfaceUI/home"
798
+ echo " OR: Setup → Quick Find: 'User Interface'"
799
+ echo ""
800
+ echo " b. Check the box: ☑ Enable SOAP API login()"
801
+ echo ""
802
+ echo " c. Click Save"
803
+ echo ""
804
+ echo "2️⃣ USER-LEVEL PERMISSION: Grant 'Use Any API Auth' Permission"
805
+ echo ""
806
+ echo " Option A: Via Permission Set (Recommended)"
807
+ echo " ─────────────────────────────────────────"
808
+ echo " a. Navigate to: ${INSTANCE_URL}/lightning/setup/PermSets/home"
809
+ echo " OR: Setup → Quick Find: 'Permission Sets'"
810
+ echo ""
811
+ echo " b. Create new Permission Set (e.g., 'API Access')"
812
+ echo ""
813
+ echo " c. Click 'System Permissions' → Edit"
814
+ echo ""
815
+ echo " d. Enable: ☑ Use Any API Auth"
816
+ echo ""
817
+ echo " e. Save → Manage Assignments → Assign to your user"
818
+ echo ""
819
+ echo " Option B: Via Profile (Alternative)"
820
+ echo " ────────────────────────────────────"
821
+ echo " a. Navigate to your user profile settings"
822
+ echo ""
823
+ echo " b. Edit → System Permissions"
824
+ echo ""
825
+ echo " c. Enable: ☑ Use Any API Auth"
826
+ echo ""
827
+ echo " d. Save"
828
+ echo ""
829
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
830
+ echo ""
831
+ echo "After enabling both prerequisites, re-run this script:"
832
+ echo " bash setup-all.sh $ORG_ALIAS"
833
+ echo ""
834
+ exit 1
835
+ fi
836
+
837
+ # Check for other SOAP errors
838
+ if echo "$SOAP_TEST" | grep -q "<faultcode>"; then
839
+ FAULT_CODE=$(echo "$SOAP_TEST" | grep -o "<faultcode>[^<]*" | sed 's/<faultcode>//')
840
+ FAULT_STRING=$(redact_token "$(echo "$SOAP_TEST" | grep -o "<faultstring>[^<]*" | sed 's/<faultstring>//')")
841
+
842
+ log_warn "SOAP API test returned an error"
843
+ echo ""
844
+ echo " Fault Code: $FAULT_CODE"
845
+ echo " Fault Message: $FAULT_STRING"
846
+ echo ""
847
+
848
+ if [[ "$FAULT_CODE" == *"INVALID_TYPE"* ]]; then
849
+ log_info "This may be expected for test/trial orgs. Proceeding..."
850
+ track_issue "⚠️ WARN" "SOAP API returned INVALID_TYPE" "Expected for test/trial orgs - Continued"
851
+ echo ""
852
+ else
853
+ echo " This may indicate a SOAP API configuration issue."
854
+ echo " If setup fails, verify SOAP API prerequisites above."
855
+ track_issue "⚠️ WARN" "SOAP API error: $FAULT_CODE" "User confirmed continue"
856
+ echo ""
857
+ # Non-interactive: skip (don't block on user input in agent context)
858
+ if [[ -t 0 ]]; then
859
+ read -p "Press Enter to continue anyway, or Ctrl+C to exit..."
860
+ fi
861
+ echo ""
862
+ fi
863
+ else
864
+ log_pass "SOAP API access verified ✓"
865
+ fi
866
+
867
+ # --- Generic enable-and-verify harness ---
868
+ # Tries SOAP → CLI deploy → verifies via SOAP readMetadata
869
+ # Returns:
870
+ # 0 = success
871
+ # 1 = failure (all approaches exhausted)
872
+ # 2 = metadata type not available (INVALID_TYPE - org lacks license/edition)
873
+ #
874
+ # Usage: enable_setting <MetadataType> <fullName> <field> <displayName>
875
+ enable_setting() {
876
+ local META_TYPE="$1"
877
+ local FULL_NAME="$2"
878
+ local FIELD="$3"
879
+ local DISPLAY_NAME="$4"
880
+
881
+ # --- CHECK: already enabled? ---
882
+ local CURRENT
883
+ local READ_RESULT
884
+ READ_RESULT=$(soap_read "$META_TYPE" "$FULL_NAME")
885
+
886
+ # Check for INVALID_TYPE in read response (metadata type not available)
887
+ if echo "$READ_RESULT" | grep -qi "INVALID_TYPE"; then
888
+ log_warn "$DISPLAY_NAME: metadata type not available in this org (licensing/edition limitation)"
889
+ return 2
890
+ fi
891
+
892
+ CURRENT=$(soap_extract_bool "$READ_RESULT" "$FIELD")
893
+ if [[ "$CURRENT" == "true" ]]; then
894
+ log_pass "$DISPLAY_NAME: enabled"
895
+ return 0
896
+ fi
897
+
898
+ # --- APPROACH 1: SOAP updateMetadata ---
899
+ log_try "$DISPLAY_NAME: enabling via SOAP Metadata API..."
900
+ local SOAP_RESULT
901
+ SOAP_RESULT=$(soap_update "<met:metadata xsi:type='met:${META_TYPE}' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>${FULL_NAME}</met:fullName><met:${FIELD}>true</met:${FIELD}></met:metadata>")
902
+
903
+ # Check for INVALID_TYPE in update response
904
+ if echo "$SOAP_RESULT" | grep -qi "INVALID_TYPE"; then
905
+ log_warn "$DISPLAY_NAME: metadata type not available in this org (licensing/edition limitation)"
906
+ return 2
907
+ fi
908
+
909
+ if echo "$SOAP_RESULT" | grep -q "<success>true</success>"; then
910
+ sleep $SLEEP_SHORT
911
+ # VERIFY
912
+ CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
913
+ if [[ "$CURRENT" == "true" ]]; then
914
+ log_pass "$DISPLAY_NAME: enabled (SOAP)"
915
+ return 0
916
+ fi
917
+ log_warn "$DISPLAY_NAME: SOAP reported success but verification failed. Retrying..."
918
+ sleep $SLEEP_LONG
919
+ CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
920
+ if [[ "$CURRENT" == "true" ]]; then
921
+ log_pass "$DISPLAY_NAME: enabled (SOAP, delayed verification)"
922
+ return 0
923
+ fi
924
+ fi
925
+
926
+ local SOAP_ERR
927
+ SOAP_ERR=$(echo "$SOAP_RESULT" | grep -o '<message>[^<]*</message>' | sed 's/<[^>]*>//g' | head -1 || echo "")
928
+ [[ -n "$SOAP_ERR" ]] && echo " SOAP error: $(redact_token "$SOAP_ERR")"
929
+
930
+ # --- APPROACH 2: CLI retrieve → modify → deploy ---
931
+ # Map MetadataType to CLI settings name (strip "Settings" suffix for CLI)
932
+ local CLI_NAME="${META_TYPE%Settings}"
933
+ # Special cases where CLI name differs
934
+ case "$META_TYPE" in
935
+ EinsteinGptSettings) CLI_NAME="EinsteinGpt" ;;
936
+ EinsteinCopilotSettings) CLI_NAME="EinsteinCopilot" ;;
937
+ AgentPlatformSettings) CLI_NAME="AgentPlatform" ;;
938
+ EnhancedNotesSettings) CLI_NAME="EnhancedNotes" ;;
939
+ OpportunitySettings) CLI_NAME="Opportunity" ;;
940
+ SalesDealAgentSettings) CLI_NAME="SalesDealAgent" ;;
941
+ esac
942
+
943
+ log_try "$DISPLAY_NAME: trying CLI retrieve/modify/deploy..."
944
+ ensure_sfdx_project
945
+ mkdir -p force-app/main/default/settings
946
+
947
+ # Attempt retrieve
948
+ sf project retrieve start --metadata "Settings:${CLI_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null || true
949
+
950
+ local SETTINGS_FILE="force-app/main/default/settings/${CLI_NAME}.settings-meta.xml"
951
+ if [[ -f "$SETTINGS_FILE" ]]; then
952
+ # Modify: update existing field or insert new field
953
+ if grep -q "<${FIELD}>" "$SETTINGS_FILE"; then
954
+ sed_inplace "s|<${FIELD}>[^<]*</${FIELD}>|<${FIELD}>true</${FIELD}>|g" "$SETTINGS_FILE"
955
+ else
956
+ # Insert before closing tag
957
+ sed_inplace "/<\/.*Settings>/i\\
958
+ <${FIELD}>true</${FIELD}>" "$SETTINGS_FILE"
959
+ fi
960
+
961
+ # Deploy
962
+ local DEPLOY_RESULT
963
+ DEPLOY_RESULT=$(sf project deploy start --source-dir force-app/main/default/settings --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
964
+
965
+ if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
966
+ sleep $SLEEP_MEDIUM
967
+ # VERIFY
968
+ CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
969
+ if [[ "$CURRENT" == "true" ]]; then
970
+ log_pass "$DISPLAY_NAME: enabled (CLI deploy)"
971
+ return 0
972
+ fi
973
+ log_warn "$DISPLAY_NAME: CLI deploy succeeded but SOAP verification failed"
974
+ else
975
+ local DEPLOY_ERR
976
+ DEPLOY_ERR=$(safe_jq "$DEPLOY_RESULT" '.result.details.componentFailures[0].problem // .message // "unknown"' "unknown")
977
+ echo " CLI deploy error: $DEPLOY_ERR"
978
+ fi
979
+ else
980
+ echo " CLI retrieve returned no settings file for $CLI_NAME"
981
+ fi
982
+
983
+ # --- APPROACH 3: REST Composite API (for settings that support it) ---
984
+ log_try "$DISPLAY_NAME: trying REST API PATCH..."
985
+ show_progress "REST API PATCH for $DISPLAY_NAME"
986
+ local REST_RESULT
987
+ REST_RESULT=$(curl -s --max-time $CURL_TIMEOUT -X PATCH "${INSTANCE_URL}/services/data/v${API_VERSION}/tooling/sobjects/${META_TYPE}/${FULL_NAME}" \
988
+ -H "Authorization: Bearer ${ACCESS_TOKEN}" \
989
+ -H "Content-Type: application/json" \
990
+ -d "{\"${FIELD}\": true}" 2>/dev/null || echo "")
991
+ clear_progress
992
+
993
+ # REST may return empty on success (204) or an error body
994
+ if [[ -z "$REST_RESULT" ]] || echo "$REST_RESULT" | grep -qi "\"success\":true\|\"id\""; then
995
+ sleep $SLEEP_MEDIUM
996
+ CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
997
+ if [[ "$CURRENT" == "true" ]]; then
998
+ log_pass "$DISPLAY_NAME: enabled (REST API)"
999
+ return 0
1000
+ fi
1001
+ fi
1002
+
1003
+ # --- ALL APPROACHES EXHAUSTED ---
1004
+ # Final verification attempt after waiting
1005
+ sleep $SLEEP_LONG
1006
+ CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
1007
+ if [[ "$CURRENT" == "true" ]]; then
1008
+ log_pass "$DISPLAY_NAME: enabled (delayed propagation)"
1009
+ return 0
1010
+ fi
1011
+
1012
+ log_fail "$DISPLAY_NAME: COULD NOT ENABLE (all 3 approaches failed)"
1013
+ return 1
1014
+ }
1015
+
1016
+ # ============================================================
1017
+ # Phase 2: Prerequisites (with verification gate)
1018
+ # ============================================================
1019
+ section "Phase 2: Prerequisites"
1020
+
1021
+ PREREQ_BLOCKERS=0
1022
+
1023
+ # --- 2a: Einstein Generative AI (CRITICAL — everything depends on this) ---
1024
+ GENAI_RESULT=0
1025
+ enable_setting "EinsteinGptSettings" "EinsteinGpt" "enableEinsteinGptPlatform" "Einstein Generative AI" || GENAI_RESULT=$?
1026
+
1027
+ if [[ "$GENAI_RESULT" -eq 2 ]]; then
1028
+ # Metadata type not available (INVALID_TYPE) - org lacks required license/edition
1029
+ echo ""
1030
+ echo " BLOCKER: Einstein Generative AI metadata type not available in this org."
1031
+ echo " This is the foundation for ALL Agentforce features."
1032
+ echo ""
1033
+ echo " Root cause: EinsteinGptSettings metadata type returned INVALID_TYPE"
1034
+ echo " This means the org is missing:"
1035
+ echo " - Enterprise Edition (or higher) with Einstein features"
1036
+ echo " - Einstein Generative AI licenses"
1037
+ echo " - Platform-level Einstein feature enablement"
1038
+ echo ""
1039
+ echo " Resolution:"
1040
+ echo " 1. Verify org edition: Setup → Company Information"
1041
+ echo " Required: Enterprise Edition or higher"
1042
+ echo " 2. Check licenses: Setup → Company Information → User Licenses"
1043
+ echo " Look for: 'Einstein', 'Sales Cloud Einstein', 'Service Cloud Einstein'"
1044
+ echo " 3. If licenses are missing:"
1045
+ echo " → Contact your Salesforce account team to provision Einstein licenses"
1046
+ echo " → Trial orgs: Request Einstein trial extension"
1047
+ echo " 4. Re-run this script: ./setup-all.sh $ORG_ALIAS"
1048
+ echo ""
1049
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1050
+ elif [[ "$GENAI_RESULT" -ne 0 ]]; then
1051
+ # Other failure (return code 1) - could be ToS, permissions, or other config issue
1052
+ echo ""
1053
+ echo " BLOCKER: Einstein Generative AI cannot be enabled."
1054
+ echo " This is the foundation for ALL Agentforce features."
1055
+ echo ""
1056
+ echo " Common causes:"
1057
+ echo " - Org edition doesn't include Einstein GenAI features"
1058
+ echo " - Einstein Terms of Service not accepted (Setup → Einstein Setup)"
1059
+ echo " - Org-level feature toggle disabled by Salesforce"
1060
+ echo " - Missing required licenses for Einstein features"
1061
+ echo ""
1062
+ echo " Manual resolution:"
1063
+ echo " 1. Setup → Einstein Setup → Review and accept Terms of Service"
1064
+ echo " 2. Setup → Einstein Generative AI → Enable"
1065
+ echo " 3. Check Setup → Company Information → User Licenses for Einstein/Agentforce licenses"
1066
+ echo ""
1067
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1068
+ fi
1069
+
1070
+ # --- 2b: Agentforce Agent (CRITICAL — depends on GenAI) ---
1071
+ COPILOT_RESULT=0
1072
+ enable_setting "EinsteinCopilotSettings" "EinsteinCopilot" "enableEinsteinGptCopilot" "Agentforce Agent" || COPILOT_RESULT=$?
1073
+
1074
+ if [[ "$COPILOT_RESULT" -eq 2 ]]; then
1075
+ # Metadata type not available (INVALID_TYPE) - org lacks required license/edition
1076
+ echo ""
1077
+ echo " BLOCKER: Agentforce Agent metadata type not available in this org."
1078
+ echo " This indicates the org does NOT have the required Salesforce edition or licenses."
1079
+ echo ""
1080
+ echo " Root cause: EinsteinCopilotSettings metadata type returned INVALID_TYPE"
1081
+ echo " This means the org is missing:"
1082
+ echo " - Enterprise Edition (or higher) with Einstein/Agentforce features"
1083
+ echo " - Einstein Agent licenses"
1084
+ echo " - Agentforce for Sales add-on licenses"
1085
+ echo ""
1086
+ echo " Resolution:"
1087
+ echo " 1. Verify org edition: Setup → Company Information"
1088
+ echo " Required: Enterprise Edition or higher"
1089
+ echo " 2. Check licenses: Setup → Company Information → User Licenses"
1090
+ echo " Look for: 'Einstein Agent', 'Agentforce for Sales', 'Sales Cloud Einstein'"
1091
+ echo " 3. If licenses are missing:"
1092
+ echo " → Contact your Salesforce account team to provision licenses"
1093
+ echo " → Trial orgs: Request Einstein/Agentforce trial extension"
1094
+ echo " 4. Re-run this script: ./setup-all.sh $ORG_ALIAS"
1095
+ echo ""
1096
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1097
+ elif [[ "$COPILOT_RESULT" -ne 0 ]]; then
1098
+ # Other failure (return code 1) - could be ToS, permissions, or other config issue
1099
+ echo ""
1100
+ echo " BLOCKER: Agentforce Agent cannot be enabled."
1101
+ echo " Requires Einstein Generative AI (step above) to be enabled first."
1102
+ echo ""
1103
+ echo " Common causes:"
1104
+ echo " - Missing Einstein Agent licenses"
1105
+ echo " - Org edition doesn't include Einstein/Agentforce features"
1106
+ echo " - Einstein Terms of Service not accepted"
1107
+ echo ""
1108
+ # Check if Einstein Agent licenses exist
1109
+ EA_LICENSE=$(get_license_count "Einstein Agent")
1110
+ if [[ "$EA_LICENSE" == "ERROR" ]]; then
1111
+ echo " License Check: ⚠️ Could not verify license status (API error)"
1112
+ echo " → Check org connectivity and try again"
1113
+ echo ""
1114
+ elif [[ "$EA_LICENSE" -eq 0 ]]; then
1115
+ echo " License Check: ❌ No Einstein Agent licenses found"
1116
+ echo " → Contact your Salesforce account team to provision Einstein Agent licenses"
1117
+ echo " → View licenses: Setup → Company Information → User Licenses"
1118
+ echo ""
1119
+ else
1120
+ echo " License Check: ✓ Einstein Agent licenses available ($EA_LICENSE)"
1121
+ echo ""
1122
+ echo " Most likely cause: Einstein Terms of Service NOT accepted"
1123
+ echo ""
1124
+ echo " Required action:"
1125
+ echo " 1. Navigate to: ${INSTANCE_URL}/lightning/setup/EinsteinGPTSetup/home"
1126
+ echo " (Note: Assumes Lightning Experience. For Classic, use Setup menu.)"
1127
+ echo " 2. Review and accept the Einstein Terms of Service"
1128
+ echo " 3. Then enable Agentforce Agent at: ${INSTANCE_URL}/lightning/setup/EinsteinCopilot/home"
1129
+ echo " 4. Re-run this script: ./setup-all.sh $ORG_ALIAS"
1130
+ echo ""
1131
+ fi
1132
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1133
+ fi
1134
+
1135
+ # --- 2b2: Agentforce Studio / Agent Platform (CRITICAL — gates Deal Agent) ---
1136
+ # AgentPlatformSettings.enableAgentPlatform (OrgPreference AgentPlatformEnabled)
1137
+ # must be ON before SalesDealAgentSettings can be enabled. Core enforces this:
1138
+ # DealAgentEnabledOrgPreference.validateAgentforcePlatformAndLog() throws if the
1139
+ # Agentforce Platform preference is not enabled, so a missing step here surfaces
1140
+ # later as an opaque Deal Agent activation failure. Ordering: GenAI → Copilot →
1141
+ # AgentPlatform → SalesDealAgent.
1142
+ AGENT_PLATFORM_RESULT=0
1143
+ enable_setting "AgentPlatformSettings" "AgentPlatform" "enableAgentPlatform" "Agentforce Studio (Agent Platform)" || AGENT_PLATFORM_RESULT=$?
1144
+
1145
+ if [[ "$AGENT_PLATFORM_RESULT" -eq 2 ]]; then
1146
+ # Metadata type not available (INVALID_TYPE) - org lacks required license/edition
1147
+ echo ""
1148
+ echo " BLOCKER: Agentforce Studio (Agent Platform) metadata type not available in this org."
1149
+ echo " AgentPlatformSettings returned INVALID_TYPE — the org is missing the"
1150
+ echo " Enterprise Edition (or higher) or Agentforce licenses required for Agent Platform."
1151
+ echo ""
1152
+ echo " Resolution:"
1153
+ echo " 1. Verify org edition: Setup → Company Information (Enterprise Edition or higher)"
1154
+ echo " 2. Check licenses: Setup → Company Information → User Licenses"
1155
+ echo " Look for: 'Agentforce', 'Einstein Agent', 'Agentforce for Sales'"
1156
+ echo " 3. Re-run this script: ./setup-all.sh $ORG_ALIAS"
1157
+ echo ""
1158
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1159
+ elif [[ "$AGENT_PLATFORM_RESULT" -ne 0 ]]; then
1160
+ echo ""
1161
+ echo " BLOCKER: Agentforce Studio (Agent Platform) cannot be enabled."
1162
+ echo " This preference (AgentPlatformEnabled) gates the Sales Deal Agent —"
1163
+ echo " Deal Agent activation fails downstream if it is not enabled first."
1164
+ echo ""
1165
+ echo " Common causes:"
1166
+ echo " - Einstein Generative AI / Agentforce Agent not enabled (steps above)"
1167
+ echo " - Missing Agentforce licenses"
1168
+ echo " - Einstein Terms of Service not accepted"
1169
+ echo ""
1170
+ echo " Manual resolution:"
1171
+ echo " 1. Enable Einstein GenAI and Agentforce Agent (steps above)"
1172
+ echo " 2. Navigate to: ${INSTANCE_URL}/lightning/setup/EinsteinAgentBuilder/home"
1173
+ echo " (Agentforce Studio) and confirm the platform is enabled"
1174
+ echo " 3. Re-run this script: ./setup-all.sh $ORG_ALIAS"
1175
+ echo ""
1176
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1177
+ fi
1178
+
1179
+ # --- 2c: Enhanced Notes (CRITICAL — PM data source) ---
1180
+ if ! enable_setting "EnhancedNotesSettings" "EnhancedNotes" "enableEnhancedNotes" "Enhanced Notes"; then
1181
+ echo ""
1182
+ echo " BLOCKER: Enhanced Notes cannot be enabled."
1183
+ echo " Pipeline Management uses ContentNote as a primary data source."
1184
+ echo ""
1185
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1186
+ fi
1187
+
1188
+ # --- 2d: Opportunity Team (CRITICAL — flow uses OpportunityTeamMember) ---
1189
+ if ! enable_setting "OpportunitySettings" "Opportunity" "enableOpportunityTeam" "Opportunity Team"; then
1190
+ echo ""
1191
+ echo " BLOCKER: Opportunity Team cannot be enabled."
1192
+ echo " Pipeline Management flow requires OpportunityTeamMember object access."
1193
+ echo ""
1194
+ PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
1195
+ fi
1196
+
1197
+ # --- 2e: Pipeline Inspection (required for UI review of suggestions) ---
1198
+ enable_setting "OpportunitySettings" "Opportunity" "enablePipelineInspection" "Pipeline Inspection" || \
1199
+ log_warn "Pipeline Inspection not enabled — suggestions generate but users have no UI to review/accept them"
1200
+
1201
+ # --- 2f: Enhanced Email (optional) ---
1202
+ enable_setting "EmailAdministrationSettings" "EmailAdministration" "enableEnhancedEmailEnabled" "Enhanced Email" || \
1203
+ log_warn "Enhanced Email not enabled (email body indexing unavailable — agent uses notes instead)"
1204
+
1205
+ # --- VERIFICATION GATE: All critical prerequisites must pass ---
1206
+ echo ""
1207
+ echo " --- Prerequisite Verification Gate ---"
1208
+
1209
+ GATE_PASS=true
1210
+
1211
+ # Classify each gate setting so a real "disabled" is distinguished from an
1212
+ # auth/SOAP fault or a network/service error (W-23215519). gate_check logs the
1213
+ # right diagnostic and fails the gate for any non-"true" outcome.
1214
+ # Args: $1 = display label, $2 = classification string from soap_classify
1215
+ gate_check() {
1216
+ local label="$1" status="$2"
1217
+ case "$status" in
1218
+ true)
1219
+ return 0
1220
+ ;;
1221
+ false)
1222
+ log_fail "GATE: $label NOT confirmed (setting is disabled)"
1223
+ ;;
1224
+ AUTH_ERROR:*)
1225
+ log_fail "GATE: $label cannot be verified — auth/SOAP fault (${status#AUTH_ERROR:})"
1226
+ echo " Re-authenticate: sf org login web --alias $ORG_ALIAS"
1227
+ ;;
1228
+ TYPE_UNAVAILABLE:*)
1229
+ log_fail "GATE: $label cannot be verified — metadata type unavailable (${status#TYPE_UNAVAILABLE:})"
1230
+ ;;
1231
+ NETWORK_ERROR)
1232
+ log_fail "GATE: $label cannot be verified — network/service error (check connectivity)"
1233
+ ;;
1234
+ *)
1235
+ log_fail "GATE: $label NOT confirmed (unexpected status: $status)"
1236
+ ;;
1237
+ esac
1238
+ GATE_PASS=false
1239
+ return 1
1240
+ }
1241
+
1242
+ GENAI_STATUS=$(soap_classify "$(soap_read "EinsteinGptSettings" "EinsteinGpt")" "enableEinsteinGptPlatform")
1243
+ COPILOT_STATUS=$(soap_classify "$(soap_read "EinsteinCopilotSettings" "EinsteinCopilot")" "enableEinsteinGptCopilot")
1244
+ AGENT_PLATFORM_STATUS=$(soap_classify "$(soap_read "AgentPlatformSettings" "AgentPlatform")" "enableAgentPlatform")
1245
+ NOTES_STATUS=$(soap_classify "$(soap_read "EnhancedNotesSettings" "EnhancedNotes")" "enableEnhancedNotes")
1246
+ OPP_TEAM_STATUS=$(soap_classify "$(soap_read "OpportunitySettings" "Opportunity")" "enableOpportunityTeam")
1247
+
1248
+ # Keep boolean mirrors for the existing failure-summary conditionals below.
1249
+ VERIFY_GENAI=$([[ "$GENAI_STATUS" == "true" ]] && echo "true" || echo "false")
1250
+ VERIFY_COPILOT=$([[ "$COPILOT_STATUS" == "true" ]] && echo "true" || echo "false")
1251
+ VERIFY_AGENT_PLATFORM=$([[ "$AGENT_PLATFORM_STATUS" == "true" ]] && echo "true" || echo "false")
1252
+ VERIFY_NOTES=$([[ "$NOTES_STATUS" == "true" ]] && echo "true" || echo "false")
1253
+ VERIFY_OPP_TEAM=$([[ "$OPP_TEAM_STATUS" == "true" ]] && echo "true" || echo "false")
1254
+
1255
+ gate_check "Einstein Generative AI" "$GENAI_STATUS" || true
1256
+ gate_check "Agentforce Agent" "$COPILOT_STATUS" || true
1257
+ gate_check "Agentforce Studio (Agent Platform)" "$AGENT_PLATFORM_STATUS" || true
1258
+ gate_check "Enhanced Notes" "$NOTES_STATUS" || true
1259
+ gate_check "Opportunity Team" "$OPP_TEAM_STATUS" || true
1260
+
1261
+ if [[ "$GATE_PASS" != "true" ]]; then
1262
+ echo ""
1263
+ log_fail "PREREQUISITE GATE FAILED — cannot proceed to Pipeline Management enablement."
1264
+ echo ""
1265
+ echo " The following critical settings must be enabled before PM can be activated:"
1266
+ [[ "$VERIFY_GENAI" != "true" ]] && echo " ❌ EinsteinGptSettings.enableEinsteinGptPlatform"
1267
+ [[ "$VERIFY_COPILOT" != "true" ]] && echo " ❌ EinsteinCopilotSettings.enableEinsteinGptCopilot"
1268
+ [[ "$VERIFY_AGENT_PLATFORM" != "true" ]] && echo " ❌ AgentPlatformSettings.enableAgentPlatform"
1269
+ [[ "$VERIFY_NOTES" != "true" ]] && echo " ❌ EnhancedNotesSettings.enableEnhancedNotes"
1270
+ [[ "$VERIFY_OPP_TEAM" != "true" ]] && echo " ❌ OpportunitySettings.enableOpportunityTeam"
1271
+ echo ""
1272
+ echo " Manual resolution:"
1273
+ echo " 1. Setup → Einstein → Einstein Generative AI → Enable"
1274
+ echo " 2. Setup → Einstein → Copilot → Enable"
1275
+ echo " 3. Setup → Agentforce Studio → Enable (Agent Platform)"
1276
+ echo " 4. Setup → Notes → Enable Enhanced Notes"
1277
+ echo " 5. Setup → Opportunity Settings → Enable Opportunity Team"
1278
+ echo " 6. Re-run: bash setup-all.sh $ORG_ALIAS"
1279
+ exit 1
1280
+ fi
1281
+
1282
+ log_pass "GATE: All critical prerequisites verified ✓"
1283
+
1284
+ # ============================================================
1285
+ # Phase 2.5: License and Metadata Type Validation
1286
+ # ============================================================
1287
+ echo ""
1288
+ echo " --- Pipeline Management Compatibility Gate ---"
1289
+
1290
+ # Expected license names: "Agentforce for Sales", "Sales Cloud Einstein", "Agentforce 1"
1291
+ # If Salesforce introduces new variants, this query may need updating
1292
+ SALES_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)
1293
+ SALES_LICENSE_COUNT=$(safe_jq_int "$SALES_LICENSE_OUTPUT" '.result.records[0].expr0 // 0')
1294
+
1295
+ EINSTEIN_AGENT_COUNT=$(get_license_count "Einstein Agent")
1296
+ # Handle API errors gracefully
1297
+ if [[ "$EINSTEIN_AGENT_COUNT" == "ERROR" ]]; then
1298
+ EINSTEIN_AGENT_COUNT="unknown"
1299
+ fi
1300
+
1301
+ # Check if SalesDealAgentSettings metadata type is available
1302
+ METADATA_TYPE_CHECK=$(soap_read "SalesDealAgentSettings" "SalesDealAgent" 2>/dev/null || echo "")
1303
+
1304
+ # Backstop only — the Step 0 gate (Phase 0.5) is the primary check and normally
1305
+ # stops an unlicensed org before any question. This catches a post-enablement
1306
+ # regression (e.g. the type going away mid-run). Shares print_license_blocker().
1307
+ if echo "$METADATA_TYPE_CHECK" | grep -q "INVALID_TYPE"; then
1308
+ log_fail "GATE: SalesDealAgentSettings metadata type NOT available"
1309
+ print_license_blocker "$EINSTEIN_AGENT_COUNT" "$SALES_LICENSE_COUNT"
1310
+ exit 1
1311
+ fi
1312
+
1313
+ if [[ "$SALES_LICENSE_COUNT" -eq 0 ]]; then
1314
+ log_warn "GATE: No Agentforce for Sales licenses found (but metadata type exists)"
1315
+ echo ""
1316
+ echo " Warning: The org has the metadata type but no explicit Sales licenses."
1317
+ echo " Pipeline Management may work via base Einstein Agent licenses ($EINSTEIN_AGENT_COUNT)."
1318
+ echo " Proceeding with enablement attempt..."
1319
+ echo ""
1320
+ else
1321
+ log_pass "GATE: Agentforce for Sales licenses found ($SALES_LICENSE_COUNT) ✓"
1322
+ fi
1323
+
1324
+ # ============================================================
1325
+ # Phase 3: Enable Pipeline Management (with multi-approach + verification)
1326
+ # ============================================================
1327
+ section "Phase 3: Pipeline Management"
1328
+
1329
+ PM_ENABLED=false
1330
+
1331
+ # Check current state
1332
+ PM_CURRENT=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
1333
+ if [[ "$PM_CURRENT" == "true" ]]; then
1334
+ log_pass "Pipeline Management: already enabled"
1335
+ PM_ENABLED=true
1336
+ fi
1337
+
1338
+ if [[ "$PM_ENABLED" != "true" ]]; then
1339
+ # --- APPROACH 1: SOAP updateMetadata ---
1340
+ log_try "Enabling via SOAP Metadata API v${API_VERSION}..."
1341
+ RESULT=$(soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>true</met:enableDealAgent><met:enableDealAgentAutoApproveAllTasks>false</met:enableDealAgentAutoApproveAllTasks></met:metadata>")
1342
+
1343
+ if echo "$RESULT" | grep -q "<success>true</success>"; then
1344
+ sleep $SLEEP_MEDIUM
1345
+ PM_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
1346
+ if [[ "$PM_CHECK" == "true" ]]; then
1347
+ log_pass "Pipeline Management: enabled (SOAP)"
1348
+ PM_ENABLED=true
1349
+ fi
1350
+ fi
1351
+
1352
+ if [[ "$PM_ENABLED" != "true" ]]; then
1353
+ SOAP_ERR=$(echo "$RESULT" | grep -o '<message>[^<]*</message>' | sed 's/<[^>]*>//g' | head -1 || echo "")
1354
+ [[ -n "$SOAP_ERR" ]] && echo " SOAP error: $(redact_token "$SOAP_ERR")"
1355
+ fi
1356
+ fi
1357
+
1358
+ if [[ "$PM_ENABLED" != "true" ]]; then
1359
+ # --- APPROACH 2: CLI Metadata deploy (SalesDealAgent settings XML) ---
1360
+ log_try "Enabling via CLI Metadata deploy..."
1361
+ ensure_sfdx_project
1362
+ mkdir -p force-app/main/default/settings
1363
+
1364
+ # Create settings XML directly (CLI retrieve fails for this type)
1365
+ cat > force-app/main/default/settings/SalesDealAgent.settings-meta.xml << 'XMLEOF'
1366
+ <?xml version="1.0" encoding="UTF-8"?>
1367
+ <SalesDealAgentSettings xmlns="http://soap.sforce.com/2006/04/metadata">
1368
+ <enableDealAgent>true</enableDealAgent>
1369
+ <enableDealAgentAutoApproveAllTasks>false</enableDealAgentAutoApproveAllTasks>
1370
+ </SalesDealAgentSettings>
1371
+ XMLEOF
1372
+
1373
+ 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}')
1374
+ if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
1375
+ sleep $SLEEP_LONG
1376
+ PM_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
1377
+ if [[ "$PM_CHECK" == "true" ]]; then
1378
+ log_pass "Pipeline Management: enabled (CLI deploy)"
1379
+ PM_ENABLED=true
1380
+ fi
1381
+ else
1382
+ DEPLOY_ERR=$(safe_jq "$DEPLOY_RESULT" '.result.details.componentFailures[0].problem // .message // "unknown"' "unknown")
1383
+ echo " CLI deploy error: $DEPLOY_ERR"
1384
+ fi
1385
+ fi
1386
+
1387
+ if [[ "$PM_ENABLED" != "true" ]]; then
1388
+ # --- APPROACH 3: Recognize genuine mid-provisioning WITHOUT asserting enablement ---
1389
+ # #9: PSG existence is NOT a setup signal — SalesManagementUserPsg /
1390
+ # SalesManagementAgentUserPsg ship with the Agentforce-for-Sales license and exist even
1391
+ # in a never-configured org, so "PSGs exist" says nothing about whether PM was set up.
1392
+ # The authoritative signal is SalesDealAgentSettings.enableDealAgent via SOAP readMetadata
1393
+ # (checked above). Here we re-read it authoritatively; only if it reads true do we mark
1394
+ # enabled. If it's still false, we check the REAL "setup has run" signals — an agent user
1395
+ # holding SalesManagementAgentUserPsg, a cloned scheduled flow, or a PM BotDefinition. If
1396
+ # any is present, a bundle-publish/template-provisioned org may be mid-flight, so we mark
1397
+ # "unconfirmed" and continue, letting the downstream verification gate confirm. Otherwise
1398
+ # we fall through to Approach 4.
1399
+ log_try "Re-reading enableDealAgent authoritatively (SOAP) and checking real setup signals..."
1400
+ PM_RECHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
1401
+
1402
+ if [[ "$PM_RECHECK" == "true" ]]; then
1403
+ log_pass "Pipeline Management: enabled (confirmed via SOAP enableDealAgent)"
1404
+ PM_ENABLED=true
1405
+ else
1406
+ AGENT_PSG_HELD=$(pm_agent_user_holds_psg "$ORG_ALIAS")
1407
+ PM_FLOW_EXISTS=$(pm_flow_clone_exists "$ORG_ALIAS")
1408
+ PM_BOT_EXISTS=$(pm_bot_count "$ORG_ALIAS")
1409
+ if [[ "$AGENT_PSG_HELD" -ge 1 || "$PM_FLOW_EXISTS" -ge 1 || "$PM_BOT_EXISTS" -ge 1 ]]; then
1410
+ 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."
1411
+ echo " Continuing with setup; enablement is UNCONFIRMED and will be re-verified in Phase 8."
1412
+ PM_ENABLED="unconfirmed"
1413
+ fi
1414
+ fi
1415
+ fi
1416
+
1417
+ if [[ "$PM_ENABLED" != "true" && "$PM_ENABLED" != "unconfirmed" ]]; then
1418
+ # --- APPROACH 4: OrgPreference via Tooling API ---
1419
+ log_try "Enabling via OrgPreference Tooling API..."
1420
+ show_progress "Tooling API OrgPreference update"
1421
+ PREF_RESULT=$(curl -s --max-time $CURL_TIMEOUT -X PATCH "${INSTANCE_URL}/services/data/v${API_VERSION}/tooling/sobjects/OrganizationSettingsDetail/DealAgentEnabled" \
1422
+ -H "Authorization: Bearer ${ACCESS_TOKEN}" \
1423
+ -H "Content-Type: application/json" \
1424
+ -d '{"value": true}' 2>/dev/null || echo "")
1425
+ clear_progress
1426
+
1427
+ sleep $SLEEP_LONG
1428
+ PM_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
1429
+ if [[ "$PM_CHECK" == "true" ]]; then
1430
+ log_pass "Pipeline Management: enabled (Tooling API OrgPreference)"
1431
+ PM_ENABLED=true
1432
+ else
1433
+ # Do NOT treat PSG presence as a provisioning signal (the PM PSGs ship with the
1434
+ # license). Re-check the same real signals as Approach 3: an agent user holding
1435
+ # SalesManagementAgentUserPsg, a cloned scheduled flow, or a PM BotDefinition.
1436
+ AGENT_PSG_HELD=$(pm_agent_user_holds_psg "$ORG_ALIAS")
1437
+ PM_FLOW_EXISTS=$(pm_flow_clone_exists "$ORG_ALIAS")
1438
+ PM_BOT_EXISTS=$(pm_bot_count "$ORG_ALIAS")
1439
+ if [[ "$AGENT_PSG_HELD" -ge 1 || "$PM_FLOW_EXISTS" -ge 1 || "$PM_BOT_EXISTS" -ge 1 ]]; then
1440
+ 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"
1441
+ PM_ENABLED="unconfirmed"
1442
+ fi
1443
+ fi
1444
+ fi
1445
+
1446
+ # --- PM VERIFICATION GATE ---
1447
+ if [[ "$PM_ENABLED" != "true" && "$PM_ENABLED" != "unconfirmed" ]]; then
1448
+ echo ""
1449
+ log_fail "PIPELINE MANAGEMENT GATE FAILED — all 4 approaches exhausted."
1450
+ echo ""
1451
+ echo " Attempted:"
1452
+ echo " 1. SOAP Metadata API v${API_VERSION} (updateMetadata SalesDealAgentSettings)"
1453
+ echo " 2. CLI Metadata deploy (settings XML)"
1454
+ echo " 3. Authoritative SOAP re-read + provisioning-signal detection (agent-user / flow / bot presence)"
1455
+ echo " 4. Tooling API OrgPreference (DealAgentEnabled)"
1456
+ echo ""
1457
+ echo " This org may require manual enablement:"
1458
+ echo " Setup → Sales → Pipeline Management → Enable toggle"
1459
+ echo " URL: ${INSTANCE_URL}/lightning/setup/PipelineManagement/home"
1460
+ echo ""
1461
+ echo " After enabling, re-run: bash setup-all.sh $ORG_ALIAS"
1462
+ exit 1
1463
+ fi
1464
+
1465
+ # ============================================================
1466
+ # Phase 3.5: Enable Autonomous Field Updates
1467
+ # ============================================================
1468
+ section "Phase 3.5: Autonomous Field Updates"
1469
+
1470
+ # After Pipeline Management is enabled, set enableDealAgentAutoApproveAllTasks=true
1471
+ # to allow the agent to update opportunity fields without user approval.
1472
+ #
1473
+ # CONSENT GATE (#2): autonomous updates are opt-in only. Without --autonomous (or an
1474
+ # interactive "yes" in Phase 0), the agent stays in suggestion-only mode and this phase
1475
+ # makes NO change. This prevents the agent from silently auto-applying field writes.
1476
+
1477
+ AUTO_APPROVE_CURRENT=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgentAutoApproveAllTasks")
1478
+
1479
+ if [[ "$ENABLE_AUTONOMOUS" != "true" ]]; then
1480
+ if [[ "$AUTO_APPROVE_CURRENT" == "true" ]]; then
1481
+ log_warn "Autonomous updates are currently ON in this org, but --autonomous was not passed."
1482
+ echo " Leaving the existing org setting unchanged (not disabling)."
1483
+ echo " To turn autonomous updates OFF, use the Pipeline Management setup UI."
1484
+ else
1485
+ log_pass "Autonomous updates: OFF (suggestions only) — not enabled without --autonomous"
1486
+ fi
1487
+ elif [[ "$AUTO_APPROVE_CURRENT" == "true" ]]; then
1488
+ log_pass "Autonomous updates: already enabled"
1489
+ else
1490
+ log_info "Enabling autonomous field updates (enableDealAgentAutoApproveAllTasks=true)..."
1491
+
1492
+ # SOAP Metadata API approach
1493
+ RESULT=$(soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>true</met:enableDealAgent><met:enableDealAgentAutoApproveAllTasks>true</met:enableDealAgentAutoApproveAllTasks></met:metadata>")
1494
+
1495
+ if echo "$RESULT" | grep -qi "success.*true"; then
1496
+ AUTO_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgentAutoApproveAllTasks")
1497
+ if [[ "$AUTO_CHECK" == "true" ]]; then
1498
+ log_pass "Autonomous updates: enabled (SOAP API)"
1499
+ else
1500
+ log_warn "SOAP update reported success, but verification failed"
1501
+ fi
1502
+ else
1503
+ log_warn "SOAP API update failed, trying CLI metadata deploy..."
1504
+
1505
+ # CLI metadata deploy approach (fallback)
1506
+ TMP_DIR=$(mktemp -d)
1507
+ mkdir -p "$TMP_DIR/settings"
1508
+
1509
+ cat > "$TMP_DIR/settings/SalesDealAgent.settings-meta.xml" <<EOF
1510
+ <?xml version="1.0" encoding="UTF-8"?>
1511
+ <SalesDealAgentSettings xmlns="http://soap.sforce.com/2006/04/metadata">
1512
+ <enableDealAgent>true</enableDealAgent>
1513
+ <enableDealAgentAutoApproveAllTasks>true</enableDealAgentAutoApproveAllTasks>
1514
+ </SalesDealAgentSettings>
1515
+ EOF
1516
+
1517
+ CLI_RESULT=$(sf project deploy start --metadata-dir "$TMP_DIR/settings" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
1518
+ rm -rf "$TMP_DIR"
1519
+
1520
+ CLI_SUCCESS=$(echo "$CLI_RESULT" | jq -r '.status // 1' 2>/dev/null | head -1 || echo "1")
1521
+ if [[ "$CLI_SUCCESS" == "0" ]]; then
1522
+ AUTO_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgentAutoApproveAllTasks")
1523
+ if [[ "$AUTO_CHECK" == "true" ]]; then
1524
+ log_pass "Autonomous updates: enabled (CLI deploy)"
1525
+ else
1526
+ log_warn "CLI deploy succeeded but verification failed"
1527
+ fi
1528
+ else
1529
+ log_warn "CLI deploy failed. Autonomous updates may require manual enablement:"
1530
+ echo " Setup → Sales → Pipeline Management → Settings"
1531
+ echo " Enable: 'Update Fields Autonomously'"
1532
+ echo " URL: ${INSTANCE_URL}/lightning/setup/PipelineManagement/home"
1533
+ fi
1534
+ fi
1535
+ fi
1536
+
1537
+ # ============================================================
1538
+ # Phase 4: Wait for Provisioning (with polling loop)
1539
+ # ============================================================
1540
+ section "Phase 4: Provisioning (agent user + PSG assignment)"
1541
+
1542
+ MAX_WAIT_SECONDS=90
1543
+ POLL_INTERVAL=5
1544
+
1545
+ # Provisioning is complete when the agent user exists AND holds
1546
+ # SalesManagementAgentUserPsg. The two PM PSGs themselves ship with the
1547
+ # Agentforce-for-Sales license and exist even in a never-configured org, so
1548
+ # "PSG count >= 2" is NOT a completion signal — the agent-user assignment is.
1549
+ check_agent_provisioned() { # sets AGENT_USER (Username) + AGENT_PSG_HELD; returns 0 when complete
1550
+ local rec agent_id
1551
+ 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 "")
1552
+ AGENT_USER=$(echo "$rec" | jq -r '.Username // empty' 2>/dev/null || echo "")
1553
+ AGENT_PSG_HELD=0
1554
+ if [[ -n "$AGENT_USER" ]]; then
1555
+ AGENT_PSG_HELD=$(pm_agent_user_holds_psg "$ORG_ALIAS")
1556
+ fi
1557
+ [[ -n "$AGENT_USER" && "${AGENT_PSG_HELD:-0}" -ge 1 ]]
1558
+ }
1559
+
1560
+ if check_agent_provisioned; then
1561
+ log_pass "Agent user: $AGENT_USER"
1562
+ log_pass "Agent user holds SalesManagementAgentUserPsg"
1563
+ else
1564
+ echo -n " Waiting for auto-provisioning (up to ${MAX_WAIT_SECONDS}s)"
1565
+ WAITED=0
1566
+ while [[ $WAITED -lt $MAX_WAIT_SECONDS ]]; do
1567
+ if check_agent_provisioned; then
1568
+ echo ""
1569
+ log_pass "Agent user: $AGENT_USER"
1570
+ log_pass "Agent user holds SalesManagementAgentUserPsg"
1571
+ break
1572
+ fi
1573
+
1574
+ echo -n "."
1575
+ sleep "$POLL_INTERVAL"
1576
+ WAITED=$((WAITED + POLL_INTERVAL))
1577
+ done
1578
+
1579
+ if [[ $WAITED -ge $MAX_WAIT_SECONDS ]]; then
1580
+ echo ""
1581
+ # Report what we have
1582
+ if [[ -n "$AGENT_USER" ]]; then
1583
+ log_pass "Agent user: $AGENT_USER"
1584
+ [[ "${AGENT_PSG_HELD:-0}" -ge 1 ]] && log_pass "Agent user holds SalesManagementAgentUserPsg" || log_warn "Agent user does not yet hold SalesManagementAgentUserPsg"
1585
+ else
1586
+ log_warn "Agent user: not yet provisioned"
1587
+ fi
1588
+ echo ""
1589
+ echo " Provisioning may still be in progress. Continuing..."
1590
+ echo " (Re-run verify-all.sh after 5 minutes to confirm.)"
1591
+ fi
1592
+ fi
1593
+
1594
+ # ------------------------------------------------------------
1595
+ # Phase 4a: Report recalculation Status for BOTH PSGs (no waiting)
1596
+ # ------------------------------------------------------------
1597
+ # #10: report the recalculation Status for BOTH SalesManagementUserPsg AND the
1598
+ # autonomous agent user's SalesManagementAgentUserPsg (previously only the former
1599
+ # was surfaced).
1600
+ #
1601
+ # What triggers each PSG's recalc:
1602
+ # - SalesManagementUserPsg: define-agent-access.sh inserts a
1603
+ # PermissionSetGroupComponent (the custom Agent Access permset), which fires
1604
+ # its recalc. That is a CUSTOM addition this skill owns.
1605
+ # - SalesManagementAgentUserPsg: this is a MANAGED PSG owned by the Deal Agent
1606
+ # package. Its components (and thus its recalc) are established by provisioning
1607
+ # (bundle publish / enablement) — we deliberately do NOT insert a component
1608
+ # into it, because writing to a managed PSG's membership is not ours to do and
1609
+ # PermissionSetGroup.Status is not client-writable. So for the agent PSG we
1610
+ # only REPORT Status; a lingering non-'Updated' here points to a provisioning
1611
+ # issue, not something setup can nudge.
1612
+ #
1613
+ # Both recalcs converge asynchronously — we deliberately do NOT block on either
1614
+ # (completion only matters when a user views suggestions in the UI or the
1615
+ # scheduled flow runs). We read Status once for reporting; a 'Failed' Status is
1616
+ # tracked as an issue.
1617
+ for PSG_NAME in SalesManagementUserPsg SalesManagementAgentUserPsg; do
1618
+ PSG_STATE=$(psg_status "$ORG_ALIAS" "$PSG_NAME")
1619
+ case "$PSG_STATE" in
1620
+ Updated) log_pass "PSG '$PSG_NAME' Status: Updated" ;;
1621
+ not-found) log_info "PSG '$PSG_NAME' not present yet (provisioning may still be in progress)" ;;
1622
+ 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." ;;
1623
+ *) log_info "PSG '$PSG_NAME' Status: $PSG_STATE — recalculation is async and will complete on its own." ;;
1624
+ esac
1625
+ done
1626
+
1627
+ # ============================================================
1628
+ # Phase 4b: Agent Verification
1629
+ # ============================================================
1630
+ # CRITICAL: A Pipeline Management BotDefinition is REQUIRED for a functional
1631
+ # Pipeline Management agent. Users need an interactive Sales Agent they can
1632
+ # chat with.
1633
+ #
1634
+ # Detection matches the scheduled flow's logic: BotDefinition.AgentTemplate
1635
+ # IN ('SalesMgmt__NGASalesAgent','SalesMgmt__SalesAgent'). We do NOT match
1636
+ # by hardcoded DeveloperName — different orgs publish under different local
1637
+ # names (SalesAgent, Sale_Agent, etc.).
1638
+ #
1639
+ # This phase verifies:
1640
+ # 1. A PM BotDefinition exists (created via authoring bundle)
1641
+ # 2. Agent user + PSG provisioned
1642
+ #
1643
+ # If BotDefinition is missing, it MUST be created. The script exits 1 on failure.
1644
+ # ============================================================
1645
+
1646
+ section "Phase 4b: Agent Architecture Verification"
1647
+
1648
+ # Check 1: Does a PM BotDefinition exist (matched by AgentTemplate)?
1649
+ AGENT_EXISTS=$(pm_bot_count "$ORG_ALIAS")
1650
+
1651
+ if [[ "$AGENT_EXISTS" -ge 1 ]]; then
1652
+ PM_BOT_NAME=$(pm_bot_developer_name "$ORG_ALIAS")
1653
+ log_pass "BotDefinition:${PM_BOT_NAME:-<unknown>} exists (matched by AgentTemplate)"
1654
+
1655
+ # Existence is not enough: auto-provisioning leaves the BotVersion Inactive.
1656
+ # An inactive agent means the scheduled flow never adds the agent user to
1657
+ # opportunity teams (no suggestions) and Agent Access (Phase 4c) can't be
1658
+ # defined. Verify an active version exists and activate the latest if not.
1659
+ BOT_ACTIVE_COUNT=$(pm_bot_active_version_count "$ORG_ALIAS")
1660
+ if [[ "$BOT_ACTIVE_COUNT" -ge 1 ]]; then
1661
+ log_pass "BotDefinition:${PM_BOT_NAME:-<unknown>} has an active version"
1662
+ else
1663
+ log_warn "BotDefinition:${PM_BOT_NAME:-<unknown>} has no active version — activating (auto-provisioned bots land Inactive)..."
1664
+ if activate_pm_bot_if_inactive "$ORG_ALIAS"; then
1665
+ log_pass "BotDefinition:${PM_BOT_NAME:-<unknown>} activated"
1666
+ else
1667
+ 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"
1668
+ fi
1669
+ fi
1670
+ else
1671
+ # BotDefinition missing - must be created
1672
+ # Re-check agent user (may have been set in Phase 4)
1673
+ 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 "")
1674
+
1675
+ if [[ -n "$AGENT_USER_RECHECK" ]]; then
1676
+ # Agent user exists but BotDefinition missing - create it via bundle publish
1677
+ log_warn "No PM BotDefinition (AgentTemplate match) found - attempting to create..."
1678
+ log_pass " Agent user: $AGENT_USER_RECHECK"
1679
+ echo " The agent user + PSGs are sufficient for suggestion generation."
1680
+ echo " The schedule-triggered flow runs as the agent user directly."
1681
+
1682
+ # OPTIONAL: Deploy standalone agent for richer agent experience
1683
+ log_info "Deploying standalone Sales Management Agent for enhanced capabilities..."
1684
+
1685
+ # Use shared library function (returns on failure so setup-all can track issues)
1686
+ if ! publish_and_activate_agent "$ORG_ALIAS" "return" "structured"; then
1687
+ track_issue "🔴 BLOCKER" "Agent bundle publish/activate failed" "A Pipeline Management BotDefinition (AgentTemplate match) is REQUIRED"
1688
+ echo " SETUP FAILED: Cannot proceed without an active Sales Agent."
1689
+ exit 1
1690
+ fi
1691
+ else
1692
+ # Neither BotDefinition nor agent user — genuinely failed provisioning
1693
+ log_warn "No agent user or BotDefinition found after provisioning wait"
1694
+ track_issue "🔴 ERROR" "Agent user not provisioned after enablement" "Attempting SOAP toggle to re-provision agent user + PSGs, then publish authoring bundle for BotDefinition"
1695
+ echo ""
1696
+ echo " Pipeline Management provisioning appears incomplete."
1697
+ echo " This is unusual — the agent user is normally created on enablement."
1698
+ echo ""
1699
+
1700
+ # Only attempt fallback deployment if we have NO agent user at all
1701
+ # --- APPROACH 1: SOAP toggle — re-provisions the agent user and PSGs only.
1702
+ # It does NOT create BotDefinition:SalesAgent; that comes from the
1703
+ # authoring-bundle publish in APPROACH 2 below.
1704
+ log_try "Attempting SOAP toggle to re-provision agent user + PSGs (does NOT create BotDefinition)..."
1705
+ # Disable
1706
+ soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>false</met:enableDealAgent></met:metadata>" >/dev/null
1707
+ sleep $SLEEP_TOGGLE_OFF
1708
+ # Re-enable
1709
+ soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>true</met:enableDealAgent><met:enableDealAgentAutoApproveAllTasks>false</met:enableDealAgentAutoApproveAllTasks></met:metadata>" >/dev/null
1710
+ sleep $SLEEP_TOGGLE_ON
1711
+
1712
+ # Verify: check for agent user (primary indicator that the toggle took effect)
1713
+ 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 "")
1714
+ if [[ -n "$AGENT_USER_RETRY" ]]; then
1715
+ log_pass "Agent user + PSGs re-provisioned via SOAP toggle: $AGENT_USER_RETRY"
1716
+ else
1717
+ log_warn "SOAP toggle did not re-provision the agent user"
1718
+ fi
1719
+
1720
+ # --- APPROACH 2: Publish the authoring bundle to create BotDefinition:SalesAgent.
1721
+ # Always attempt this: the toggle above never creates the BotDefinition,
1722
+ # so it must come from the bundle publish regardless of whether the
1723
+ # toggle re-provisioned the agent user.
1724
+ log_try "Publishing authoring bundle to create BotDefinition:SalesAgent..."
1725
+ if publish_and_activate_agent "$ORG_ALIAS" "return" "structured"; then
1726
+ log_pass "BotDefinition:SalesAgent published and activated via authoring bundle"
1727
+ else
1728
+ log_warn "Authoring bundle publish/activate failed"
1729
+ fi
1730
+
1731
+ # Final check
1732
+ 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 "")
1733
+ if [[ -z "$FINAL_AGENT_USER" ]]; then
1734
+ log_warn "Agent provisioning failed via all approaches"
1735
+ echo " BLOCKER: No agent user found. Suggestions cannot generate without it."
1736
+ echo ""
1737
+ echo " Manual resolution:"
1738
+ echo " 1. Setup → Sales → Pipeline Management → disable and re-enable"
1739
+ echo " 2. Wait 5 minutes for provisioning"
1740
+ echo " 3. Re-run: bash verify-all.sh $ORG_ALIAS"
1741
+ # Recovery exhausted: upgrade the provisional ERROR to a terminal BLOCKER so the
1742
+ # final gate reports SETUP INCOMPLETE / exit 1 instead of a false SETUP COMPLETE.
1743
+ 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"
1744
+ fi
1745
+ fi
1746
+ fi
1747
+
1748
+ # ============================================================
1749
+ # Phase 4c: Agent Access (W-23242378)
1750
+ # Once a PM BotDefinition exists, add it to "Agent Access" on a custom permset
1751
+ # linked into SalesManagementUserPsg so human users can launch the agent.
1752
+ # define-agent-access.sh is idempotent and exits gracefully if no active bot.
1753
+ # ============================================================
1754
+ section "Phase 4c: Agent Access"
1755
+
1756
+ if [[ -f "$SCRIPT_DIR/define-agent-access.sh" ]]; then
1757
+ bash "$SCRIPT_DIR/define-agent-access.sh" "$ORG_ALIAS" \
1758
+ || 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)"
1759
+ else
1760
+ log_warn "define-agent-access.sh not found — skipping Agent Access definition"
1761
+ fi
1762
+
1763
+ # ============================================================
1764
+ # Phase 4c.5: Stage Descriptions (BEFORE the StageName prompt)
1765
+ # ============================================================
1766
+ # Stage descriptions are the grounding source the managed StageName template reads;
1767
+ # they MUST exist before Phase 4d deploys/activates and Phase 4e tests that prompt,
1768
+ # otherwise a StageName-selected run would verify a prompt that can generate nothing.
1769
+ # run_stage_descriptions() self-gates on STAGENAME_SELECTED (no-op otherwise) and
1770
+ # sets the script-global STAGE_DESCRIPTIONS_BLOCKED for the Phase 8.5 summary. This
1771
+ # lives in Block A, so on a --from-phase flow (Call 2) run it is skipped entirely —
1772
+ # by then Call 1 has already created the descriptions.
1773
+ run_stage_descriptions
1774
+
1775
+ # ============================================================
1776
+ # Phase 4d: Prompt Templates for Selected Fields
1777
+ # ============================================================
1778
+ # 3-phase setup: create/activate the field-completion prompt templates BEFORE
1779
+ # the flow goes live, so the verification gate (Phase 4e) can test them and the
1780
+ # admin can confirm output before any scheduled run. Delegates to
1781
+ # add-field-suggestion.sh --skip-flow (the flow wiring is owned by Phase 5).
1782
+ # - StageName: picklist, uses the managed template — nothing to deploy.
1783
+ # - NextStep : has a curated OOTB managed prompt. Deploy an OVERRIDE only when
1784
+ # the admin supplied a goal/instruction; otherwise leave the OOTB prompt.
1785
+ # - Custom fields: always deploy a new field-completion template (with the
1786
+ # admin's goal/instruction, or add-field-suggestion.sh's defaults).
1787
+ section "Phase 4d: Prompt Templates for Selected Fields"
1788
+
1789
+ declare -a DEPLOYED_TEMPLATE_FIELDS=()
1790
+ for _f in "${FIELD_ARRAY[@]}"; do
1791
+ if [[ "$_f" == "StageName" ]]; then
1792
+ log_info "StageName: uses the managed picklist template (nothing to deploy)."
1793
+ continue
1794
+ fi
1795
+ _goal="${FIELD_PROMPTS["${_f}_goal"]:-}"
1796
+ _instr="${FIELD_PROMPTS["${_f}_instruction"]:-}"
1797
+ if [[ "$_f" == "NextStep" && -z "$_goal" && -z "$_instr" ]]; then
1798
+ log_info "NextStep: keeping the OOTB managed prompt (no customization requested)."
1799
+ DEPLOYED_TEMPLATE_FIELDS+=("$_f")
1800
+ continue
1801
+ fi
1802
+
1803
+ # Build the add-field-suggestion.sh argument list safely (array, not eval).
1804
+ _afs_args=("$ORG_ALIAS" "$_f" --skip-flow)
1805
+ [[ -n "$_goal" ]] && _afs_args+=(--goal "$_goal")
1806
+ [[ -n "$_instr" ]] && _afs_args+=(--instruction "$_instr")
1807
+
1808
+ log_try "Deploying prompt template for '$_f'..."
1809
+ if bash "$SCRIPT_DIR/add-field-suggestion.sh" "${_afs_args[@]}"; then
1810
+ log_pass "Prompt template ready for '$_f'."
1811
+ DEPLOYED_TEMPLATE_FIELDS+=("$_f")
1812
+ else
1813
+ log_warn "Prompt template deploy failed for '$_f' — suggestions for this field may not generate."
1814
+ track_issue "🟡 WARNING" "Prompt template deploy failed for field '$_f'" "Re-run: bash add-field-suggestion.sh $ORG_ALIAS $_f"
1815
+ fi
1816
+ done
1817
+
1818
+ # --- Prune fields whose template deploy failed, BEFORE the flow is built ---
1819
+ # A field with no active template that is still wired into the flow produces zero
1820
+ # suggestions at runtime — the exact silent failure this 3-phase refactor exists to
1821
+ # prevent. The wired set must be: successfully-deployed fields + StageName (managed
1822
+ # picklist template, nothing to deploy). Rebuild SELECTED_FIELDS/FIELD_ARRAY from
1823
+ # those survivors so Phase 5 wires only fields that will actually generate.
1824
+ _WIRE_FIELDS=("${DEPLOYED_TEMPLATE_FIELDS[@]}")
1825
+ if [[ "$STAGENAME_SELECTED" == "true" ]]; then
1826
+ _WIRE_FIELDS+=("StageName")
1827
+ fi
1828
+ if [[ ${#_WIRE_FIELDS[@]} -lt ${#FIELD_ARRAY[@]} ]]; then
1829
+ # At least one selected field was dropped (template deploy failed).
1830
+ _dropped=()
1831
+ for _sf in "${FIELD_ARRAY[@]}"; do
1832
+ _keep=false
1833
+ for _wf in "${_WIRE_FIELDS[@]}"; do [[ "$_sf" == "$_wf" ]] && { _keep=true; break; }; done
1834
+ [[ "$_keep" == "false" ]] && _dropped+=("$_sf")
1835
+ done
1836
+ log_warn "Dropping field(s) with no active template from the flow: ${_dropped[*]}"
1837
+ fi
1838
+ # Abort if every field fell out — a flow with an empty OpportunityFields collection
1839
+ # would be a no-op, and this preserves the up-front abort-on-zero invariant.
1840
+ if [[ ${#_WIRE_FIELDS[@]} -eq 0 ]]; then
1841
+ log_fail "No field has an active prompt template — cannot build a working flow."
1842
+ echo " Every selected field's template failed to deploy in Phase 4d (see warnings above)."
1843
+ echo " Fix the template deploy(s), then re-run setup."
1844
+ exit 1
1845
+ fi
1846
+ FIELD_ARRAY=("${_WIRE_FIELDS[@]}")
1847
+ SELECTED_FIELDS=$(IFS=,; echo "${_WIRE_FIELDS[*]}")
1848
+
1849
+ # Fields to VERIFY (Phase 4e) — a superset of DEPLOYED_TEMPLATE_FIELDS. StageName
1850
+ # deploys nothing (managed picklist template) so it's absent from the deployed set,
1851
+ # but it still generates a default suggestion the admin should see and get a chance
1852
+ # to override. Same for NextStep left uncustomized (OOTB prompt) — it's already in
1853
+ # the deployed set. So: show a default suggestion for every SELECTED field, even
1854
+ # uncustomized ones ("only when selected" — never pull in unselected fields).
1855
+ declare -a VERIFY_FIELDS=("${DEPLOYED_TEMPLATE_FIELDS[@]}")
1856
+ if [[ "$STAGENAME_SELECTED" == "true" ]]; then
1857
+ VERIFY_FIELDS+=("StageName")
1858
+ fi
1859
+
1860
+ # --- --through-phase prompts: stop here, hand off to the agent tune-loop ---
1861
+ # The templates are deployed and active but the flow is NOT built. The agent now
1862
+ # runs the interactive per-field tune-loop (generate a tailored note → seed → run
1863
+ # /generations → show the suggestion → loop until the user approves), then resumes
1864
+ # with `--from-phase flow`. We STOP BEFORE the built-in Phase 4e gate because the
1865
+ # agent's loop supersedes it (and a piped-stdin run couldn't block on it anyway).
1866
+ if [[ "$RUN_THROUGH_PHASE" == "prompts" ]]; then
1867
+ section "Prompt templates ready — handing off for tuning"
1868
+ log_pass "Deployed/active prompt template field(s): ${DEPLOYED_TEMPLATE_FIELDS[*]:-none}"
1869
+ if [[ "$STAGENAME_SELECTED" == "true" ]]; then
1870
+ log_info "StageName is also selected (managed picklist template; no per-field tuning)."
1871
+ fi
1872
+ echo ""
1873
+ echo " NEXT (agent tune-loop): for each CUSTOMIZED field, seed a tailored note and test:"
1874
+ echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --force \\"
1875
+ echo " --goal '<goal>' --instruction '<instruction>' \\"
1876
+ echo " --verify-with-note --note '<note tailored to the instruction>'"
1877
+ echo ""
1878
+ echo " ALSO show the DEFAULT suggestion for every selected field left UNCUSTOMIZED"
1879
+ echo " (StageName always; NextStep when no goal/instruction was given) so the user"
1880
+ echo " can decide whether to keep or change the default even though they didn't tune it:"
1881
+ echo " bash verify-prompt-generation.sh $ORG_ALIAS <Field> <oppId>"
1882
+ echo " To CHANGE it: NextStep is tunable via add-field-suggestion.sh (above);"
1883
+ echo " StageName's suggestion is shaped by its stage descriptions — re-run with"
1884
+ echo " --create-stage-descriptions (or edit OpptStageDescription) to change it."
1885
+ echo ""
1886
+ echo " Loop until the user approves each suggestion, then build & activate the flow:"
1887
+ echo " bash setup-all.sh $ORG_ALIAS --fields \"$SELECTED_FIELDS\" --from-phase flow"
1888
+ echo ""
1889
+ log_info "Stopping after Phase 4d (--through-phase prompts). Flow NOT yet built."
1890
+ exit 0
1891
+ fi
1892
+
1893
+ # ============================================================
1894
+ # Phase 4e: Prompt Verification Gate (before the flow goes live)
1895
+ # ============================================================
1896
+ # 3-phase setup: the templates from Phase 4d are now active but the flow is NOT
1897
+ # yet deployed/activated. Test each template SYNCHRONOUSLY against a real
1898
+ # opportunity + a seeded note, show the admin exactly what the AI will suggest,
1899
+ # and (interactively) gate flow activation on their approval. This catches a bad
1900
+ # goal/instruction BEFORE the scheduled run ever fires — far cheaper to fix now
1901
+ # than after go-live.
1902
+ #
1903
+ # We verify VERIFY_FIELDS — every SELECTED field that generates a suggestion,
1904
+ # including ones left uncustomized (NextStep on its OOTB prompt, and StageName on
1905
+ # the managed picklist template). Showing those defaults lets the admin override a
1906
+ # default they never explicitly tuned. StageName generates only because Phase 4c.5
1907
+ # already created its grounding stage descriptions.
1908
+ #
1909
+ # Fully skipped when:
1910
+ # - --skip-prompt-verification was passed (explicit opt-out), OR
1911
+ # - no selected field generates a suggestion (VERIFY_FIELDS empty).
1912
+ # In NON_INTERACTIVE runs (incl. agent-driven — a piped stdin forces it) the
1913
+ # generation still RUNS and the suggestions are PRINTED for review; only the human
1914
+ # yes/no gate degrades to informational (the flow proceeds without blocking).
1915
+ section "Phase 4e: Prompt Verification Gate"
1916
+
1917
+ if [[ "$SKIP_PROMPT_VERIFICATION" == "true" ]]; then
1918
+ log_info "Prompt verification skipped (--skip-prompt-verification)."
1919
+ elif [[ ${#VERIFY_FIELDS[@]} -eq 0 ]]; then
1920
+ log_info "No selected field generates a suggestion to verify."
1921
+ else
1922
+ # NOTE ON NON-INTERACTIVE RUNS (incl. agent-driven, since a piped stdin forces
1923
+ # NON_INTERACTIVE): we STILL seed a test opp/note and run /generations below so
1924
+ # the generated suggestions are printed for review. What a non-interactive run
1925
+ # cannot do is BLOCK on a human yes/no — so the approval gate at the end degrades
1926
+ # to informational output and the flow proceeds. An agent relays this output to
1927
+ # the user; a true unattended run (cron/CI) simply logs it.
1928
+ # --- Find an eligible open opportunity (matches the flow's own filter) ---
1929
+ # IsClosed=false AND CloseDate within +90 days, so we don't test against an opp
1930
+ # the scheduled flow would itself skip (which would look like a false negative).
1931
+ VERIFY_OPP_ID=$(sf data query -q "SELECT Id FROM Opportunity WHERE IsClosed=false AND CloseDate>=TODAY AND CloseDate<=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')
1932
+
1933
+ if [[ -z "$VERIFY_OPP_ID" ]]; then
1934
+ log_try "No eligible open opportunity found — creating one for verification."
1935
+ # A bare Name + future CloseDate + open Stage satisfies the flow's eligibility
1936
+ # filter. Don't hardcode 'Prospecting' — orgs with custom Sales Processes/record
1937
+ # types may not have it. Query the first active, open (non-won/lost) stage.
1938
+ _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')
1939
+ [[ -z "$_open_stage" ]] && _open_stage="Prospecting" # last-resort default
1940
+ VERIFY_OPP_ID=$(sf data create record --sobject Opportunity \
1941
+ --values "Name='PM Setup Verification Opp' StageName='${_open_stage}' CloseDate=$(python3 -c 'import datetime; print((datetime.date.today()+datetime.timedelta(days=30)).isoformat())')" \
1942
+ --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty')
1943
+ if [[ -n "$VERIFY_OPP_ID" ]]; then
1944
+ log_pass "Created verification opportunity $VERIFY_OPP_ID"
1945
+ else
1946
+ log_warn "Could not create a verification opportunity; skipping the prompt gate."
1947
+ track_issue "🟡 WARNING" "Prompt verification skipped — no eligible opportunity" "Create an open Opportunity (CloseDate within 90 days) and test with verify-prompt-generation.sh."
1948
+ VERIFY_OPP_ID=""
1949
+ fi
1950
+ else
1951
+ log_pass "Using eligible opportunity $VERIFY_OPP_ID for verification."
1952
+ fi
1953
+
1954
+ if [[ -n "$VERIFY_OPP_ID" ]]; then
1955
+ # --- Seed a ContentNote with sample conversation signal + link to the opp ---
1956
+ # Interpolate relative dates (today / today+9d) so the sample never reads as
1957
+ # stale to the model on a run weeks after this was authored.
1958
+ _note_today=$(python3 -c 'import datetime; print(datetime.date.today().strftime("%B %-d"))')
1959
+ _note_soon=$(python3 -c 'import datetime; print((datetime.date.today()+datetime.timedelta(days=9)).strftime("%B %-d"))')
1960
+ 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."
1961
+ # macOS/BSD `base64` line-wraps at 76 cols (embeds newlines); GNU's `-w0` isn't
1962
+ # portable. `tr -d '\n'` strips wraps on both so the value stays single-line and
1963
+ # doesn't break the --values string below.
1964
+ NOTE_B64=$(printf '%s' "$NOTE_BODY" | base64 | tr -d '\n')
1965
+ log_try "Seeding a ContentNote with sample conversation data."
1966
+ # Note-seeding is best-effort: `|| true` keeps a create failure from tripping
1967
+ # `set -e` so it degrades to the log_warn fallback instead of aborting the gate.
1968
+ 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)
1969
+ if [[ -n "$VERIFY_NOTE_ID" ]]; then
1970
+ # ContentNote Id IS the ContentDocumentId (shared 069 prefix); link directly.
1971
+ # The ContentVersion→LatestVersionId subquery returns null on some orgs.
1972
+ 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
1973
+ log_pass "Seeded note and linked to $VERIFY_OPP_ID"
1974
+ else
1975
+ log_warn "Note created but could not link to the opportunity; generation runs against existing grounding data only."
1976
+ fi
1977
+ else
1978
+ log_warn "Could not seed a note; generation runs against existing grounding data only."
1979
+ fi
1980
+
1981
+ # --- Generate a suggestion per verified field and show it to the admin ---
1982
+ echo ""
1983
+ echo " Testing each prompt against opportunity $VERIFY_OPP_ID:"
1984
+ echo ""
1985
+ _gen_ok=0 # count templates that returned usable output
1986
+ for _vf in "${VERIFY_FIELDS[@]}"; do
1987
+ # Resolve the template name the /generations endpoint expects:
1988
+ # - StageName: the managed picklist template (nothing deployed, but it still
1989
+ # generates a default the admin should see and can choose to override).
1990
+ # - NextStep: the managed template (an override folds into it as a new
1991
+ # version; the OOTB prompt shares the same name), so always query the
1992
+ # managed name.
1993
+ # - Custom field: Recommend<Field, stripped of __c and non-alnum>forOpp,
1994
+ # matching add-field-suggestion.sh's dev-name derivation.
1995
+ if [[ "$_vf" == "StageName" ]]; then
1996
+ _vtmpl="sales_pipe_mgmt__RecommendStageforOpp"
1997
+ elif [[ "$_vf" == "NextStep" ]]; then
1998
+ _vtmpl="sales_pipe_mgmt__RecommendNextStepforOpp"
1999
+ else
2000
+ _vtmpl="Recommend$(printf '%s' "$_vf" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')forOpp"
2001
+ fi
2002
+
2003
+ # Bound at 120s: generation can legitimately take longer than a plain REST
2004
+ # call (CURL_TIMEOUT=30), but must not hang indefinitely and blow the run's
2005
+ # tool timeout. A timeout yields non-zero → the `|| echo '{}'` fallback, so a
2006
+ # stall reads as "no output" and flows into the blocked-field handling below.
2007
+ _gen=$(with_timeout 120 sf api request rest "/services/data/v${API_VERSION}/einstein/prompt-templates/${_vtmpl}/generations" \
2008
+ --method POST --target-org "$ORG_ALIAS" \
2009
+ --body "{\"isPreview\":false,\"inputParams\":{\"valueMap\":{\"Input:Opportunity\":{\"value\":{\"id\":\"${VERIFY_OPP_ID}\"}}}},\"additionalConfig\":{\"numGenerations\":1,\"temperature\":0,\"applicationName\":\"PromptTemplateGenerationsInvocable\"}}" 2>/dev/null || echo '{}')
2010
+ _gentext=$(echo "$_gen" | jq -r '.generations[0].text // empty' 2>/dev/null)
2011
+ if [[ -n "$_gentext" ]]; then
2012
+ # The generation text is itself a JSON blob (FieldName/SuggestedNewValue/Reasoning).
2013
+ _sugg=$(echo "$_gentext" | jq -r '.SuggestedNewValue // empty' 2>/dev/null)
2014
+ _reason=$(echo "$_gentext" | jq -r '.Reasoning // empty' 2>/dev/null)
2015
+ _gen_ok=$((_gen_ok + 1))
2016
+ if [[ -n "$_sugg" ]]; then
2017
+ echo " ── $_vf ($_vtmpl)"
2018
+ echo " Suggested: $_sugg"
2019
+ [[ -n "$_reason" ]] && echo " Reasoning: $_reason"
2020
+ else
2021
+ echo " ── $_vf ($_vtmpl)"
2022
+ echo " Raw output: $_gentext"
2023
+ fi
2024
+ else
2025
+ _generr=$(echo "$_gen" | jq -r '.[0].message // .message // empty' 2>/dev/null | head -1)
2026
+ echo " ── $_vf ($_vtmpl)"
2027
+ log_warn "No suggestion generated${_generr:+ — $_generr}"
2028
+ fi
2029
+ echo ""
2030
+ done
2031
+
2032
+ # --- The gate: admin must approve before the flow is deployed/activated ---
2033
+ # If NOT ONE template produced output, the gate tested nothing — approving
2034
+ # "prompts look good" here would be meaningless. Require an explicit,
2035
+ # differently-worded override so a reviewer who saw only warnings cannot
2036
+ # wave through unvalidated prompts on autopilot.
2037
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
2038
+ # No TTY to block on — the gate degrades to informational output. Report
2039
+ # what was (or wasn't) generated and proceed; the caller (agent/operator)
2040
+ # reviews the printed suggestions and can re-run add-field-suggestion.sh to
2041
+ # adjust a prompt afterward.
2042
+ if [[ "$_gen_ok" -eq 0 ]]; then
2043
+ log_warn "Non-interactive run: NO template produced output — flow will activate on UNVERIFIED prompts."
2044
+ echo " Review the warnings above; adjust with:"
2045
+ echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
2046
+ track_issue "🟡 WARNING" "Prompts unverified (non-interactive, no generation output)" "Review suggestions; re-run add-field-suggestion.sh to adjust, then re-run setup."
2047
+ else
2048
+ log_info "Non-interactive run: suggestions shown above are informational — proceeding to flow activation."
2049
+ echo " If a suggestion looks wrong, adjust its prompt with:"
2050
+ echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
2051
+ fi
2052
+ elif [[ "$_gen_ok" -eq 0 ]]; then
2053
+ log_warn "No template produced any output — the prompts could NOT be validated."
2054
+ echo ""
2055
+ echo " Common causes: templates still activating (retry in a minute), no"
2056
+ echo " grounding data on the opportunity, or a template deploy that failed above."
2057
+ echo " Proceeding now would activate the flow on UNVERIFIED prompts."
2058
+ echo ""
2059
+ if ! confirm "Proceed to build & activate the flow anyway, WITHOUT verified prompt output?"; then
2060
+ log_fail "Verification could not complete and was not overridden. Flow NOT activated."
2061
+ echo ""
2062
+ echo " The prompt templates are deployed and active, but the suggestion flow"
2063
+ echo " has not been built or activated, so no scheduled suggestions will run."
2064
+ echo " Inspect a prompt with:"
2065
+ echo " bash verify-prompt-generation.sh $ORG_ALIAS <Field>"
2066
+ echo " or adjust one with:"
2067
+ echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
2068
+ echo " Then re-run this setup to complete flow activation."
2069
+ exit 1
2070
+ fi
2071
+ log_warn "Proceeding to flow activation on UNVERIFIED prompts (explicit override)."
2072
+ elif ! confirm "Prompts look good — proceed to build & activate the flow?"; then
2073
+ log_fail "Verification declined. Flow NOT activated."
2074
+ echo ""
2075
+ echo " The prompt templates are deployed and active, but the suggestion flow"
2076
+ echo " has not been built or activated, so no scheduled suggestions will run."
2077
+ echo " To adjust a prompt, edit it with:"
2078
+ echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
2079
+ echo " Then re-run this setup to complete flow activation."
2080
+ exit 1
2081
+ else
2082
+ log_pass "Verification approved — proceeding to flow activation."
2083
+ fi
2084
+ fi
2085
+ fi
2086
+
2087
+ fi # END BLOCK A (Phase 1.5–4e) — skipped entirely on a --from-phase flow run.
2088
+
2089
+ # ============================================================
2090
+ # BLOCK B — Flow, Agent wiring & Verification (Phase 5 → 8)
2091
+ # ============================================================
2092
+ # Builds the suggestion flow from the approved field set, activates it, wires the
2093
+ # agent/PSG, and runs final verification. Self-contained: everything it needs it
2094
+ # re-derives from the org (bot count, PSG id/state) or from the context vars set in
2095
+ # Phase 0/1/1.7 — so it runs correctly both in a full end-to-end pass and as the
2096
+ # second half of a split (--from-phase flow) run.
2097
+
2098
+ # --- Split-path re-prune: fields must have an ACTIVE prompt template ---
2099
+ # On a full run, Block A already pruned SELECTED_FIELDS/FIELD_ARRAY to fields with
2100
+ # a successfully-deployed active template (lines ~1635-1664). On a --from-phase flow
2101
+ # run Block A is skipped, so SELECTED_FIELDS is still the RAW --fields the agent
2102
+ # passed. Wiring a field with no active template into the flow yields zero
2103
+ # suggestions at runtime — the exact silent failure this refactor prevents. So
2104
+ # re-derive the survivor set here from the org (read-only), mirroring Block A's
2105
+ # invariant: keep StageName unconditionally (managed picklist, nothing to deploy),
2106
+ # keep any other field whose GenAiPromptTemplate is active.
2107
+ if [[ "$RUN_FROM_PHASE" == "flow" ]]; then
2108
+ _ACTIVE_TEMPLATES=$(sf api request rest "/services/data/v${API_VERSION}/einstein/prompt-templates" --target-org "$ORG_ALIAS" 2>/dev/null \
2109
+ | jq -r '.promptRecords[]? | select(.fields.IsActive.value==true or .fields.IsActive.value=="true") | .fields.DeveloperName.value' 2>/dev/null || echo "")
2110
+ _WIRE_FIELDS=()
2111
+ _dropped=()
2112
+ for _sf in "${FIELD_ARRAY[@]}"; do
2113
+ if [[ "$_sf" == "StageName" ]]; then
2114
+ _WIRE_FIELDS+=("StageName")
2115
+ continue
2116
+ fi
2117
+ # Expected dev name: Recommend<Field-without-__c-and-non-alnum>forOpp. Match with
2118
+ # or without the sales_pipe_mgmt__ namespace prefix (NextStep's managed template
2119
+ # is namespaced; standalone custom-field templates are not).
2120
+ _clean=$(echo "$_sf" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')
2121
+ _expect="Recommend${_clean}forOpp"
2122
+ if echo "$_ACTIVE_TEMPLATES" | grep -qiE "(^|__)${_expect}\$"; then
2123
+ _WIRE_FIELDS+=("$_sf")
2124
+ else
2125
+ _dropped+=("$_sf")
2126
+ fi
2127
+ done
2128
+ if [[ ${#_dropped[@]} -gt 0 ]]; then
2129
+ log_warn "Dropping field(s) with no active prompt template from the flow: ${_dropped[*]}"
2130
+ echo " These templates were not active on the org. Re-run the tune-loop"
2131
+ echo " (add-field-suggestion.sh ... --force) for each, then re-run --from-phase flow."
2132
+ fi
2133
+ if [[ ${#_WIRE_FIELDS[@]} -eq 0 ]]; then
2134
+ log_fail "No selected field has an active prompt template — cannot build a working flow."
2135
+ echo " Deploy/activate at least one field's template (Call 1 + tune-loop), then re-run --from-phase flow."
2136
+ exit 1
2137
+ fi
2138
+ FIELD_ARRAY=("${_WIRE_FIELDS[@]}")
2139
+ SELECTED_FIELDS=$(IFS=,; echo "${_WIRE_FIELDS[*]}")
2140
+ log_info "Flow will wire ${#FIELD_ARRAY[@]} field(s) with active templates: $SELECTED_FIELDS"
2141
+ fi
2142
+
2143
+ # ============================================================
2144
+ # Phase 5: Schedule-Triggered Flow (with deploy + activation)
2145
+ # ============================================================
2146
+ section "Phase 5: Schedule-Triggered Flow"
2147
+
2148
+ # Detection: check by SourceTemplateId, naming convention, and exact API name
2149
+ FLOW_NAME=""
2150
+ for QUERY in \
2151
+ "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false" \
2152
+ "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=false" \
2153
+ "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false"; do
2154
+
2155
+ 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 "")
2156
+ [[ -n "$FLOW_NAME" ]] && break
2157
+ done
2158
+
2159
+ if [[ -n "$FLOW_NAME" ]]; then
2160
+ log_pass "Flow found: $FLOW_NAME"
2161
+ else
2162
+ log_info "Flow not found. Deploying from pre-retrieved template..."
2163
+
2164
+ TEMPLATE_FILE="$SCRIPT_DIR/../assets/pipeline_management_flow.flow-meta.xml"
2165
+
2166
+ if [[ -f "$TEMPLATE_FILE" ]]; then
2167
+ ensure_sfdx_project
2168
+ mkdir -p force-app/main/default/flows
2169
+ FLOW_FILE="force-app/main/default/flows/Process_Field_Update_Suggestions.flow-meta.xml"
2170
+
2171
+ # Greenfield: BUILD the flow with ONLY the up-front-selected fields (no
2172
+ # deploy-then-strip). build_field_collection reads the base template and
2173
+ # writes FLOW_FILE with exactly SELECTED_FIELDS wired into
2174
+ # AddOppFieldsToCollection, emitting a distinct nonzero exit on each failure
2175
+ # (2 empty set / 3 parse-fail / 4 assignment-missing / 5 write-fail).
2176
+ # We do NOT fall back to copying the base template: that verbatim copy wires
2177
+ # BOTH OOTB fields, which would ship StageName the admin excluded (and with no
2178
+ # supporting stage descriptions). The selected set is already validated
2179
+ # non-empty (Phase 1.7) and pruned to fields with active templates, so a build
2180
+ # failure here is a genuine environment problem — abort loudly instead.
2181
+ if build_field_collection "$SELECTED_FIELDS" "$TEMPLATE_FILE" "$FLOW_FILE"; then
2182
+ log_pass "Flow built with ${#FIELD_ARRAY[@]} selected field(s): $SELECTED_FIELDS"
2183
+ else
2184
+ _bfc_rc=$?
2185
+ log_fail "Could not build the flow from selected fields (build_field_collection rc=$_bfc_rc)."
2186
+ case $_bfc_rc in
2187
+ 2) echo " Empty field set after pruning — nothing to wire." ;;
2188
+ 3) echo " Base flow template did not parse as XML: $TEMPLATE_FILE" ;;
2189
+ 4) echo " AddOppFieldsToCollection assignment not found in the base template." ;;
2190
+ 5) echo " Could not write the built flow to $FLOW_FILE (permissions/disk)." ;;
2191
+ *) echo " Unexpected failure; check that python3 is on PATH." ;;
2192
+ esac
2193
+ echo " Flow NOT deployed. Fix the above and re-run setup."
2194
+ exit 1
2195
+ fi
2196
+
2197
+ # Update startDate to today
2198
+ TODAY=$(date +%Y-%m-%d)
2199
+ sed_inplace "s|<startDate>[^<]*</startDate>|<startDate>${TODAY}</startDate>|g" "$FLOW_FILE"
2200
+
2201
+ # Sync the flow's SalesManagementAgentTemplate constant with the actual
2202
+ # AgentTemplate of the bot deployed to this org. The template's default
2203
+ # value is the legacy SalesMgmt__SalesAgent, but our authoring bundle
2204
+ # publishes the NGA variant (SalesMgmt__NGASalesAgent). The flow's
2205
+ # BotDefinition lookup matches on this constant, so a mismatch means
2206
+ # the action silently fails to find the agent at runtime.
2207
+ set +e # Allow pm_bot_agent_template to return non-zero without aborting
2208
+ DETECTED_TEMPLATE=$(pm_bot_agent_template "$ORG_ALIAS")
2209
+ DETECT_RC=$?
2210
+ set -e
2211
+ case $DETECT_RC in
2212
+ 0)
2213
+ PM_BOT_TOTAL=$(pm_bot_count "$ORG_ALIAS")
2214
+ if [[ "${PM_BOT_TOTAL:-0}" -gt 1 ]]; then
2215
+ 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."
2216
+ fi
2217
+ sed_inplace "s|<stringValue>SalesMgmt__SalesAgent</stringValue>|<stringValue>${DETECTED_TEMPLATE}</stringValue>|g" "$FLOW_FILE"
2218
+ log_info "Flow template constant set to: $DETECTED_TEMPLATE"
2219
+ ;;
2220
+ 1)
2221
+ 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."
2222
+ exit 1
2223
+ ;;
2224
+ *)
2225
+ log_fail "Could not query BotDefinition for AgentTemplate (sf data query failed — auth, network, or API error). Re-run after resolving the underlying issue."
2226
+ exit 1
2227
+ ;;
2228
+ esac
2229
+
2230
+ # Field wiring for the greenfield flow is already done: build_field_collection
2231
+ # (above) wrote FLOW_FILE with exactly the up-front-selected fields. There is
2232
+ # no deploy-then-strip here. strip_flow_field / add_flow_field (from
2233
+ # shared/flow-builder.sh) are reserved for the REPAIR path below, which reconciles
2234
+ # an already-deployed flow's wired fields to the selected set.
2235
+
2236
+ # --- APPROACH 1: Deploy as source ---
2237
+ log_try "Deploying flow via source deploy..."
2238
+ DEPLOY_RESULT=$(sf project deploy start --source-dir "$FLOW_FILE" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
2239
+
2240
+ if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
2241
+ FLOW_NAME="Process_Field_Update_Suggestions"
2242
+ log_pass "Flow deployed: $FLOW_NAME"
2243
+ else
2244
+ DEPLOY_ERR=$(safe_jq "$DEPLOY_RESULT" '.result.details.componentFailures[0].problem // .message // "unknown"' "unknown")
2245
+ echo " Source deploy error: $DEPLOY_ERR"
2246
+
2247
+ # --- APPROACH 2: Deploy as metadata ---
2248
+ log_try "Retrying as metadata deploy..."
2249
+ DEPLOY_RESULT=$(sf project deploy start --metadata "Flow:Process_Field_Update_Suggestions" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
2250
+ if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
2251
+ FLOW_NAME="Process_Field_Update_Suggestions"
2252
+ log_pass "Flow deployed: $FLOW_NAME (metadata approach)"
2253
+ else
2254
+ log_warn "Both deploy approaches failed."
2255
+ fi
2256
+ fi
2257
+ else
2258
+ log_warn "Template file not found at: $TEMPLATE_FILE"
2259
+ fi
2260
+
2261
+ # --- If still no flow, UI guidance ---
2262
+ if [[ -z "$FLOW_NAME" ]]; then
2263
+ log_warn "Flow requires manual creation via Setup UI."
2264
+ echo ""
2265
+ echo " Steps:"
2266
+ echo " 1. Open: ${INSTANCE_URL}/lightning/setup/Flows/home"
2267
+ echo " 2. Find 'OppSuggGenSchFlow' (template, sales_pipe_mgmt namespace)"
2268
+ echo " 3. Click 'Save As' → Label: 'Process Field Update Suggestions'"
2269
+ echo " 4. Click 'Activate'"
2270
+ echo ""
2271
+
2272
+ # Non-interactive: skip (don't block on user input in agent context)
2273
+ if [[ -t 0 ]]; then
2274
+ while true; do
2275
+ read -rp " Press Enter after completing (or 'skip' to continue): " RESPONSE
2276
+ if [[ "$RESPONSE" == "skip" ]]; then break; fi
2277
+ 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 "")
2278
+ [[ -n "$FLOW_NAME" ]] && { log_pass "Flow detected: $FLOW_NAME"; break; }
2279
+ log_warn "Flow not detected yet."
2280
+ done
2281
+ fi
2282
+ fi
2283
+ fi
2284
+
2285
+ # --- Flow Activation + Field Reconciliation (REPAIR path for an existing flow) ---
2286
+ # When the flow already exists we do NOT rebuild it from the base template (that
2287
+ # would discard the org-specific SalesManagementAgentTemplate constant the
2288
+ # deployed flow carries). Instead we RECONCILE the flow's wired fields to the
2289
+ # up-front selected set — the "fix method": strip fields no longer selected,
2290
+ # add newly-selected ones (via strip_flow_field / add_flow_field). We then set
2291
+ # it Active and redeploy. This runs whether or not the flow is currently active,
2292
+ # because the field set may have changed even on an active flow.
2293
+ if [[ -n "$FLOW_NAME" ]]; then
2294
+ 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")
2295
+
2296
+ log_try "Reconciling existing flow to selected fields + activating..."
2297
+
2298
+ # Retrieve → reconcile fields → set Active → reconcile constant → deploy
2299
+ ensure_sfdx_project
2300
+ mkdir -p force-app/main/default/flows
2301
+ sf project retrieve start --metadata "Flow:${FLOW_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null || true
2302
+
2303
+ FLOW_FILE="force-app/main/default/flows/${FLOW_NAME}.flow-meta.xml"
2304
+ if [[ -f "$FLOW_FILE" ]]; then
2305
+ # --- Field reconciliation: make the wired set == SELECTED_FIELDS ---
2306
+ # Strip any wired field not in the selected set; add any selected field
2307
+ # not yet wired. Both helpers are idempotent and namespace-safe (python3).
2308
+ # The AddOppFieldsToCollection assignment must never end up empty — the
2309
+ # selected set is guaranteed non-empty (validated in Phase 1.7), so as long
2310
+ # as we add-before-we-finish the assignment stays valid.
2311
+ FIELDS_CHANGED=false
2312
+ WIRED_FIELDS="$(flow_wired_fields "$FLOW_FILE" 2>/dev/null || echo "")"
2313
+ # Normalized ",a,b,c," form computed once for exact-comma membership tests
2314
+ # (mirrors the ,$SELECTED_FIELDS, idiom on the strip side).
2315
+ WIRED_CSV=",$(printf '%s' "$WIRED_FIELDS" | tr '\n' ',' | sed 's/,,*/,/g'),"
2316
+ if [[ -n "$WIRED_FIELDS" ]]; then
2317
+ while IFS= read -r _wired; do
2318
+ [[ -z "$_wired" ]] && continue
2319
+ if [[ ",$SELECTED_FIELDS," != *",$_wired,"* ]]; then
2320
+ # strip: rc 0 = removed, 2 = not present (benign), other = real error.
2321
+ _rc=0; strip_flow_field "$FLOW_FILE" "$_wired" || _rc=$?
2322
+ case $_rc in
2323
+ 0) FIELDS_CHANGED=true; log_info "Unwired field no longer selected: $_wired" ;;
2324
+ 2) : ;; # not present — nothing to do
2325
+ *) log_warn "Could not unwire '$_wired' (rc=$_rc) — it may remain in the flow."
2326
+ 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." ;;
2327
+ esac
2328
+ fi
2329
+ done <<< "$WIRED_FIELDS"
2330
+ fi
2331
+ for _sel in "${FIELD_ARRAY[@]}"; do
2332
+ if [[ "$WIRED_CSV" != *",$_sel,"* ]]; then
2333
+ # add: rc 0 = added, 2 = already wired (benign), other = real error.
2334
+ _rc=0; add_flow_field "$FLOW_FILE" "$_sel" || _rc=$?
2335
+ case $_rc in
2336
+ 0) FIELDS_CHANGED=true; log_info "Wired newly-selected field: $_sel" ;;
2337
+ 2) : ;; # already wired — nothing to do
2338
+ *) log_warn "Could not wire '$_sel' (rc=$_rc) — it may not generate suggestions."
2339
+ 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" ;;
2340
+ esac
2341
+ fi
2342
+ done
2343
+ if [[ "$FIELDS_CHANGED" == "true" ]]; then
2344
+ log_pass "Flow fields reconciled to: $SELECTED_FIELDS"
2345
+ else
2346
+ log_info "Flow fields already match selected set: $SELECTED_FIELDS"
2347
+ fi
2348
+
2349
+ # Short-circuit only when nothing needs deploying: already active AND no
2350
+ # field change. Otherwise fall through to set-Active + redeploy.
2351
+ if [[ "$FLOW_ACTIVE" == "true" && "$FIELDS_CHANGED" == "false" ]]; then
2352
+ log_pass "Flow is ACTIVE (no field changes needed)"
2353
+ else
2354
+ sed_inplace 's|<status>Draft</status>|<status>Active</status>|g' "$FLOW_FILE"
2355
+ sed_inplace 's|<status>Obsolete</status>|<status>Active</status>|g' "$FLOW_FILE"
2356
+
2357
+ # Reconcile the flow's SalesManagementAgentTemplate constant with the
2358
+ # on-org bot. The retrieved flow carries whatever template was wired
2359
+ # in at last deploy; if the org has since been upgraded (legacy ->
2360
+ # NGA) the constant is stale and the BotDefinition lookup misses.
2361
+ # The second sed handles the symmetric NGA -> legacy downgrade case.
2362
+ #
2363
+ # In this reconcile path we warn-and-skip rather than exit on failure:
2364
+ # the flow already exists on the org, so the worst case is that we
2365
+ # leave the existing constant in place and the deploy below may fail
2366
+ # loudly with a Salesforce-specific error — preferable to aborting
2367
+ # the whole setup script just because we couldn't sync the constant.
2368
+ set +e # Allow pm_bot_agent_template to return non-zero without aborting
2369
+ DETECTED_TEMPLATE=$(pm_bot_agent_template "$ORG_ALIAS")
2370
+ DETECT_RC=$?
2371
+ set -e
2372
+ case $DETECT_RC in
2373
+ 0)
2374
+ PM_BOT_TOTAL=$(pm_bot_count "$ORG_ALIAS")
2375
+ if [[ "${PM_BOT_TOTAL:-0}" -gt 1 ]]; then
2376
+ 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."
2377
+ fi
2378
+ sed_inplace "s|<stringValue>SalesMgmt__SalesAgent</stringValue>|<stringValue>${DETECTED_TEMPLATE}</stringValue>|g" "$FLOW_FILE"
2379
+ sed_inplace "s|<stringValue>SalesMgmt__NGASalesAgent</stringValue>|<stringValue>${DETECTED_TEMPLATE}</stringValue>|g" "$FLOW_FILE"
2380
+ log_info "Flow template constant reconciled to: $DETECTED_TEMPLATE"
2381
+ ;;
2382
+ 1)
2383
+ 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."
2384
+ ;;
2385
+ *)
2386
+ log_warn "Could not query BotDefinition for AgentTemplate (sf data query failed). Skipping template-constant reconciliation; activation will proceed with the existing constant."
2387
+ ;;
2388
+ esac
2389
+
2390
+ 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
2391
+ log_pass "Flow activated"
2392
+ else
2393
+ log_warn "Flow activation failed. Activate manually: Setup → Flows → ${FLOW_NAME} → Activate"
2394
+ fi
2395
+ fi # end: active+unchanged short-circuit vs set-Active+redeploy
2396
+ else
2397
+ log_warn "Could not retrieve flow. Activate manually in Setup → Flows."
2398
+ fi
2399
+ fi
2400
+
2401
+ # ============================================================
2402
+ # Phase 6: Stage Descriptions — RELOCATED
2403
+ # ============================================================
2404
+ # Stage-description creation now runs earlier, in Block A as Phase 4c.5 (before the
2405
+ # StageName prompt is deployed/activated/tested), via run_stage_descriptions() in
2406
+ # shared/stage-descriptions.sh. It is a prerequisite for the managed StageName template
2407
+ # to generate anything, so it must precede Phase 4d/4e. Nothing to do here.
2408
+
2409
+ # ============================================================
2410
+ # Phase 7: User Permissions
2411
+ # ============================================================
2412
+ section "Phase 7: User Permissions"
2413
+
2414
+ CURRENT_USER=$(sf org display --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.username // empty' 2>/dev/null || echo "")
2415
+ CURRENT_USER=$(sanitize_display "$CURRENT_USER" 80)
2416
+ # Sanitize for SOQL interpolation (prevent injection)
2417
+ CURRENT_USER_SAFE=$(sanitize_soql_value "$CURRENT_USER")
2418
+
2419
+ PSG_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 "")
2420
+
2421
+ if [[ -z "$PSG_ID" ]]; then
2422
+ log_warn "SalesManagementUserPsg not found (provisioning may still be in progress)"
2423
+ else
2424
+ # The PSG recalc is triggered by provisioning and converges asynchronously; we do
2425
+ # NOT block on it. Report its current Status for context, then attempt assignment.
2426
+ # #5: if the PSG has not finished recalculating, the insert is rejected with
2427
+ # INVALID_CROSS_REFERENCE_KEY — assign_user_psg() classifies that explicitly and
2428
+ # tells the operator to re-run, instead of the old `|| log_pass` that masked the
2429
+ # rejection as a bogus "Already assigned".
2430
+ PSG_STATE=$(psg_status "$ORG_ALIAS" "SalesManagementUserPsg")
2431
+ if [[ "$PSG_STATE" != "Updated" ]]; then
2432
+ log_info "SalesManagementUserPsg Status: $PSG_STATE — recalculation may still be in progress; assignment may need a re-run."
2433
+ fi
2434
+
2435
+ # assign_user_psg <user-id> <display-name> — insert PermissionSetAssignment and
2436
+ # classify the outcome. Never masks a real rejection as success.
2437
+ assign_user_psg() {
2438
+ local user_id="$1" display="$2" res msg
2439
+ res=$(sf data create record --sobject PermissionSetAssignment \
2440
+ --values "AssigneeId='${user_id}' PermissionSetGroupId='${PSG_ID}'" \
2441
+ --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
2442
+ if echo "$res" | jq -e '.result.id // empty' >/dev/null 2>&1; then
2443
+ log_pass "Assigned SalesManagementUserPsg to: $display"; return 0
2444
+ fi
2445
+ msg=$(echo "$res" | jq -r '.message // .result.message // ""' 2>/dev/null || echo "")
2446
+ if echo "$msg" | grep -qi "DUPLICATE_VALUE"; then
2447
+ log_info "Already assigned: $display"
2448
+ elif echo "$msg" | grep -qi "INVALID_CROSS_REFERENCE_KEY"; then
2449
+ log_warn "Assignment for $display rejected (INVALID_CROSS_REFERENCE_KEY) — SalesManagementUserPsg has not finished recalculating."
2450
+ echo " Re-run: bash setup-all.sh $ORG_ALIAS once the PSG Status reads 'Updated'."
2451
+ track_issue "🟡 WARNING" "User PSG assignment deferred (PSG recalculating)" "Re-run setup-all.sh once SalesManagementUserPsg Status is 'Updated' to finish assigning $display."
2452
+ else
2453
+ log_warn "Failed to assign $display: ${msg:-unknown}"
2454
+ fi
2455
+ return 1
2456
+ }
2457
+
2458
+ # assign_user_list <comma-separated-usernames> — resolve each username to an Id and
2459
+ # assign the PSG. Only ever processes the EXPLICIT list handed to it; there is no
2460
+ # code path that queries all/standard users and bulk-grants. Adding users is always
2461
+ # deliberate: the running user, an operator-typed interactive list, or --users.
2462
+ assign_user_list() {
2463
+ local raw_list="$1" raw_user username username_safe user_id
2464
+ IFS=',' read -ra _USERS <<< "$raw_list"
2465
+ for raw_user in "${_USERS[@]}"; do
2466
+ username=$(echo "$raw_user" | xargs)
2467
+ [[ -z "$username" ]] && continue
2468
+ username_safe=$(sanitize_soql_value "$username")
2469
+ 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 "")
2470
+ if [[ -z "$user_id" ]]; then
2471
+ log_fail "User not found: $username"
2472
+ else
2473
+ assign_user_psg "$user_id" "$username" || true
2474
+ fi
2475
+ done
2476
+ }
2477
+
2478
+ # Assign current user
2479
+ if [[ -n "$CURRENT_USER" ]]; then
2480
+ 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 "")
2481
+ if [[ -n "$USER_ID" ]]; then
2482
+ assign_user_psg "$USER_ID" "$CURRENT_USER" || true
2483
+ fi
2484
+ fi
2485
+
2486
+ # Explicit --users list (works in both interactive and non-interactive runs).
2487
+ if [[ -n "$USERS_LIST" ]]; then
2488
+ log_info "Assigning SalesManagementUserPsg to --users: $USERS_LIST"
2489
+ assign_user_list "$USERS_LIST"
2490
+ fi
2491
+
2492
+ # Interactive: ask for more users. Non-interactive runs NEVER prompt and NEVER
2493
+ # bulk-assign — the only additional grantees come from the explicit --users flag.
2494
+ if [[ "$NON_INTERACTIVE" != "true" ]]; then
2495
+ echo ""
2496
+ echo " Assign SalesManagementUserPsg to additional users?"
2497
+ read -rp " Usernames (comma-separated, or Enter to skip): " EXTRA_USERS
2498
+
2499
+ if [[ -n "$EXTRA_USERS" ]]; then
2500
+ assign_user_list "$EXTRA_USERS"
2501
+ fi
2502
+ fi
2503
+ fi
2504
+
2505
+ # ============================================================
2506
+ # Phase 8: Final Verification
2507
+ # ============================================================
2508
+ section "Phase 8: Final Verification"
2509
+
2510
+ if [[ -f "$SCRIPT_DIR/verify-all.sh" ]]; then
2511
+ # verify-all.sh is a DIAGNOSTIC: it exits nonzero when it reports any FAIL
2512
+ # (e.g. stage descriptions absent on a NextStep-only run). That is informational
2513
+ # status, NOT a setup failure — swallow it with `|| true` so `set -e` does not
2514
+ # abort here and skip the Configuration Summary table below. Capture the exit so
2515
+ # the summary can note that verification flagged issues to review.
2516
+ VERIFY_EXIT=0
2517
+ bash "$SCRIPT_DIR/verify-all.sh" "$ORG_ALIAS" || VERIFY_EXIT=$?
2518
+ if [[ "$VERIFY_EXIT" -ne 0 ]]; then
2519
+ log_warn "verify-all.sh reported issues (exit $VERIFY_EXIT) — review its output above; setup summary follows."
2520
+ fi
2521
+ else
2522
+ log_warn "verify-all.sh not found. Run manually to confirm setup."
2523
+ fi
2524
+
2525
+ # ============================================================
2526
+ # Phase 8.5: Stage Descriptions Summary Table
2527
+ # ============================================================
2528
+ # Display all persisted OpptStageDescription records after setup
2529
+
2530
+ if [[ "${STAGE_DESCRIPTIONS_BLOCKED:-}" != "true" ]]; then
2531
+ # Query all stage descriptions (global-per-stage; no OpportunityRecordTypeId column)
2532
+ 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}')
2533
+
2534
+ SUMMARY_COUNT=$(echo "$SUMMARY_QUERY" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo "0")
2535
+ SUMMARY_COUNT="${SUMMARY_COUNT:-0}"
2536
+
2537
+ if [[ "$SUMMARY_COUNT" -gt 0 ]]; then
2538
+ echo ""
2539
+ section "Stage Descriptions Summary"
2540
+ echo ""
2541
+ echo "Persisted OpptStageDescription records: $SUMMARY_COUNT"
2542
+ echo ""
2543
+
2544
+ # Determine methodology from descriptions (detect MEDDIC/BANT/SPICED keywords)
2545
+ FIRST_DESC=$(echo "$SUMMARY_QUERY" | jq -r '.result.records[0].Description // ""' 2>/dev/null)
2546
+ METHODOLOGY="Unknown"
2547
+ if echo "$FIRST_DESC" | grep -qi "MEDDIC\|Metrics.*Economic Buyer"; then
2548
+ METHODOLOGY="MEDDIC"
2549
+ elif echo "$FIRST_DESC" | grep -qi "BANT\|Budget.*Authority"; then
2550
+ METHODOLOGY="BANT"
2551
+ elif echo "$FIRST_DESC" | grep -qi "SPICED\|Situation.*Pain.*Impact"; then
2552
+ METHODOLOGY="SPICED"
2553
+ fi
2554
+
2555
+ # Print table header — descriptions are global-per-stage, no record-type column
2556
+ printf "┌──────────────────────────┬──────────────────────────────────────────────────────────────┬────────────┐\n"
2557
+ printf "│ %-24s │ %-60s │ %-10s │\n" "Stage Name" "Description (first 60 chars)" "Methodology"
2558
+ printf "├──────────────────────────┼──────────────────────────────────────────────────────────────┼────────────┤\n"
2559
+
2560
+ # Print table rows
2561
+ while IFS= read -r DESC_JSON; do
2562
+ [[ -z "$DESC_JSON" ]] && continue
2563
+
2564
+ STAGE_NAME=$(echo "$DESC_JSON" | jq -r '.MasterLabel // .OpportunityStageApiName // "N/A"' 2>/dev/null)
2565
+ STAGE_DESC=$(echo "$DESC_JSON" | jq -r '.Description // "N/A"' 2>/dev/null)
2566
+
2567
+ # Truncate description to 60 characters
2568
+ if [[ ${#STAGE_DESC} -gt 60 ]]; then
2569
+ STAGE_DESC_TRUNC="${STAGE_DESC:0:57}..."
2570
+ else
2571
+ STAGE_DESC_TRUNC="$STAGE_DESC"
2572
+ fi
2573
+
2574
+ # Print row
2575
+ printf "│ %-24s │ %-60s │ %-10s │\n" "$STAGE_NAME" "$STAGE_DESC_TRUNC" "$METHODOLOGY"
2576
+
2577
+ done < <(echo "$SUMMARY_QUERY" | jq -c '.result.records[]?' 2>/dev/null)
2578
+
2579
+ # Print table footer
2580
+ printf "└──────────────────────────┴──────────────────────────────────────────────────────────────┴────────────┘\n"
2581
+ echo ""
2582
+ echo "Note: Full descriptions are stored in Salesforce. This table shows first 60 characters only."
2583
+ echo ""
2584
+ fi
2585
+ fi
2586
+
2587
+ echo ""
2588
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2589
+ echo " PIPELINE MANAGEMENT SETUP - CONFIGURATION SUMMARY "
2590
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2591
+ echo ""
2592
+
2593
+ # Query current org configuration state
2594
+ log_info "Querying final org configuration state..."
2595
+ echo ""
2596
+
2597
+ # Build configuration summary
2598
+ printf "┌──────────────────────────────────────────┬─────────────────────┬─────────────────────┬────────────────┐\n"
2599
+ printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Configuration Item" "Before Setup" "After Setup" "Status"
2600
+ printf "├──────────────────────────────────────────┼─────────────────────┼─────────────────────┼────────────────┤\n"
2601
+
2602
+ # Configuration Summary rows compare BEFORE_* (snapshot captured pre-Phase-3)
2603
+ # vs. AFTER_* (live post-run query). Emit UNCHANGED when they match so
2604
+ # idempotent reruns don't misreport as CHANGED. See W-23277501.
2605
+
2606
+ # Cache one SOAP read so both settings come from the same document
2607
+ SOAP_SETTINGS_AFTER=$(soap_read "SalesDealAgentSettings" "SalesDealAgent" 2>/dev/null || echo "")
2608
+
2609
+ # --- Pipeline Management ---
2610
+ AFTER_PM=$(soap_extract_bool "$SOAP_SETTINGS_AFTER" "enableDealAgent" 2>/dev/null || echo "false")
2611
+ PM_BEFORE_LABEL=$([ "$BEFORE_PM" == "true" ] && echo "✅ Enabled" || echo "❌ Not Enabled")
2612
+ PM_AFTER_LABEL=$([ "$AFTER_PM" == "true" ] && echo "✅ Enabled" || echo "❌ Not Enabled")
2613
+ if [[ "$BEFORE_PM" == "$AFTER_PM" ]]; then
2614
+ PM_STATUS_COL=$([ "$AFTER_PM" == "true" ] && echo "✅ UNCHANGED" || echo "⚠️ NOT ENABLED")
2615
+ else
2616
+ PM_STATUS_COL=$([ "$AFTER_PM" == "true" ] && echo "✅ CHANGED" || echo "⚠️ REVERTED")
2617
+ fi
2618
+ printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Pipeline Management" "$PM_BEFORE_LABEL" "$PM_AFTER_LABEL" "$PM_STATUS_COL"
2619
+
2620
+ # --- Autonomous Field Updates ---
2621
+ AFTER_AUTO=$(soap_extract_bool "$SOAP_SETTINGS_AFTER" "enableDealAgentAutoApproveAllTasks" 2>/dev/null || echo "false")
2622
+ AUTO_BEFORE_LABEL=$([ "$BEFORE_AUTO" == "true" ] && echo "✅ Enabled" || echo "❌ Disabled")
2623
+ AUTO_AFTER_LABEL=$([ "$AFTER_AUTO" == "true" ] && echo "✅ Enabled" || echo "❌ Disabled")
2624
+ if [[ "$BEFORE_AUTO" == "$AFTER_AUTO" ]]; then
2625
+ AUTO_STATUS_COL=$([ "$AFTER_AUTO" == "true" ] && echo "✅ UNCHANGED" || echo "⚠️ NOT ENABLED")
2626
+ else
2627
+ AUTO_STATUS_COL=$([ "$AFTER_AUTO" == "true" ] && echo "✅ CHANGED" || echo "⚠️ REVERTED")
2628
+ fi
2629
+ printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Autonomous Field Updates" "$AUTO_BEFORE_LABEL" "$AUTO_AFTER_LABEL" "$AUTO_STATUS_COL"
2630
+
2631
+ # --- Agent User ---
2632
+ AFTER_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")
2633
+ AFTER_AGENT_USER=$(sanitize_display "$AFTER_AGENT_USER" 60)
2634
+ AGENT_BEFORE_LABEL=$([ "$BEFORE_AGENT_USER" != "Not Created" ] && echo "✅ Created" || echo "Not Created")
2635
+ AGENT_AFTER_LABEL=$([ "$AFTER_AGENT_USER" != "Not Created" ] && echo "✅ Created" || echo "❌ Not Created")
2636
+ if [[ "$BEFORE_AGENT_USER" == "$AFTER_AGENT_USER" ]]; then
2637
+ AGENT_STATUS_COL=$([ "$AFTER_AGENT_USER" != "Not Created" ] && echo "✅ UNCHANGED" || echo "⚠️ NOT CREATED")
2638
+ else
2639
+ AGENT_STATUS_COL=$([ "$AFTER_AGENT_USER" != "Not Created" ] && echo "✅ CREATED" || echo "⚠️ REMOVED")
2640
+ fi
2641
+ printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Agent User (salesmanagementagent...)" "$AGENT_BEFORE_LABEL" "$AGENT_AFTER_LABEL" "$AGENT_STATUS_COL"
2642
+
2643
+ # --- PSGs ---
2644
+ AFTER_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")
2645
+ AFTER_PSG_COUNT=$((${AFTER_PSG_COUNT:-0} + 0))
2646
+ if [[ "$BEFORE_PSG_COUNT" == "$AFTER_PSG_COUNT" ]]; then
2647
+ PSG_STATUS_COL=$([ "$AFTER_PSG_COUNT" -ge 2 ] && echo "✅ UNCHANGED" || echo "⚠️ PARTIAL")
2648
+ else
2649
+ PSG_STATUS_COL=$([ "$AFTER_PSG_COUNT" -ge 2 ] && echo "✅ CREATED" || echo "⚠️ PARTIAL")
2650
+ fi
2651
+ printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Permission Set Groups (license)" "$BEFORE_PSG_COUNT PSGs" "$AFTER_PSG_COUNT PSGs" "$PSG_STATUS_COL"
2652
+
2653
+ # --- Schedule Flow ---
2654
+ # Resolve via the shared SourceTemplateId-first resolver (#6) so a freshly deployed
2655
+ # flow is not misreported as "Not Found" by a fuzzy label query.
2656
+ AFTER_FLOW_ACTIVE=$(schedule_flow_active "$ORG_ALIAS")
2657
+ case "$BEFORE_FLOW_ACTIVE" in
2658
+ true) FLOW_BEFORE_LABEL="✅ Active" ;;
2659
+ false) FLOW_BEFORE_LABEL="⚠️ Draft" ;;
2660
+ *) FLOW_BEFORE_LABEL="Not Deployed" ;;
2661
+ esac
2662
+ case "$AFTER_FLOW_ACTIVE" in
2663
+ true) FLOW_AFTER_LABEL="✅ Active" ;;
2664
+ false) FLOW_AFTER_LABEL="⚠️ Draft" ;;
2665
+ *) FLOW_AFTER_LABEL="❌ Not Found" ;;
2666
+ esac
2667
+ if [[ "$BEFORE_FLOW_ACTIVE" == "$AFTER_FLOW_ACTIVE" ]]; then
2668
+ FLOW_STATUS_COL=$([ "$AFTER_FLOW_ACTIVE" == "true" ] && echo "✅ UNCHANGED" || echo "⚠️ PARTIAL")
2669
+ else
2670
+ FLOW_STATUS_COL=$([ "$AFTER_FLOW_ACTIVE" == "true" ] && echo "✅ CHANGED" || echo "⚠️ PARTIAL")
2671
+ fi
2672
+ printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Schedule Flow (Suggestions)" "$FLOW_BEFORE_LABEL" "$FLOW_AFTER_LABEL" "$FLOW_STATUS_COL"
2673
+
2674
+ # --- Stage Descriptions ---
2675
+ # Same INVALID_TYPE / "0\n0" arithmetic-crash guard as the BEFORE snapshot above:
2676
+ # if OpptStageDescription is not yet queryable (PM enable still settling), `|| true`
2677
+ # lets jq's `// 0` default stand instead of appending a second line.
2678
+ AFTER_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)
2679
+ AFTER_STAGE_DESC_COUNT=$((${AFTER_STAGE_DESC_COUNT:-0} + 0))
2680
+ if [[ "$BEFORE_STAGE_DESC_COUNT" == "$AFTER_STAGE_DESC_COUNT" ]]; then
2681
+ DESC_STATUS_COL=$([ "$AFTER_STAGE_DESC_COUNT" -gt 0 ] && echo "✅ UNCHANGED" || echo "⚠️ NONE")
2682
+ else
2683
+ DESC_STATUS_COL=$([ "$AFTER_STAGE_DESC_COUNT" -gt 0 ] && echo "✅ CREATED" || echo "⚠️ NONE")
2684
+ fi
2685
+ printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Opportunity Stage Descriptions" "$BEFORE_STAGE_DESC_COUNT descriptions" "$AFTER_STAGE_DESC_COUNT descriptions" "$DESC_STATUS_COL"
2686
+
2687
+ printf "└──────────────────────────────────────────┴─────────────────────┴─────────────────────┴────────────────┘\n"
2688
+ echo ""
2689
+
2690
+ # Feature Dependency Sequence
2691
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2692
+ echo " FEATURE DEPENDENCY SEQUENCE "
2693
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2694
+ echo ""
2695
+ echo " Phase 1: Prerequisites"
2696
+ echo " ├─ SOAP API Enabled"
2697
+ echo " ├─ User: 'Use Any API Auth' Permission"
2698
+ echo " ├─ Einstein GenAI Enabled"
2699
+ echo " └─ Enhanced Notes Enabled"
2700
+ echo " │"
2701
+ echo " ▼"
2702
+ echo " Phase 2: Prerequisite Objects"
2703
+ echo " ├─ Enhanced Notes Configuration"
2704
+ echo " └─ Opportunity Splits Configuration"
2705
+ echo " │"
2706
+ echo " ▼"
2707
+ echo " Phase 3: Pipeline Management Enablement"
2708
+ echo " └─ SalesDealAgentSettings.enableDealAgent = true"
2709
+ echo " │"
2710
+ echo " ▼"
2711
+ echo " Phase 3.5: Autonomous Field Updates (opt-in via --autonomous)"
2712
+ echo " ├─ Requires: Phase 3 Complete"
2713
+ echo " └─ Sets: enableDealAgentAutoApproveAllTasks = true (only when opted in; else suggestions-only)"
2714
+ echo " │"
2715
+ echo " ▼"
2716
+ echo " Phase 4: Provisioning (async)"
2717
+ echo " ├─ Agent User: salesmanagementagentuser@..."
2718
+ echo " └─ PSGs: SalesManagementUserPsg, SalesManagementAgentUserPsg"
2719
+ echo " │"
2720
+ echo " ▼"
2721
+ echo " Phase 4c.5: Opportunity Stage Descriptions (only if StageName selected; opt-in via --create-stage-descriptions)"
2722
+ echo " ├─ Requires: Phase 4 Complete (OpptStageDescription API accessible)"
2723
+ echo " ├─ Runs BEFORE the StageName prompt is deployed/tested (it is the grounding)"
2724
+ echo " ├─ Standard Stages: Methodology-specific descriptions"
2725
+ echo " └─ Custom Stages: Context-aware recommendations (user confirmation)"
2726
+ echo " │"
2727
+ echo " ▼"
2728
+ echo " Phase 5: Schedule-Triggered Flow"
2729
+ echo " └─ Deploy & Activate: Process_Field_Update_Suggestions"
2730
+ echo " │"
2731
+ echo " ▼"
2732
+ echo " Phase 7: User Permissions"
2733
+ echo " └─ Assign: SalesManagementUserPsg to users"
2734
+ echo ""
2735
+
2736
+ # Issues Encountered
2737
+ if [[ ${#SETUP_ISSUES[@]} -gt 0 ]]; then
2738
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2739
+ echo " ISSUES ENCOUNTERED "
2740
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2741
+ echo ""
2742
+ printf "┌──────────┬────────────────────────────────────────────────────────────┬──────────────────────────────────┐\n"
2743
+ printf "│ %-8s │ %-58s │ %-32s │\n" "Severity" "Issue Description" "Resolution"
2744
+ printf "├──────────┼────────────────────────────────────────────────────────────┼──────────────────────────────────┤\n"
2745
+
2746
+ for ISSUE in "${SETUP_ISSUES[@]}"; do
2747
+ IFS='|' read -r SEVERITY DESC RESOLUTION <<< "$ISSUE"
2748
+ printf "│ %-8s │ %-58s │ %-32s │\n" "$SEVERITY" "${DESC:0:58}" "${RESOLUTION:0:32}"
2749
+ done
2750
+
2751
+ printf "└──────────┴────────────────────────────────────────────────────────────┴──────────────────────────────────┘\n"
2752
+ echo ""
2753
+ fi
2754
+
2755
+ # A tracked "🔴 BLOCKER" means setup did NOT complete — do not report success.
2756
+ # (track_issue only records; some blocking paths exit inline, but a BLOCKER can also
2757
+ # be recorded without an inline exit, e.g. bot-activation failure at Phase 4b. This
2758
+ # gate ensures such a run reports SETUP INCOMPLETE and exits non-zero.)
2759
+ # NOTE: only "🔴 BLOCKER" is terminal. "🔴 ERROR" is provisional — it marks a state
2760
+ # the script then attempts to recover from (e.g. SOAP re-provisioning toggle); a
2761
+ # recovered run must still be allowed to report SETUP COMPLETE.
2762
+ BLOCKER_COUNT=0
2763
+ for ISSUE in "${SETUP_ISSUES[@]}"; do
2764
+ case "$ISSUE" in
2765
+ "🔴 BLOCKER"*) BLOCKER_COUNT=$((BLOCKER_COUNT + 1)) ;;
2766
+ esac
2767
+ done
2768
+
2769
+ if [[ "$BLOCKER_COUNT" -gt 0 ]]; then
2770
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2771
+ echo " SETUP INCOMPLETE "
2772
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2773
+ echo ""
2774
+ echo " $BLOCKER_COUNT blocking issue(s) (🔴) were recorded above. Setup did not finish successfully."
2775
+ echo " Resolve the issue(s) in the table and re-run: bash setup-all.sh $ORG_ALIAS"
2776
+ echo ""
2777
+ exit 1
2778
+ fi
2779
+
2780
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2781
+ echo " SETUP COMPLETE "
2782
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2783
+ echo ""
2784
+ echo "Next steps:"
2785
+ echo " - If stage descriptions show FAIL, re-run in 5 minutes (provisioning)"
2786
+ echo " - Add notes/emails to opportunities to give the agent data"
2787
+ echo " - Wait for daily schedule flow (or check Setup → Flows → run history)"
2788
+ echo " - Agent Analytics: Setup → Einstein Feedback and Monitoring"
2789
+ echo ""
2790
+ echo "Setup URL: $(sanitize_display "${INSTANCE_URL}/lightning/setup/PipelineManagement/home" 80)"
2791
+ echo ""