@salesforce/afv-skills 1.30.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 (126) 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/data360-code-extension-generate/SKILL.md +15 -2
  13. package/skills/dx-devops-work-item-manage/SKILL.md +270 -0
  14. package/skills/dx-devops-work-item-manage/examples/common-workflows.md +479 -0
  15. package/skills/dx-devops-work-item-manage/references/cli-commands.md +476 -0
  16. package/skills/dx-org-manage/SKILL.md +8 -5
  17. package/skills/dx-org-trial-expiration-check/SKILL.md +239 -0
  18. package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +588 -0
  19. package/skills/dx-pkg-post-install-configure/SKILL.md +184 -0
  20. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +202 -35
  21. package/skills/experience-ui-bundle-app-coordinate/scripts/check-hosting-target.sh +26 -0
  22. package/skills/experience-ui-bundle-app-coordinate/scripts/check-sfdx-project.sh +15 -0
  23. package/skills/experience-ui-bundle-custom-app-generate/SKILL.md +29 -5
  24. package/skills/experience-ui-bundle-custom-app-generate/references/configure-metadata-custom-application.md +1 -1
  25. package/skills/experience-ui-bundle-custom-app-generate/scripts/resolve-uibundle-path.sh +30 -0
  26. package/skills/experience-ui-bundle-features-generate/SKILL.md +86 -11
  27. package/skills/experience-ui-bundle-features-generate/references/conflict-resolution-schema.json +20 -0
  28. package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +14 -0
  29. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +6 -1
  30. package/skills/experience-ui-bundle-frontend-generate/references/component.md +34 -1
  31. package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -1
  32. package/skills/experience-ui-bundle-metadata-generate/SKILL.md +19 -8
  33. package/skills/experience-ui-bundle-metadata-generate/references/csp-metadata-format.md +2 -2
  34. package/skills/experience-ui-bundle-metadata-generate/scripts/check-api-version.sh +30 -0
  35. package/skills/experience-ui-bundle-project-generate/SKILL.md +111 -0
  36. package/skills/experience-ui-bundle-project-generate/scripts/flatten-project.mjs +33 -0
  37. package/skills/platform-custom-field-generate/SKILL.md +12 -11
  38. package/skills/platform-custom-field-generate/references/advanced-picklists.md +17 -17
  39. package/skills/platform-custom-lightning-type-generate/SKILL.md +9 -14
  40. package/skills/platform-custom-lightning-type-generate/references/widget-rendition.md +27 -111
  41. package/skills/platform-custom-report-type-generate/SKILL.md +259 -0
  42. package/skills/platform-custom-report-type-generate/examples/AccountProjectsWithTasks.reportType-meta.xml +51 -0
  43. package/skills/platform-custom-report-type-generate/examples/AccountsWithIndustry.reportType-meta.xml +27 -0
  44. package/skills/platform-custom-report-type-generate/examples/AccountsWithProjects.reportType-meta.xml +44 -0
  45. package/skills/platform-custom-report-type-generate/references/category-values.md +32 -0
  46. package/skills/platform-custom-report-type-generate/references/errors-and-troubleshooting.md +24 -0
  47. package/skills/platform-dataspace-access-configure/SKILL.md +318 -0
  48. package/skills/platform-docs-get/SKILL.md +4 -0
  49. package/skills/platform-docs-get/scripts/extract_salesforce_doc.py +78 -13
  50. package/skills/platform-encryption-configure/SKILL.md +122 -0
  51. package/skills/platform-encryption-configure/assets/EncryptionKey.settings-meta.xml +17 -0
  52. package/skills/platform-encryption-configure/assets/PlatformEncryption.settings-meta.xml +15 -0
  53. package/skills/platform-encryption-configure/assets/encrypted-field.field-meta.xml +22 -0
  54. package/skills/platform-encryption-configure/examples/cache-only-keys.settings-meta.xml +11 -0
  55. package/skills/platform-encryption-configure/references/encryption-schemes.md +34 -0
  56. package/skills/platform-encryption-configure/references/key-models.md +34 -0
  57. package/skills/platform-encryption-configure/references/tenant-secret-lifecycle.md +23 -0
  58. package/skills/platform-encryption-configure/scripts/validate-encryption-metadata.sh +54 -0
  59. package/skills/platform-lightning-type-widget-coordinate/SKILL.md +214 -0
  60. package/skills/platform-lightning-type-widget-coordinate/examples/existing-lightning-type-with-widget-prompt.md +80 -0
  61. package/skills/platform-lightning-type-widget-coordinate/examples/new-lightning-type-with-widget-prompt.md +75 -0
  62. package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +67 -0
  63. package/skills/platform-lightning-type-widget-coordinate/references/lightning-type-discovery.md +124 -0
  64. package/skills/platform-lightning-type-widget-coordinate/references/validation-gates.md +130 -0
  65. package/skills/platform-metadata-api-context-get/SKILL.md +13 -10
  66. package/skills/platform-sharing-owd-configure/SKILL.md +142 -0
  67. package/skills/platform-sharing-owd-configure/examples/get_owd_output.md +38 -0
  68. package/skills/platform-sharing-owd-configure/examples/update_owd_output.md +44 -0
  69. package/skills/platform-sharing-owd-configure/references/access_levels.md +49 -0
  70. package/skills/platform-sharing-owd-configure/references/metadata_api_approach.md +69 -0
  71. package/skills/platform-sharing-rules-generate/SKILL.md +89 -19
  72. package/skills/platform-sharing-rules-generate/examples/test-cases.md +381 -0
  73. package/skills/platform-widget-generate/SKILL.md +241 -0
  74. package/skills/platform-widget-generate/examples/conditional.json +73 -0
  75. package/skills/platform-widget-generate/examples/list-with-foreach.json +66 -0
  76. package/skills/platform-widget-generate/examples/single-object.json +64 -0
  77. package/skills/platform-widget-generate/references/schema-from-lightning-type.md +76 -0
  78. package/skills/platform-widget-generate/references/widget-bundle-layout.md +116 -0
  79. package/skills/platform-widget-generate/references/widget-meta-directives.md +130 -0
  80. package/skills/sales-agentforce-pipeline-management-configure/SKILL.md +268 -0
  81. package/skills/sales-agentforce-pipeline-management-configure/assets/field-completion-template.genAiPromptTemplate-meta.xml +141 -0
  82. package/skills/sales-agentforce-pipeline-management-configure/assets/pipeline_management_flow.flow-meta.xml +548 -0
  83. package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.agent +2938 -0
  84. package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.bundle-meta.xml +4 -0
  85. package/skills/sales-agentforce-pipeline-management-configure/assets/sfdx-project.json +12 -0
  86. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/bant.txt +11 -0
  87. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/meddic.txt +11 -0
  88. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/spiced.txt +11 -0
  89. package/skills/sales-agentforce-pipeline-management-configure/examples/custom-prompt-instructions.md +317 -0
  90. package/skills/sales-agentforce-pipeline-management-configure/references/admin-communication.md +355 -0
  91. package/skills/sales-agentforce-pipeline-management-configure/references/agent-creation.md +383 -0
  92. package/skills/sales-agentforce-pipeline-management-configure/references/auth-and-cli.md +146 -0
  93. package/skills/sales-agentforce-pipeline-management-configure/references/automation-matrix.md +23 -0
  94. package/skills/sales-agentforce-pipeline-management-configure/references/autonomous-updates.md +215 -0
  95. package/skills/sales-agentforce-pipeline-management-configure/references/canonical-agent-path.md +75 -0
  96. package/skills/sales-agentforce-pipeline-management-configure/references/data-sources.md +155 -0
  97. package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompt-template.md +120 -0
  98. package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompts.md +149 -0
  99. package/skills/sales-agentforce-pipeline-management-configure/references/flags.md +13 -0
  100. package/skills/sales-agentforce-pipeline-management-configure/references/flow-clone-from-template.md +447 -0
  101. package/skills/sales-agentforce-pipeline-management-configure/references/metadata-inventory.md +330 -0
  102. package/skills/sales-agentforce-pipeline-management-configure/references/opportunity-stages.md +246 -0
  103. package/skills/sales-agentforce-pipeline-management-configure/references/repair-diagnostics.md +265 -0
  104. package/skills/sales-agentforce-pipeline-management-configure/references/setup-order.md +263 -0
  105. package/skills/sales-agentforce-pipeline-management-configure/references/soap-api-enablement.md +528 -0
  106. package/skills/sales-agentforce-pipeline-management-configure/scripts/CHANGES.md +180 -0
  107. package/skills/sales-agentforce-pipeline-management-configure/scripts/README.md +56 -0
  108. package/skills/sales-agentforce-pipeline-management-configure/scripts/add-field-suggestion.sh +679 -0
  109. package/skills/sales-agentforce-pipeline-management-configure/scripts/create-agent.sh +302 -0
  110. package/skills/sales-agentforce-pipeline-management-configure/scripts/create-flow.sh +250 -0
  111. package/skills/sales-agentforce-pipeline-management-configure/scripts/define-agent-access.sh +430 -0
  112. package/skills/sales-agentforce-pipeline-management-configure/scripts/deploy-settings.sh +77 -0
  113. package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-deal-agent.sh +241 -0
  114. package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-prerequisites.sh +538 -0
  115. package/skills/sales-agentforce-pipeline-management-configure/scripts/flow-debug-and-verify.sh +385 -0
  116. package/skills/sales-agentforce-pipeline-management-configure/scripts/retrieve-settings.sh +131 -0
  117. package/skills/sales-agentforce-pipeline-management-configure/scripts/setup-all.sh +2791 -0
  118. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-bundle-publish.sh +189 -0
  119. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-detection.sh +239 -0
  120. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/flow-builder.sh +238 -0
  121. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/psg.sh +48 -0
  122. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/soap.sh +140 -0
  123. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/stage-descriptions.sh +448 -0
  124. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/test-opp.sh +166 -0
  125. package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-all.sh +636 -0
  126. package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-prompt-generation.sh +346 -0
@@ -0,0 +1,679 @@
1
+ #!/bin/bash
2
+
3
+ # Add an AI field-update suggestion for an Opportunity text field.
4
+ #
5
+ # WHEN to use this: to add a field AFTER initial setup. Greenfield setup is
6
+ # field-selection-driven — run setup-all.sh with --fields "<...>" to build the
7
+ # flow with the desired field set from the start (setup-all.sh calls this script
8
+ # internally with --skip-flow to create/activate each selected field's template,
9
+ # then wires the whole set via shared/flow-builder.sh build_field_collection). Reach
10
+ # for this script directly only to add a NEW field to an already-configured org:
11
+ # it creates/activates the template AND wires the field into the live flow.
12
+ #
13
+ # WHAT this does (the three things a field needs to produce suggestions):
14
+ # 1. Creates + ACTIVATES a field-completion (type einstein_gpt__fieldCompletion)
15
+ # GenAiPromptTemplate for the field, from the canonical asset
16
+ # assets/field-completion-template.genAiPromptTemplate-meta.xml.
17
+ # 2. Adds the field's API name to the ACTIVE suggestion flow's
18
+ # OpportunityFields collection (Process_Field_Update_Suggestions),
19
+ # idempotently, so getOrExecFieldUpdtSuggestion asks for it.
20
+ # 3. Verifies the field is accessible (FLS) — warns if not.
21
+ #
22
+ # WHY this is hardened (no retry loops — the failures this AVOIDS):
23
+ # - Template activation is DETERMINISTIC. GenAiPromptTemplate IS retrievable
24
+ # via CLI for user-created templates, so we deploy (Published, no version
25
+ # identifiers), RETRIEVE the platform-generated <versionIdentifier>, set it
26
+ # as <activeVersionIdentifier>, and redeploy. We never guess the identifier.
27
+ # - The flow edit is XML-aware (python3 ElementTree, namespace-registered),
28
+ # not sed. It inserts a <assignmentItems> block ONLY if the field is absent
29
+ # (idempotent), preserves every sales_pipe_mgmt__* namespace reference, and
30
+ # keeps <status>Active</status> so the running schedule is never deactivated.
31
+ # - Verification uses the SYNCHRONOUS /generations endpoint (proves the
32
+ # template resolves + grounds now), not the async AiGenActionItem schedule
33
+ # path. The scheduled suggestions land in AiGenActionItem (~2-3 min after
34
+ # the daily run; poll >=240s); that guidance is printed, not blocked on.
35
+ #
36
+ # Usage:
37
+ # ./add-field-suggestion.sh <org-alias> <FieldApiName> [options]
38
+ #
39
+ # Options:
40
+ # --label "<text>" Field label for prompt text (default: from describe)
41
+ # --instruction "<text>" One-line field-specific extraction guidance
42
+ # --goal "<text>" One phrase for "You must think about <goal>"
43
+ # --name <DevName> GenAiPromptTemplate developerName (default: Recommend<Field>forOpp)
44
+ # --verify-with-note Seed a ContentNote on a sample opp + run /generations
45
+ # --note "<text>" Body of the seeded verification note (used with
46
+ # --verify-with-note). Pass a note tailored to this
47
+ # field's goal/instruction so generation exercises the
48
+ # customized prompt. Default: a generic multi-signal note.
49
+ # --opp <OppId> Opportunity to use for --verify-with-note (default: first open opp)
50
+ # --skip-flow Create/activate the template only; do not edit the flow
51
+ # --force Regenerate + redeploy the template even if it is
52
+ # already active (mints a new template version)
53
+ #
54
+ # OOTB fields (currently only NextStep): two modes
55
+ # The flow no longer ships every OOTB field — a field only generates suggestions
56
+ # once it is wired into the flow's OpportunityFields collection. For a field that
57
+ # ships with a managed field-completion prompt, this script has two modes:
58
+ #
59
+ # 1. WIRE-ONLY (no --goal/--instruction): keep the curated OOTB prompt exactly as
60
+ # shipped and just wire the field into the flow. Nothing is deployed under a
61
+ # custom template name; Steps 2-3 are skipped. This is the common way to add
62
+ # NextStep post-setup when it was not selected during initial setup.
63
+ # 2. OVERRIDE (with --goal and/or --instruction): deploy an OVERRIDE of the managed
64
+ # template (<overrideSource>), which the platform records as a NEW VERSION of
65
+ # that managed template — not a separate prompt. This is the only supported way
66
+ # to customize the OOTB goal/instruction.
67
+ #
68
+ # Either way Step 4 runs its idempotent flow wiring to guarantee the field is in
69
+ # OpportunityFields. Passing --skip-flow with an OOTB field and no --goal/
70
+ # --instruction is a no-op and is rejected. StageName is a picklist and is not
71
+ # eligible for either mode.
72
+ #
73
+ # Re-run safety: the flow edit (Step 4) is fully idempotent. The template
74
+ # deploy (Steps 2-3) is NOT — a redeploy mints a NEW template version each run.
75
+ # To avoid churning versions on an already-configured field, this script
76
+ # SKIPS Steps 2-3 when the template already exists and is active, unless
77
+ # --force is passed. The flow wiring and verification still run.
78
+
79
+ set -euo pipefail
80
+
81
+ # sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.
82
+ # 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).
83
+ export NO_COLOR=1
84
+ export FORCE_COLOR=0
85
+
86
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
87
+ ASSET_TEMPLATE="$SCRIPT_DIR/../assets/field-completion-template.genAiPromptTemplate-meta.xml"
88
+ FLOW_API_NAME="Process_Field_Update_Suggestions"
89
+ TEMPLATE_DIR="force-app/main/default/genAiPromptTemplates"
90
+ FLOW_DIR="force-app/main/default/flows"
91
+ # Metadata is authored at 67.0 (see sfdx-project.json below), but the Einstein
92
+ # prompt-template REST endpoints are pinned to v64.0 — the version verified to
93
+ # return the /generations and /prompt-templates shapes this script parses, and
94
+ # the version the sibling setup scripts use. Deliberate split; do not "upgrade".
95
+ API_VERSION="v64.0"
96
+ # Hard cap on the number of Opportunity fields the suggestion flow may manage,
97
+ # OOTB fields (NextStep, StageName) inclusive. Pipeline Management supports at
98
+ # most this many managed fields; the flow-wiring step below refuses to exceed it.
99
+ MAX_SUGGESTION_FIELDS=5
100
+
101
+ # OOTB fields that already ship with a MANAGED field-completion prompt template
102
+ # (sales_pipe_mgmt namespace). You cannot edit a managed template, so customizing
103
+ # one of these fields' goal/instruction is done by deploying an OVERRIDE — a
104
+ # fieldCompletion template whose <overrideSource> points at the managed template.
105
+ # The platform records the override as a NEW VERSION of the managed template
106
+ # (verified on a live org), NOT as a separate prompt. Overriding only happens when
107
+ # the caller explicitly passes --goal/--instruction; otherwise the OOTB prompt is
108
+ # left untouched. Only NextStep is listed: StageName is a picklist and is rejected
109
+ # by the eligibility gate below, so it is intentionally NOT overridable here.
110
+ declare -A OOTB_MANAGED_TEMPLATE=(
111
+ [NextStep]="sales_pipe_mgmt__RecommendNextStepforOpp"
112
+ )
113
+
114
+ # --- Helpers (mirror setup-all.sh / define-agent-access.sh conventions) ---
115
+ log_pass() { echo " [PASS] $1"; }
116
+ log_warn() { echo " [WARN] $1"; }
117
+ log_fail() { echo " [FAIL] $1"; }
118
+ log_info() { echo " [....] $1"; }
119
+ log_try() { echo " [TRY ] $1"; }
120
+
121
+ die() { log_fail "$1"; exit 1; }
122
+
123
+ # --- Arg parsing ---
124
+ ORG_ALIAS="${1:-}"
125
+ FIELD_API="${2:-}"
126
+ FIELD_LABEL=""
127
+ FIELD_INSTRUCTION=""
128
+ FIELD_GOAL=""
129
+ DEV_NAME=""
130
+ VERIFY_WITH_NOTE=false
131
+ OPP_ID=""
132
+ NOTE_TEXT="" # --note "<text>": caller-supplied verification note body (used with --verify-with-note)
133
+ SKIP_FLOW=false
134
+ FORCE=false
135
+
136
+ if [[ -z "$ORG_ALIAS" || -z "$FIELD_API" ]]; then
137
+ echo "Usage: $0 <org-alias> <FieldApiName> [--label TEXT] [--instruction TEXT] [--goal TEXT] [--name DevName] [--verify-with-note] [--note TEXT] [--opp OppId] [--skip-flow] [--force]"
138
+ exit 1
139
+ fi
140
+
141
+ shift 2 || true
142
+ # Require a value for value-taking flags (else a trailing bare flag would make
143
+ # `shift 2` fail and abort under `set -e` with no message).
144
+ need_val() { [[ $# -ge 2 ]] || die "Option '$1' requires a value."; }
145
+ while [[ $# -gt 0 ]]; do
146
+ case "$1" in
147
+ --label) need_val "$@"; FIELD_LABEL="$2"; shift 2 ;;
148
+ --instruction) need_val "$@"; FIELD_INSTRUCTION="$2"; shift 2 ;;
149
+ --goal) need_val "$@"; FIELD_GOAL="$2"; shift 2 ;;
150
+ --name) need_val "$@"; DEV_NAME="$2"; shift 2 ;;
151
+ --opp) need_val "$@"; OPP_ID="$2"; shift 2 ;;
152
+ --note) need_val "$@"; NOTE_TEXT="$2"; shift 2 ;;
153
+ --verify-with-note) VERIFY_WITH_NOTE=true; shift ;;
154
+ --skip-flow) SKIP_FLOW=true; shift ;;
155
+ --force) FORCE=true; shift ;;
156
+ *) die "Unknown option: $1" ;;
157
+ esac
158
+ done
159
+
160
+ command -v python3 >/dev/null 2>&1 || die "python3 is required for the XML-aware flow edit."
161
+ [[ -f "$ASSET_TEMPLATE" ]] || die "Canonical template asset not found: $ASSET_TEMPLATE"
162
+
163
+ # Ensure a project context exists for deploy/retrieve.
164
+ if [[ ! -f "sfdx-project.json" ]]; then
165
+ cat > sfdx-project.json << 'EOF'
166
+ {"packageDirectories": [{"path": "force-app", "default": true}], "sourceApiVersion": "67.0"}
167
+ EOF
168
+ fi
169
+
170
+ echo "Add Field Suggestion: $FIELD_API (org: $ORG_ALIAS)"
171
+ echo ""
172
+
173
+ # ============================================================
174
+ # Step 1 — Validate field eligibility (describe)
175
+ # ============================================================
176
+ echo "Step 1: Validate field eligibility"
177
+
178
+ DESCRIBE=$(sf sobject describe --sobject Opportunity --target-org "$ORG_ALIAS" --json 2>/dev/null) \
179
+ || die "Could not describe Opportunity on org '$ORG_ALIAS'. Check auth."
180
+
181
+ FIELD_JSON=$(echo "$DESCRIBE" | jq -c --arg f "$FIELD_API" '.result.fields[] | select(.name == $f)')
182
+ if [[ -z "$FIELD_JSON" ]]; then
183
+ # `describe` only returns fields the running user can SEE. A freshly created
184
+ # custom field has NO field-level security, so it exists but is invisible here.
185
+ # Distinguish "doesn't exist" from "no FLS" via the Tooling API, and tell the
186
+ # user the exact fix — so this never turns into blind retries.
187
+ EXISTS=$(sf data query -q "SELECT QualifiedApiName FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName='Opportunity' AND QualifiedApiName='${FIELD_API}'" \
188
+ --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null | jq -r '.result.records[0].QualifiedApiName // empty')
189
+ if [[ -n "$EXISTS" ]]; then
190
+ ADMIN_PS=$(sf data query -q "SELECT Id FROM PermissionSet WHERE IsOwnedByProfile=true AND Profile.Name='System Administrator' LIMIT 1" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty')
191
+ echo ""
192
+ log_fail "Field '$FIELD_API' exists but is not visible to you — it has no field-level security yet."
193
+ echo " A newly created custom field is invisible to 'describe' until FLS is granted."
194
+ echo " Grant read/edit to your admin permission set, then re-run this script:"
195
+ echo ""
196
+ echo " sf data create record --sobject FieldPermissions \\"
197
+ echo " --values \"ParentId='${ADMIN_PS:-<AdminPermSetId>}' SobjectType='Opportunity' Field='Opportunity.${FIELD_API}' PermissionsRead=true PermissionsEdit=true\" \\"
198
+ echo " --target-org $ORG_ALIAS"
199
+ echo ""
200
+ exit 1
201
+ fi
202
+ die "Field '$FIELD_API' not found on Opportunity (no such field in FieldDefinition)."
203
+ fi
204
+
205
+ F_TYPE=$(echo "$FIELD_JSON" | jq -r '.type')
206
+ F_LEN=$(echo "$FIELD_JSON" | jq -r '.length')
207
+ F_UPDATEABLE=$(echo "$FIELD_JSON" | jq -r '.updateable')
208
+ F_CALCULATED=$(echo "$FIELD_JSON" | jq -r '.calculated')
209
+ F_HTML=$(echo "$FIELD_JSON" | jq -r '.htmlFormatted')
210
+ F_LABEL_DESCRIBED=$(echo "$FIELD_JSON" | jq -r '.label')
211
+
212
+ # Eligibility: plain Text (type=string) or Text Area <=255 (type=textarea), updateable,
213
+ # not calculated, not rich text (htmlFormatted), length <= 255.
214
+ # - Text Area (255) -> type=textarea, htmlFormatted=false, length<=255 ✅
215
+ # - Long Text Area -> type=textarea, length>255 ❌ (length gate)
216
+ # - Rich Text Area -> type=textarea, htmlFormatted=true ❌ (html gate)
217
+ if [[ "$F_TYPE" != "string" && "$F_TYPE" != "textarea" ]]; then
218
+ die "Field '$FIELD_API' is type '$F_TYPE'. Only plain Text (type=string) or Text Area <=255 (type=textarea) fields are supported (not richtext/longtextarea/picklist/formula/date). Suggestions are capped at the field length."
219
+ fi
220
+ [[ "$F_HTML" == "true" ]] && die "Field '$FIELD_API' is a Rich Text Area (htmlFormatted). Not supported — use a plain Text or Text Area (<=255) field."
221
+ [[ "$F_UPDATEABLE" == "true" ]] || die "Field '$FIELD_API' is not updateable."
222
+ [[ "$F_CALCULATED" == "false" ]] || die "Field '$FIELD_API' is calculated (formula). Not supported."
223
+ if [[ "$F_LEN" -gt 255 ]]; then
224
+ die "Field '$FIELD_API' length is $F_LEN (>255 — Long Text Area). Not supported; use a Text or Text Area field of 255 or fewer characters."
225
+ fi
226
+ log_pass "Eligible: type=$F_TYPE, length=$F_LEN, updateable=$F_UPDATEABLE"
227
+
228
+ # OOTB field detection. If this field already ships with a managed field-completion
229
+ # template (only NextStep today), we NEVER author a fresh standalone template. Two
230
+ # modes, decided by whether the caller passed --goal/--instruction:
231
+ # * WIRE_ONLY (no --goal/--instruction): keep the curated OOTB prompt untouched and
232
+ # just wire the field into the flow. The flow no longer ships every OOTB field, so
233
+ # wiring NextStep post-setup is a legitimate, common action. Nothing is deployed.
234
+ # * OVERRIDE (--goal/--instruction given): author an OVERRIDE of the managed template
235
+ # (<overrideSource>), which the platform records as a NEW VERSION of the managed
236
+ # template. This is the ONLY supported way to change the OOTB goal/instruction.
237
+ # We require an explicit --goal/--instruction before overriding: doing it with the
238
+ # generic defaults would silently replace a curated OOTB prompt with something weaker.
239
+ OVERRIDE_SOURCE=""
240
+ WIRE_ONLY=false
241
+ if [[ -n "${OOTB_MANAGED_TEMPLATE[$FIELD_API]:-}" ]]; then
242
+ if [[ -z "$FIELD_GOAL" && -z "$FIELD_INSTRUCTION" ]]; then
243
+ if [[ "$SKIP_FLOW" == "true" ]]; then
244
+ die "'$FIELD_API' ships with a curated OOTB managed prompt and no --goal/--instruction was given, so there is no template to deploy — and --skip-flow means there is nothing to wire either. Nothing to do. To customize the prompt pass --goal/--instruction; to wire the field into the flow drop --skip-flow."
245
+ fi
246
+ WIRE_ONLY=true
247
+ log_info "OOTB wire-only mode: '$FIELD_API' keeps its curated OOTB prompt; wiring it into the flow (idempotent). Pass --goal/--instruction to also customize the prompt."
248
+ else
249
+ OVERRIDE_SOURCE="${OOTB_MANAGED_TEMPLATE[$FIELD_API]}"
250
+ log_info "OOTB override mode: '$FIELD_API' overrides managed template '$OVERRIDE_SOURCE' (deploys as a new version)."
251
+ fi
252
+ fi
253
+
254
+ # Defaults derived from describe.
255
+ [[ -z "$FIELD_LABEL" ]] && FIELD_LABEL="$F_LABEL_DESCRIBED"
256
+ [[ -z "$DEV_NAME" ]] && DEV_NAME="Recommend$(echo "$FIELD_API" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')forOpp"
257
+ # developerName must start with a letter and be alphanumeric (Recommend<...>forOpp
258
+ # always satisfies this, but a custom --name could not).
259
+ [[ "$DEV_NAME" =~ ^[A-Za-z][A-Za-z0-9]*$ ]] || die "Template dev name '$DEV_NAME' is invalid (must start with a letter, alphanumeric only). Pass a valid --name."
260
+ [[ -z "$FIELD_GOAL" ]] && FIELD_GOAL="what the most appropriate value of this field is, based on the latest conversation signals."
261
+ [[ -z "$FIELD_INSTRUCTION" ]] && FIELD_INSTRUCTION="Extract only what is explicitly supported by the data sources."
262
+ CHAR_CAP="$F_LEN"
263
+
264
+ # FLS check for the agent user (non-blocking warn).
265
+ AGENT_USER=$(sf data query -q "SELECT Username FROM User WHERE Username LIKE 'salesmanagementagentuser@%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Username // empty')
266
+ if [[ -n "$AGENT_USER" ]]; then
267
+ log_info "Agent user: $AGENT_USER (ensure it has edit FLS on $FIELD_API for autonomous updates)"
268
+ else
269
+ log_warn "Could not resolve the agent user; verify $FIELD_API is accessible to the agent's PSG."
270
+ fi
271
+ echo ""
272
+
273
+ # ============================================================
274
+ # Pre-check — is the template already active? (idempotency guard for #7)
275
+ # A redeploy round-trip (Steps 2-3) mints a NEW template version every run, so
276
+ # skip it when the template is already active and --force was not passed. Uses
277
+ # the same prompt-templates discovery endpoint Step 3 uses to confirm activation.
278
+ # ============================================================
279
+ # The name to look up in the /einstein/prompt-templates endpoint. An OVERRIDE folds
280
+ # into the managed template's record, so its own (un-namespaced) DeveloperName never
281
+ # appears there — the managed name does. Use the managed name for active-state reads
282
+ # in override mode, and the template's own name otherwise.
283
+ # WIRE_ONLY keeps the managed OOTB template (nothing deployed under DEV_NAME), so
284
+ # active-state reads and /generations must target the managed template's name.
285
+ if [[ "$WIRE_ONLY" == "true" ]]; then
286
+ TEMPLATE_QUERY_NAME="${OOTB_MANAGED_TEMPLATE[$FIELD_API]}"
287
+ else
288
+ TEMPLATE_QUERY_NAME="${OVERRIDE_SOURCE:-$DEV_NAME}"
289
+ fi
290
+
291
+ TEMPLATE_ALREADY_ACTIVE=false
292
+ # Skip the idempotency shortcut in override mode: the managed template is ALWAYS
293
+ # active OOTB, so a query would always read "active" and wrongly skip the override
294
+ # deploy. An override is an explicit, opt-in action (--goal/--instruction), so always
295
+ # (re)deploy it when requested; --force is only meaningful for standalone templates.
296
+ if [[ "$FORCE" != "true" && -z "$OVERRIDE_SOURCE" && "$WIRE_ONLY" != "true" ]]; then
297
+ EXISTING_ACTIVE=$(sf api request rest "/services/data/${API_VERSION}/einstein/prompt-templates" --target-org "$ORG_ALIAS" 2>/dev/null \
298
+ | jq -r --arg n "$DEV_NAME" '.promptRecords[]? | select(.fields.DeveloperName.value==$n) | .fields.IsActive.value' 2>/dev/null | head -1)
299
+ if [[ "$EXISTING_ACTIVE" == "true" ]]; then
300
+ TEMPLATE_ALREADY_ACTIVE=true
301
+ fi
302
+ fi
303
+
304
+ if [[ "$WIRE_ONLY" == "true" ]]; then
305
+ echo "Steps 2-3: SKIPPED — '$FIELD_API' keeps its curated OOTB managed prompt (wire-only mode)."
306
+ log_info "No template deployed. Pass --goal/--instruction to deploy an override of the managed prompt."
307
+ echo ""
308
+ elif [[ "$TEMPLATE_ALREADY_ACTIVE" == "true" ]]; then
309
+ echo "Steps 2-3: SKIPPED — template '$DEV_NAME' already exists and is ACTIVE."
310
+ log_info "Not redeploying (would mint a new version). Pass --force to regenerate + redeploy."
311
+ echo ""
312
+ else
313
+
314
+ # ============================================================
315
+ # Step 2 — Generate template XML from canonical asset
316
+ # ============================================================
317
+ echo "Step 2: Generate template XML ($DEV_NAME)"
318
+ mkdir -p "$TEMPLATE_DIR"
319
+ TEMPLATE_FILE="$TEMPLATE_DIR/${DEV_NAME}.genAiPromptTemplate-meta.xml"
320
+ MASTER_LABEL="Recommend ${FIELD_LABEL} for Opportunity"
321
+
322
+ # Substitute placeholders with python3 (safe with arbitrary text; no sed-escaping traps).
323
+ FIELD_API="$FIELD_API" FIELD_LABEL="$FIELD_LABEL" DEV_NAME="$DEV_NAME" \
324
+ MASTER_LABEL="$MASTER_LABEL" CHAR_CAP="$CHAR_CAP" FIELD_GOAL="$FIELD_GOAL" \
325
+ FIELD_INSTRUCTION="$FIELD_INSTRUCTION" OVERRIDE_SOURCE="$OVERRIDE_SOURCE" \
326
+ ASSET="$ASSET_TEMPLATE" OUT="$TEMPLATE_FILE" \
327
+ python3 - << 'PYEOF'
328
+ import os, re, xml.sax.saxutils as sx
329
+ src = open(os.environ["ASSET"], encoding="utf-8").read()
330
+ # Drop the leading XML comment (guidance only) so the deployed file is clean.
331
+ src = re.sub(r"<!--.*?-->\s*", "", src, count=1, flags=re.DOTALL)
332
+ repl = {
333
+ "@@DEVNAME@@": os.environ["DEV_NAME"],
334
+ "@@MASTERLABEL@@": os.environ["MASTER_LABEL"],
335
+ "@@FIELD@@": os.environ["FIELD_API"],
336
+ "@@FIELDLABEL@@": os.environ["FIELD_LABEL"],
337
+ "@@CHARCAP@@": os.environ["CHAR_CAP"],
338
+ "@@FIELD_GOAL@@": os.environ["FIELD_GOAL"],
339
+ "@@FIELD_INSTRUCTION@@": os.environ["FIELD_INSTRUCTION"],
340
+ }
341
+ for k, v in repl.items():
342
+ # Escape XML special chars in substituted values (they live inside element text).
343
+ src = src.replace(k, sx.escape(v))
344
+ # OOTB override mode: the canonical asset ships <overridable>false</overridable>.
345
+ # To register as an override of a managed template, replace that element with
346
+ # <overrideSource>NAMESPACE__ManagedTemplate</overrideSource>. The platform then
347
+ # treats this deploy as a new version of that managed template rather than a new
348
+ # standalone prompt (verified on a live org).
349
+ ovr = os.environ.get("OVERRIDE_SOURCE", "")
350
+ if ovr:
351
+ new_src, n = re.subn(
352
+ r"<overridable>[^<]*</overridable>",
353
+ "<overrideSource>%s</overrideSource>" % sx.escape(ovr),
354
+ src, count=1)
355
+ if n != 1:
356
+ raise SystemExit("expected exactly one <overridable> element to replace for override mode; found %d" % n)
357
+ src = new_src
358
+ open(os.environ["OUT"], "w", encoding="utf-8").write(src)
359
+ print(" wrote " + os.environ["OUT"] + ((" (override of %s)" % ovr) if ovr else ""))
360
+ PYEOF
361
+
362
+ grep -q "@@" "$TEMPLATE_FILE" && die "Unsubstituted placeholder remains in $TEMPLATE_FILE"
363
+ log_pass "Template XML generated (SuggestedNewValue cap = $CHAR_CAP chars)"
364
+ echo ""
365
+
366
+ # ============================================================
367
+ # Step 3 — Deterministic deploy + activate (retrieve round-trip)
368
+ # ============================================================
369
+ echo "Step 3: Deploy and activate template (deterministic round-trip)"
370
+
371
+ log_try "Deploy (status=Published, no version identifiers)"
372
+ DEPLOY1=$(sf project deploy start --source-dir "$TEMPLATE_FILE" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
373
+ if ! echo "$DEPLOY1" | jq -e '.status == 0' >/dev/null 2>&1; then
374
+ ERR=$(echo "$DEPLOY1" | jq -r '.result.details.componentFailures[0].problem // .message // "unknown"' 2>/dev/null)
375
+ die "Initial template deploy failed: $ERR"
376
+ fi
377
+ log_pass "Deployed (inactive — no active version yet)"
378
+
379
+ log_try "Retrieve platform-generated versionIdentifier"
380
+ sf project retrieve start --metadata "GenAiPromptTemplate:${DEV_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null \
381
+ || die "Retrieve failed — cannot read versionIdentifier."
382
+
383
+ # Parse the versionIdentifier as the direct child of <templateVersions> (not a
384
+ # file-wide grep) so a future multi-version template can't hand us the wrong id.
385
+ # A freshly created template has exactly one version; assert that.
386
+ VERSION_ID=$(python3 - "$TEMPLATE_FILE" << 'PYEOF'
387
+ import sys, xml.etree.ElementTree as ET
388
+ NS = "http://soap.sforce.com/2006/04/metadata"; q = lambda t: "{%s}%s" % (NS, t)
389
+ root = ET.parse(sys.argv[1]).getroot()
390
+ ids = [tv.findtext(q("versionIdentifier")) for tv in root.findall(q("templateVersions"))]
391
+ ids = [i for i in ids if i]
392
+ if len(ids) == 1:
393
+ print(ids[0])
394
+ elif len(ids) > 1:
395
+ # Unexpected for a new template; take the last (newest) and note it on stderr.
396
+ sys.stderr.write("multiple versionIdentifiers found; using the last\n"); print(ids[-1])
397
+ PYEOF
398
+ )
399
+ [[ -n "$VERSION_ID" ]] || die "No <versionIdentifier> found after retrieve. Template may not have deployed as a version."
400
+ log_pass "versionIdentifier = $VERSION_ID"
401
+
402
+ # Set activeVersionIdentifier (idempotent: replace if present, else insert after <overridable>).
403
+ if grep -q "<activeVersionIdentifier>" "$TEMPLATE_FILE"; then
404
+ VERSION_ID="$VERSION_ID" python3 - "$TEMPLATE_FILE" << 'PYEOF'
405
+ import os, re, sys
406
+ p = sys.argv[1]; s = open(p).read()
407
+ s = re.sub(r"<activeVersionIdentifier>[^<]*</activeVersionIdentifier>",
408
+ "<activeVersionIdentifier>%s</activeVersionIdentifier>" % os.environ["VERSION_ID"], s)
409
+ open(p, "w").write(s)
410
+ PYEOF
411
+ else
412
+ VERSION_ID="$VERSION_ID" python3 - "$TEMPLATE_FILE" << 'PYEOF'
413
+ import os, re, sys
414
+ p = sys.argv[1]; s = open(p).read()
415
+ tag = " <activeVersionIdentifier>%s</activeVersionIdentifier>\n" % os.environ["VERSION_ID"]
416
+ # Insert right after the opening <GenAiPromptTemplate ...> line.
417
+ s = re.sub(r"(<GenAiPromptTemplate[^>]*>\n)", r"\1" + tag, s, count=1)
418
+ open(p, "w").write(s)
419
+ PYEOF
420
+ fi
421
+
422
+ log_try "Redeploy to activate"
423
+ DEPLOY2=$(sf project deploy start --source-dir "$TEMPLATE_FILE" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
424
+ if ! echo "$DEPLOY2" | jq -e '.status == 0' >/dev/null 2>&1; then
425
+ ERR=$(echo "$DEPLOY2" | jq -r '.result.details.componentFailures[0].problem // .message // "unknown"' 2>/dev/null)
426
+ die "Activation redeploy failed: $ERR"
427
+ fi
428
+
429
+ # Confirm active via the prompt-templates discovery endpoint. In override mode this
430
+ # reads the MANAGED template's record (the override folds into it), which is expected
431
+ # to stay active — the successful redeploy above is the real proof the override took.
432
+ IS_ACTIVE=$(sf api request rest "/services/data/${API_VERSION}/einstein/prompt-templates" --target-org "$ORG_ALIAS" 2>/dev/null \
433
+ | jq -r --arg n "$TEMPLATE_QUERY_NAME" '.promptRecords[]? | select(.fields.DeveloperName.value==$n) | .fields.IsActive.value' 2>/dev/null | head -1)
434
+ if [[ "$IS_ACTIVE" == "true" ]]; then
435
+ if [[ -n "$OVERRIDE_SOURCE" ]]; then
436
+ log_pass "Override deployed — '$OVERRIDE_SOURCE' is ACTIVE with the new version"
437
+ else
438
+ log_pass "Template '$DEV_NAME' is ACTIVE"
439
+ fi
440
+ else
441
+ log_warn "Template deployed but active-state read returned '$IS_ACTIVE'. Verify in Prompt Builder."
442
+ fi
443
+ echo ""
444
+
445
+ fi # end template deploy/activate block (skipped when already active without --force)
446
+
447
+ # ============================================================
448
+ # Step 4 — Register the field in the active flow (idempotent, XML-aware)
449
+ # ============================================================
450
+ if [[ "$SKIP_FLOW" == "true" ]]; then
451
+ log_info "Skipping flow edit (--skip-flow). Field will NOT generate scheduled suggestions until added to $FLOW_API_NAME."
452
+ echo ""
453
+ else
454
+ echo "Step 4: Register field in the active suggestion flow"
455
+
456
+ FLOW_STATE=$(sf data query -q "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName='${FLOW_API_NAME}'" --target-org "$ORG_ALIAS" --json 2>/dev/null)
457
+ FLOW_EXISTS=$(echo "$FLOW_STATE" | jq -r '.result.records[0].ApiName // empty')
458
+ FLOW_ACTIVE=$(echo "$FLOW_STATE" | jq -r '.result.records[0].IsActive // empty')
459
+
460
+ if [[ -z "$FLOW_EXISTS" ]]; then
461
+ log_warn "Flow '$FLOW_API_NAME' not found. Run create-flow.sh first, then re-run with --skip-flow already applied."
462
+ else
463
+ if [[ "$FLOW_ACTIVE" != "true" ]]; then
464
+ log_warn "Flow '$FLOW_API_NAME' is INACTIVE. It must be Active for suggestions to run (create-flow.sh)."
465
+ fi
466
+
467
+ log_try "Retrieve flow"
468
+ mkdir -p "$FLOW_DIR"
469
+ sf project retrieve start --metadata "Flow:${FLOW_API_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null \
470
+ || die "Could not retrieve flow '$FLOW_API_NAME'."
471
+ FLOW_FILE="${FLOW_DIR}/${FLOW_API_NAME}.flow-meta.xml"
472
+ [[ -f "$FLOW_FILE" ]] || die "Flow file not present after retrieve: $FLOW_FILE"
473
+
474
+ # Enforce the field cap BEFORE wiring. Count the fields currently wired in the
475
+ # AddOppFieldsToCollection assignment. If the field is already present, adding is
476
+ # a no-op (handled below) and does not count against the cap. If it is new and the
477
+ # flow is already at MAX_SUGGESTION_FIELDS, refuse — the flow can manage no more.
478
+ CAP_RC=0
479
+ CAP_OUT=$(FIELD_API="$FIELD_API" MAX_FIELDS="$MAX_SUGGESTION_FIELDS" python3 - "$FLOW_FILE" << 'PYEOF'
480
+ import os, sys
481
+ import xml.etree.ElementTree as ET
482
+
483
+ NS = "http://soap.sforce.com/2006/04/metadata"
484
+ q = lambda t: "{%s}%s" % (NS, t)
485
+
486
+ path = sys.argv[1]
487
+ field = os.environ["FIELD_API"]
488
+ cap = int(os.environ["MAX_FIELDS"])
489
+ root = ET.parse(path).getroot()
490
+
491
+ target = None
492
+ for a in root.findall(q("assignments")):
493
+ name = a.find(q("name"))
494
+ if name is not None and name.text == "AddOppFieldsToCollection":
495
+ target = a
496
+ break
497
+ if target is None:
498
+ sys.stderr.write("AddOppFieldsToCollection assignment not found\n")
499
+ sys.exit(3)
500
+
501
+ fields = []
502
+ for item in target.findall(q("assignmentItems")):
503
+ sv = item.find(q("value") + "/" + q("stringValue"))
504
+ if sv is not None and sv.text:
505
+ fields.append(sv.text)
506
+
507
+ # Field already wired -> adding it is idempotent, never breaches the cap.
508
+ if field in fields:
509
+ sys.exit(0)
510
+ # New field would push past the cap -> block. Print the wired set for the message.
511
+ if len(fields) >= cap:
512
+ sys.stdout.write(", ".join(fields))
513
+ sys.exit(4)
514
+ sys.exit(0)
515
+ PYEOF
516
+ ) || CAP_RC=$?
517
+ if [[ "$CAP_RC" == "4" ]]; then
518
+ die "Field cap reached: the suggestion flow already wires $MAX_SUGGESTION_FIELDS fields (${CAP_OUT}). Pipeline Management supports at most $MAX_SUGGESTION_FIELDS managed Opportunity fields. Remove one before adding '$FIELD_API'."
519
+ elif [[ "$CAP_RC" == "3" ]]; then
520
+ die "Could not locate AddOppFieldsToCollection in flow '$FLOW_API_NAME' — cannot verify the field cap."
521
+ fi
522
+
523
+ # XML-aware idempotent insert: add a <assignmentItems> for the field into the
524
+ # AddOppFieldsToCollection assignment ONLY if not already present. Preserves
525
+ # namespace + all other content. Exits 2 = already present (no-op).
526
+ RC=0
527
+ FIELD_API="$FIELD_API" python3 - "$FLOW_FILE" << 'PYEOF' || RC=$?
528
+ import os, sys
529
+ import xml.etree.ElementTree as ET
530
+
531
+ NS = "http://soap.sforce.com/2006/04/metadata"
532
+ ET.register_namespace("", NS) # keep default namespace, no ns0: prefixes
533
+ q = lambda t: "{%s}%s" % (NS, t)
534
+
535
+ path = sys.argv[1]
536
+ field = os.environ["FIELD_API"]
537
+ tree = ET.parse(path)
538
+ root = tree.getroot()
539
+
540
+ # Find the AddOppFieldsToCollection assignment.
541
+ target = None
542
+ for a in root.findall(q("assignments")):
543
+ name = a.find(q("name"))
544
+ if name is not None and name.text == "AddOppFieldsToCollection":
545
+ target = a
546
+ break
547
+ if target is None:
548
+ sys.stderr.write("AddOppFieldsToCollection assignment not found\n")
549
+ sys.exit(3)
550
+
551
+ # Already present? (idempotent)
552
+ for item in target.findall(q("assignmentItems")):
553
+ sv = item.find(q("value") + "/" + q("stringValue"))
554
+ if sv is not None and sv.text == field:
555
+ print(" already present: %s" % field)
556
+ sys.exit(2)
557
+
558
+ # Build a new <assignmentItems> matching the existing shape.
559
+ item = ET.SubElement(target, q("assignmentItems"))
560
+ ET.SubElement(item, q("assignToReference")).text = "OpportunityFields"
561
+ ET.SubElement(item, q("operator")).text = "Add"
562
+ val = ET.SubElement(item, q("value"))
563
+ ET.SubElement(val, q("stringValue")).text = field
564
+
565
+ # assignmentItems must precede <connector>; move the new one before it if needed.
566
+ children = list(target)
567
+ conn = target.find(q("connector"))
568
+ if conn is not None:
569
+ target.remove(item)
570
+ idx = children.index(conn)
571
+ target.insert(idx, item)
572
+
573
+ tree.write(path, encoding="UTF-8", xml_declaration=True)
574
+ print(" inserted: %s" % field)
575
+ sys.exit(0)
576
+ PYEOF
577
+ if [[ "$RC" == "2" ]]; then
578
+ log_pass "Field already in flow collection — no change (idempotent)"
579
+ elif [[ "$RC" == "0" ]]; then
580
+ log_try "Deploy updated flow (status preserved)"
581
+ DEPLOYF=$(sf project deploy start --metadata "Flow:${FLOW_API_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
582
+ if echo "$DEPLOYF" | jq -e '.status == 0' >/dev/null 2>&1; then
583
+ log_pass "Flow updated — '$FIELD_API' added to OpportunityFields"
584
+ else
585
+ ERR=$(echo "$DEPLOYF" | jq -r '.result.details.componentFailures[0].problem // .message // "unknown"' 2>/dev/null)
586
+ die "Flow deploy failed: $ERR (existing suggestions unaffected — flow not changed on failure)"
587
+ fi
588
+ else
589
+ die "Flow edit helper failed (rc=$RC)."
590
+ fi
591
+ fi
592
+ echo ""
593
+ fi
594
+
595
+ # ============================================================
596
+ # Step 5 — Optional synchronous verification via /generations
597
+ # ============================================================
598
+ if [[ "$VERIFY_WITH_NOTE" == "true" ]]; then
599
+ echo "Step 5: Verify with a seeded note (synchronous /generations)"
600
+
601
+ if [[ -z "$OPP_ID" ]]; then
602
+ # Match the pipeline's own eligibility filter (IsClosed=false AND CloseDate
603
+ # within +90 days) so we don't verify against an opp the scheduled flow would
604
+ # skip — that would surface a misleading "no generation" false negative.
605
+ 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')
606
+ [[ -n "$OPP_ID" ]] || die "No eligible open Opportunity found (IsClosed=false, CloseDate within +90 days); pass --opp <OppId>."
607
+ fi
608
+ log_info "Opportunity: $OPP_ID"
609
+
610
+ # A caller (e.g. the agent-driven prompt tune-loop) can pass --note with a body
611
+ # tailored to THIS field's goal/instruction, so the seeded grounding signal
612
+ # actually exercises the customized prompt. Absent --note, fall back to a generic
613
+ # note that mentions a few common signals.
614
+ if [[ -n "$NOTE_TEXT" ]]; then
615
+ NOTE_BODY="$NOTE_TEXT"
616
+ else
617
+ NOTE_BODY="Verification note for ${FIELD_LABEL}: Customer flagged budget pressure, a competing vendor, and a timeline slip during the last call."
618
+ fi
619
+ # macOS/BSD `base64` line-wraps at 76 cols (embeds newlines); GNU's `-w0` isn't
620
+ # portable. `tr -d '\n'` strips wraps on both so the value stays single-line and
621
+ # doesn't break the --values string below.
622
+ NOTE_B64=$(printf '%s' "$NOTE_BODY" | base64 | tr -d '\n')
623
+
624
+ log_try "Create ContentNote + link to opportunity"
625
+ # Note-seeding is best-effort: `|| true` keeps a create failure from tripping
626
+ # `set -e` so it degrades to the log_warn fallback instead of aborting the run.
627
+ NOTE_ID=$(sf data create record --sobject ContentNote --values "Title='FieldSuggestion Verify ${DEV_NAME}' Content='${NOTE_B64}'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty' || true)
628
+ if [[ -n "$NOTE_ID" ]]; then
629
+ # ContentNote shares the 069 key prefix with ContentDocument — the note Id IS
630
+ # the ContentDocumentId, so link directly. (The ContentVersion→LatestVersionId
631
+ # subquery returns null on some orgs, silently dropping the link.)
632
+ if sf data create record --sobject ContentDocumentLink --values "ContentDocumentId='${NOTE_ID}' LinkedEntityId='${OPP_ID}' ShareType='V' Visibility='AllUsers'" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null; then
633
+ log_pass "Seeded note $NOTE_ID and linked to $OPP_ID"
634
+ else
635
+ log_warn "Note $NOTE_ID created but linking to $OPP_ID failed; generation will run against existing grounding data only."
636
+ fi
637
+ else
638
+ log_warn "Could not seed note; running generation against existing grounding data."
639
+ fi
640
+
641
+ log_try "POST /generations"
642
+ # In override mode, generate against the MANAGED template name — the override
643
+ # deployed as its new active version, so this exercises exactly the prompt the
644
+ # scheduled pipeline will run. The override's own dev name is not a generatable
645
+ # template (it has no standalone record).
646
+ GEN=$(sf api request rest "/services/data/${API_VERSION}/einstein/prompt-templates/${TEMPLATE_QUERY_NAME}/generations" \
647
+ --method POST --target-org "$ORG_ALIAS" \
648
+ --body "{\"isPreview\":false,\"inputParams\":{\"valueMap\":{\"Input:Opportunity\":{\"value\":{\"id\":\"${OPP_ID}\"}}}},\"additionalConfig\":{\"numGenerations\":1,\"temperature\":0,\"applicationName\":\"PromptTemplateGenerationsInvocable\"}}" 2>/dev/null || echo '{}')
649
+ GEN_TEXT=$(echo "$GEN" | jq -r '.generations[0].text // empty')
650
+ if [[ -n "$GEN_TEXT" ]]; then
651
+ log_pass "Generation succeeded:"
652
+ echo "$GEN_TEXT"
653
+ else
654
+ log_warn "No generation text returned. Response:"
655
+ echo "$GEN" | jq -r '.[0].message // .message // .' 2>/dev/null | head -5
656
+ fi
657
+ echo ""
658
+ fi
659
+
660
+ # ============================================================
661
+ # Summary
662
+ # ============================================================
663
+ echo "Done: $FIELD_API"
664
+ if [[ "$WIRE_ONLY" == "true" ]]; then
665
+ echo " Template : ${OOTB_MANAGED_TEMPLATE[$FIELD_API]} (curated OOTB prompt, unchanged)"
666
+ elif [[ -n "$OVERRIDE_SOURCE" ]]; then
667
+ echo " Template : override of $OVERRIDE_SOURCE (new active version; OOTB prompt customized)"
668
+ else
669
+ echo " Template : $DEV_NAME (active)"
670
+ fi
671
+ if [[ "$SKIP_FLOW" != "true" ]]; then
672
+ echo " Flow : $FIELD_API registered in $FLOW_API_NAME OpportunityFields (idempotent)"
673
+ fi
674
+ echo ""
675
+ echo "Scheduled suggestions land in the AiGenActionItem object (async, ~2-3 min after the daily run)."
676
+ echo "To confirm the scheduled path (NOT the synchronous test above):"
677
+ echo " 1. Baseline: sf data query -q \"SELECT COUNT() FROM AiGenActionItem WHERE CreatedDate=TODAY\" --target-org $ORG_ALIAS"
678
+ echo " 2. Wait >= 240s after the flow's scheduled run (WhatId is not SOQL-queryable — use the COUNT delta)."
679
+ echo " 3. Re-count and compare. New rows = suggestions generated."