@salesforce/afv-skills 1.47.0 → 1.49.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 (160) hide show
  1. package/package.json +1 -1
  2. package/skills/education-cloud-domain-configure/SKILL.md +234 -0
  3. package/skills/education-cloud-domain-configure/references/dependent-features.md +198 -0
  4. package/skills/education-cloud-domain-configure/references/domain-features/academic-operations.md +386 -0
  5. package/skills/education-cloud-domain-configure/references/domain-features/alumni-relations.md +155 -0
  6. package/skills/education-cloud-domain-configure/references/domain-features/cross-domain-dependencies.md +18 -0
  7. package/skills/education-cloud-domain-configure/references/domain-features/industriessettings-toggle-reference.md +43 -0
  8. package/skills/education-cloud-domain-configure/references/domain-features/mentoring.md +198 -0
  9. package/skills/education-cloud-domain-configure/references/domain-features/recruitment-admissions.md +315 -0
  10. package/skills/education-cloud-domain-configure/references/domain-features/student-management.md +155 -0
  11. package/skills/education-cloud-domain-configure/references/domain-features/student-success.md +249 -0
  12. package/skills/education-cloud-domain-configure/references/domain-features.md +27 -0
  13. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +23 -0
  14. package/skills/education-cloud-domain-configure/references/toggle-write-path.md +67 -0
  15. package/skills/education-cloud-domain-configure/scripts/check_license_provisioned.py +62 -0
  16. package/skills/education-cloud-domain-configure/scripts/check_org_edition.py +44 -0
  17. package/skills/education-cloud-domain-configure/scripts/check_permission_assigned.py +39 -0
  18. package/skills/education-cloud-multi-campus-configure/SKILL.md +2 -0
  19. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +4 -3
  20. package/skills/service-agentforce-channel-configure/SKILL.md +33 -28
  21. package/skills/service-agentforce-human-escalation-configure/SKILL.md +205 -0
  22. package/skills/service-agentforce-human-escalation-configure/assets/escalation-thresholds.instructions.md +37 -0
  23. package/skills/service-agentforce-human-escalation-configure/assets/force-app/main/default/flows/Human_Escalation_Outbound_Flow.flow-meta.xml +173 -0
  24. package/skills/service-agentforce-human-escalation-configure/assets/package.xml +8 -0
  25. package/skills/service-agentforce-human-escalation-configure/assets/sfdx-project.json +5 -0
  26. package/skills/service-agentforce-human-escalation-configure/references/classifier-contracts.md +103 -0
  27. package/skills/service-agentforce-human-escalation-configure/references/escalation-mechanism.md +63 -0
  28. package/skills/service-agentforce-human-escalation-configure/references/failure-policy.md +44 -0
  29. package/skills/service-agentforce-human-escalation-configure/references/nga-escalation.md +57 -0
  30. package/skills/service-agentforce-human-escalation-configure/references/runtime-verification.md +72 -0
  31. package/skills/service-agentforce-human-escalation-configure/scripts/classify-agent-active.mjs +49 -0
  32. package/skills/service-agentforce-human-escalation-configure/scripts/classify-nga-escalation.mjs +166 -0
  33. package/skills/service-agentforce-human-escalation-configure/scripts/extract-outbound-route.mjs +67 -0
  34. package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs +131 -0
  35. package/skills/service-agentforce-human-escalation-configure/scripts/tests/__init__.py +0 -0
  36. package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +249 -0
  37. package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +899 -0
  38. package/skills/service-agentforce-human-escalation-configure/scripts/verify-and-configure.sh +705 -0
  39. package/skills/service-agentforce-human-escalation-configure/scripts/verify-escalation-config.mjs +89 -0
  40. package/skills/service-de-channel-routing-configure/SKILL.md +13 -5
  41. package/skills/service-de-channel-routing-configure/scripts/tests/test_skill_contract.py +25 -0
  42. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +23 -10
  43. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -48
  44. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +6 -2
  45. package/skills/service-omni-agent-users-create/SKILL.md +96 -0
  46. package/skills/service-omni-agent-users-create/assets/create-users.apex.template +80 -0
  47. package/skills/service-omni-agent-users-create/references/apex-patterns.md +135 -0
  48. package/skills/service-omni-agent-users-create/references/apex-template-notes.md +40 -0
  49. package/skills/service-omni-agent-users-create/scripts/detect-and-create.sh +206 -0
  50. package/skills/service-omni-agent-users-create/scripts/detect-existing.sh +122 -0
  51. package/skills/service-omni-agent-users-create/scripts/run-create.sh +298 -0
  52. package/skills/service-omni-agent-users-create/scripts/tests/test_user_create_security.py +171 -0
  53. package/skills/service-omni-agent-work-sharing-configure/SKILL.md +78 -0
  54. package/skills/service-omni-agent-work-sharing-configure/references/api-notes.md +31 -0
  55. package/skills/service-omni-agent-work-sharing-configure/scripts/configure-and-report.sh +186 -0
  56. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/_bootstrap.py +120 -0
  57. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/test_agent_work_sharing_contracts.py +93 -0
  58. package/skills/service-omni-base-settings-configure/SKILL.md +77 -0
  59. package/skills/service-omni-base-settings-configure/assets/force-app/main/default/settings/OmniChannel.settings-meta.xml +8 -0
  60. package/skills/service-omni-base-settings-configure/assets/package.xml +8 -0
  61. package/skills/service-omni-base-settings-configure/assets/sfdx-project.json +12 -0
  62. package/skills/service-omni-base-settings-configure/references/api-notes.md +121 -0
  63. package/skills/service-omni-base-settings-configure/scripts/configure-and-report.sh +164 -0
  64. package/skills/service-omni-base-settings-configure/scripts/detect-existing.sh +69 -0
  65. package/skills/service-omni-channel-setup-coordinate/SKILL.md +168 -0
  66. package/skills/service-omni-channel-setup-coordinate/examples/output-templates.md +161 -0
  67. package/skills/service-omni-channel-setup-coordinate/references/gap-catalog.md +99 -0
  68. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +1542 -0
  69. package/skills/service-omni-channel-setup-coordinate/scripts/tests/__init__.py +0 -0
  70. package/skills/service-omni-channel-setup-coordinate/scripts/tests/_bootstrap.py +223 -0
  71. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +804 -0
  72. package/skills/service-omni-command-center-analyze/SKILL.md +84 -0
  73. package/skills/service-omni-command-center-analyze/references/api-notes.md +54 -0
  74. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +198 -0
  75. package/skills/service-omni-command-center-analyze/scripts/tests/_bootstrap.py +115 -0
  76. package/skills/service-omni-command-center-analyze/scripts/tests/test_command_center_analyze_contracts.py +165 -0
  77. package/skills/service-omni-permission-set-assign/SKILL.md +88 -0
  78. package/skills/service-omni-permission-set-assign/assets/package.xml +8 -0
  79. package/skills/service-omni-permission-set-assign/references/api-notes.md +143 -0
  80. package/skills/service-omni-permission-set-assign/scripts/verify-and-assign.sh +397 -0
  81. package/skills/service-omni-presence-status-deploy/SKILL.md +87 -0
  82. package/skills/service-omni-presence-status-deploy/assets/package.xml +12 -0
  83. package/skills/service-omni-presence-status-deploy/references/api-notes.md +115 -0
  84. package/skills/service-omni-presence-status-deploy/scripts/deploy-and-report.sh +422 -0
  85. package/skills/service-omni-presence-user-config-deploy/SKILL.md +80 -0
  86. package/skills/service-omni-presence-user-config-deploy/references/api-notes.md +60 -0
  87. package/skills/service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh +261 -0
  88. package/skills/service-omni-presence-user-config-deploy/scripts/tests/_bootstrap.py +106 -0
  89. package/skills/service-omni-presence-user-config-deploy/scripts/tests/test_presence_user_config_contracts.py +142 -0
  90. package/skills/service-omni-queue-deploy/SKILL.md +91 -0
  91. package/skills/service-omni-queue-deploy/references/api-notes.md +140 -0
  92. package/skills/service-omni-queue-deploy/scripts/verify-and-align.sh +470 -0
  93. package/skills/service-omni-queue-members-assign/SKILL.md +93 -0
  94. package/skills/service-omni-queue-members-assign/references/api-notes.md +144 -0
  95. package/skills/service-omni-queue-members-assign/scripts/tests/_bootstrap.py +67 -0
  96. package/skills/service-omni-queue-members-assign/scripts/tests/test_queue_members_contracts.py +73 -0
  97. package/skills/service-omni-queue-members-assign/scripts/verify-and-bind.sh +362 -0
  98. package/skills/service-omni-queue-routing-config-deploy/SKILL.md +101 -0
  99. package/skills/service-omni-queue-routing-config-deploy/references/api-notes.md +127 -0
  100. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/_bootstrap.py +90 -0
  101. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/test_queue_routing_config_contracts.py +70 -0
  102. package/skills/service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh +538 -0
  103. package/skills/service-omni-routing-flow-deploy/SKILL.md +111 -0
  104. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml +94 -0
  105. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.flow-meta.xml +88 -0
  106. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Cases.flow-meta.xml +144 -0
  107. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml +94 -0
  108. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.flow-meta.xml +88 -0
  109. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCalls.flow-meta.xml +144 -0
  110. package/skills/service-omni-routing-flow-deploy/assets/package.xml +11 -0
  111. package/skills/service-omni-routing-flow-deploy/assets/sfdx-project.json +11 -0
  112. package/skills/service-omni-routing-flow-deploy/references/api-notes.md +265 -0
  113. package/skills/service-omni-routing-flow-deploy/scripts/deploy-and-report.sh +684 -0
  114. package/skills/service-omni-service-channel-configure/SKILL.md +83 -0
  115. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Cases.serviceChannel-meta.xml +9 -0
  116. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Incidents.serviceChannel-meta.xml +9 -0
  117. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/MessagingSessions.serviceChannel-meta.xml +9 -0
  118. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/VoiceCalls.serviceChannel-meta.xml +9 -0
  119. package/skills/service-omni-service-channel-configure/assets/package.xml +11 -0
  120. package/skills/service-omni-service-channel-configure/assets/sfdx-project.json +12 -0
  121. package/skills/service-omni-service-channel-configure/references/api-notes.md +84 -0
  122. package/skills/service-omni-service-channel-configure/scripts/deploy-and-report.sh +383 -0
  123. package/skills/service-omni-sidebar-configure/SKILL.md +77 -0
  124. package/skills/service-omni-sidebar-configure/references/api-notes.md +51 -0
  125. package/skills/service-omni-sidebar-configure/scripts/enable-and-report.sh +315 -0
  126. package/skills/service-omni-sidebar-configure/scripts/tests/_bootstrap.py +142 -0
  127. package/skills/service-omni-sidebar-configure/scripts/tests/test_sidebar_contracts.py +139 -0
  128. package/skills/service-omni-skills-based-routing-configure/SKILL.md +81 -0
  129. package/skills/service-omni-skills-based-routing-configure/references/api-notes.md +50 -0
  130. package/skills/service-omni-skills-based-routing-configure/scripts/configure-and-report.sh +270 -0
  131. package/skills/service-omni-skills-based-routing-configure/scripts/tests/_bootstrap.py +120 -0
  132. package/skills/service-omni-skills-based-routing-configure/scripts/tests/test_sbr_contracts.py +129 -0
  133. package/skills/service-omni-supervisor-config-deploy/SKILL.md +97 -0
  134. package/skills/service-omni-supervisor-config-deploy/assets/force-app/main/default/omniSupervisorConfigs/Omni_Supervisor.omniSupervisorConfig-meta.xml +9 -0
  135. package/skills/service-omni-supervisor-config-deploy/assets/package.xml +8 -0
  136. package/skills/service-omni-supervisor-config-deploy/assets/sfdx-project.json +12 -0
  137. package/skills/service-omni-supervisor-config-deploy/references/api-notes.md +205 -0
  138. package/skills/service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh +586 -0
  139. package/skills/service-omni-supervisor-config-deploy/scripts/tests/_bootstrap.py +153 -0
  140. package/skills/service-omni-supervisor-config-deploy/scripts/tests/test_supervisor_config_contracts.py +116 -0
  141. package/skills/service-omni-supervisor-permset-assign/SKILL.md +88 -0
  142. package/skills/service-omni-supervisor-permset-assign/references/api-notes.md +48 -0
  143. package/skills/service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh +316 -0
  144. package/skills/service-omni-supervisor-surface-deploy/SKILL.md +85 -0
  145. package/skills/service-omni-supervisor-surface-deploy/references/api-notes.md +47 -0
  146. package/skills/service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh +216 -0
  147. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/_bootstrap.py +95 -0
  148. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/test_supervisor_surface_contracts.py +154 -0
  149. package/skills/service-omni-supervisor-users-create/SKILL.md +92 -0
  150. package/skills/service-omni-supervisor-users-create/assets/create-supervisors.apex.template +72 -0
  151. package/skills/service-omni-supervisor-users-create/references/apex-patterns.md +73 -0
  152. package/skills/service-omni-supervisor-users-create/references/apex-template-notes.md +41 -0
  153. package/skills/service-omni-supervisor-users-create/scripts/detect-and-create.sh +197 -0
  154. package/skills/service-omni-supervisor-users-create/scripts/detect-existing.sh +112 -0
  155. package/skills/service-omni-supervisor-users-create/scripts/run-create.sh +296 -0
  156. package/skills/service-omni-work-skill-routing-configure/SKILL.md +80 -0
  157. package/skills/service-omni-work-skill-routing-configure/references/api-notes.md +67 -0
  158. package/skills/service-omni-work-skill-routing-configure/scripts/configure-and-report.sh +262 -0
  159. package/skills/service-omni-work-skill-routing-configure/scripts/tests/_bootstrap.py +121 -0
  160. package/skills/service-omni-work-skill-routing-configure/scripts/tests/test_work_skill_routing_contracts.py +161 -0
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bash
2
+ # deploy-and-report.sh - populate the Omni Supervisor action + tab surface on an existing OmniSupervisorConfig by inserting OmniSupervisorConfigAction / OmniSupervisorConfigTab rows via the Data API. Idempotent (inserts only the missing types). Args, env, exit codes: SKILL.md; type vocabulary + reference-only exclusions: references/api-notes.md.
3
+
4
+ set -euo pipefail
5
+
6
+ if [ $# -lt 1 ]; then
7
+ echo '{"error":"Usage: bash deploy-and-report.sh <org-alias> [config_developer_name]"}' >&2
8
+ exit 1
9
+ fi
10
+
11
+ ORG="$1"
12
+ CONFIG_DN="${2:-}"
13
+
14
+ # Reference-free standard types (safe to insert as bare companion rows).
15
+ DEFAULT_ACTIONS="AllAgents.ChangeQueues,AllAgents.ChangeSkills,AllAgents.ChangeGroups,AllAgents.AssignLearning,QueuesBacklog.ManageQueues"
16
+ DEFAULT_TABS="Wallboard,Agents,QueuesBacklog,AssignedWork,SkillsBacklog"
17
+ SAFE_ACTIONS="AllAgents.ChangeQueues AllAgents.ChangeSkills AllAgents.ChangeGroups AllAgents.AssignLearning QueuesBacklog.ManageQueues"
18
+ SAFE_TABS="Wallboard Agents QueuesBacklog AssignedWork SkillsBacklog Reports Alerts"
19
+
20
+ ACTIONS_CSV="${SUPERVISOR_ACTIONS:-$DEFAULT_ACTIONS}"
21
+ TABS_CSV="${SUPERVISOR_TABS:-$DEFAULT_TABS}"
22
+
23
+ if [ -n "$CONFIG_DN" ] && ! [[ "$CONFIG_DN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
24
+ echo "{\"error\":\"Invalid config_developer_name '$CONFIG_DN'. Must start with a letter and contain only letters, digits, underscores (max 80).\"}" >&2
25
+ exit 1
26
+ fi
27
+
28
+ if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
29
+ echo "{\"error\":\"Org alias '$ORG' is not authenticated. Run: sf org login web -a $ORG -r <my-domain-url>\"}" >&2
30
+ exit 1
31
+ fi
32
+
33
+ emit_blocked() {
34
+ local msg="$1"; local target_skill="${2:-}"
35
+ local manual_actions='[]'
36
+ if [ -n "$target_skill" ]; then
37
+ manual_actions=$(jq -n --arg ts "$target_skill" '[{id:"MISSING_PREREQ",title:"Run prerequisite skill",target_skill:$ts}]')
38
+ fi
39
+ EMITTED=1
40
+ jq -n --arg msg "$msg" --arg cdn "${CONFIG_DN:-}" --argjson ma "$manual_actions" \
41
+ '{skill:"service-omni-supervisor-surface-deploy",status:"blocked",
42
+ config:{developer_name:(if $cdn=="" then null else $cdn end),id:null},
43
+ actions:{requested:[],created:[],reused:[],count:0},
44
+ tabs:{requested:[],created:[],reused:[],count:0},
45
+ manual_actions:$ma,blocking_issue:$msg}'
46
+ exit 1
47
+ }
48
+
49
+ # Fail-safe: under `set -euo pipefail` an unguarded jq/sf failure could otherwise kill the
50
+ # script with empty stdout. Emit a structured blocked JSON on any exit before a real result so
51
+ # the coordinator never parses empty output. EMITTED flips to 1 before each intentional emit.
52
+ EMITTED=0
53
+ on_exit() {
54
+ if [ "$EMITTED" != "1" ]; then
55
+ jq -n --arg cdn "${CONFIG_DN:-}" \
56
+ '{skill:"service-omni-supervisor-surface-deploy",status:"blocked",
57
+ config:{developer_name:(if $cdn=="" then null else $cdn end),id:null},
58
+ actions:{requested:[],created:[],reused:[],count:0},
59
+ tabs:{requested:[],created:[],reused:[],count:0},
60
+ manual_actions:[],
61
+ blocking_issue:"Supervisor surface deploy aborted before completing (unexpected error; see stderr). Re-run."}'
62
+ fi
63
+ }
64
+ trap on_exit EXIT
65
+
66
+ # Split a CSV into a space list, trimming blanks.
67
+ csv_to_items() { printf '%s' "$1" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$' || true; }
68
+
69
+ # Reject types that need an external reference (custom action, FlexiPage, AWS, AI) - a bare
70
+ # companion row cannot point at them (only OmniSupervisorConfigId/Type/DisplayOrder are createable).
71
+ requires_reference() {
72
+ case "$1" in
73
+ *CustomAction|*AWSDashboard|FlexipageType|AIAgents|AgentforceSDR) return 0 ;;
74
+ *) return 1 ;;
75
+ esac
76
+ }
77
+ in_list() { local needle="$1"; shift; local x; for x in $*; do [ "$x" = "$needle" ] && return 0; done; return 1; }
78
+
79
+ # ---- validate requested types up front (reject before any write) ----
80
+ REQ_ACTIONS=(); while IFS= read -r a; do REQ_ACTIONS+=("$a"); done < <(csv_to_items "$ACTIONS_CSV")
81
+ REQ_TABS=(); while IFS= read -r t; do REQ_TABS+=("$t"); done < <(csv_to_items "$TABS_CSV")
82
+
83
+ for a in "${REQ_ACTIONS[@]:-}"; do
84
+ [ -z "$a" ] && continue
85
+ if requires_reference "$a"; then
86
+ emit_blocked "Action type '$a' needs an external reference (custom action / AWS dashboard) that a bare OmniSupervisorConfigAction row cannot carry; configure it in Setup. Supported reference-free types: $SAFE_ACTIONS."
87
+ fi
88
+ in_list "$a" $SAFE_ACTIONS || emit_blocked "Unsupported action type '$a'. Supported reference-free types: $SAFE_ACTIONS."
89
+ done
90
+ for t in "${REQ_TABS[@]:-}"; do
91
+ [ -z "$t" ] && continue
92
+ if requires_reference "$t"; then
93
+ emit_blocked "Tab type '$t' needs an external reference (FlexiPage / AI agent) that a bare OmniSupervisorConfigTab row cannot carry; configure it in Setup. Supported reference-free types: $SAFE_TABS."
94
+ fi
95
+ in_list "$t" $SAFE_TABS || emit_blocked "Unsupported tab type '$t'. Supported reference-free types: $SAFE_TABS."
96
+ done
97
+
98
+ # Step 1 - safe_to_write guard
99
+ ORG_JSON=$(sf data query --target-org "$ORG" \
100
+ --query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" --json 2>/dev/null) \
101
+ || emit_blocked "Failed to query Organization for the safe_to_write guard."
102
+ IS_SANDBOX=$(echo "$ORG_JSON" | jq -r '.result.records[0].IsSandbox')
103
+ TRIAL_EXP=$(echo "$ORG_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
104
+ ORG_TYPE=$(echo "$ORG_JSON" | jq -r '.result.records[0].OrganizationType')
105
+ SAFE_TO_WRITE=false
106
+ if [ "$IS_SANDBOX" = "true" ] || [ "$TRIAL_EXP" != "null" ] \
107
+ || [ "$ORG_TYPE" = "Developer Edition" ] || [ "$ORG_TYPE" = "Base Edition" ]; then
108
+ SAFE_TO_WRITE=true
109
+ fi
110
+ [ "$SAFE_TO_WRITE" = "true" ] || emit_blocked "Refusing to write the supervisor surface to a real production customer org (IsSandbox=$IS_SANDBOX, TrialExpirationDate=$TRIAL_EXP, OrganizationType=$ORG_TYPE). Target a sandbox, trial CDO, or Developer Edition org."
111
+
112
+ # Step 2 - resolve the OmniSupervisorConfig (by DeveloperName, else the org's sole config)
113
+ if [ -n "$CONFIG_DN" ]; then
114
+ CFG_JSON=$(sf data query --target-org "$ORG" --json \
115
+ --query "SELECT Id, DeveloperName FROM OmniSupervisorConfig WHERE DeveloperName='$CONFIG_DN'" 2>/dev/null || echo '{}')
116
+ else
117
+ CFG_JSON=$(sf data query --target-org "$ORG" --json \
118
+ --query "SELECT Id, DeveloperName FROM OmniSupervisorConfig ORDER BY CreatedDate LIMIT 2" 2>/dev/null || echo '{}')
119
+ fi
120
+ CFG_COUNT=$(echo "$CFG_JSON" | jq -r '.result.records | length' 2>/dev/null || echo 0)
121
+ if [ "${CFG_COUNT:-0}" -eq 0 ]; then
122
+ emit_blocked "No OmniSupervisorConfig found${CONFIG_DN:+ named '$CONFIG_DN'} on this org. Deploy it first." "service-omni-supervisor-config-deploy"
123
+ fi
124
+ if [ -z "$CONFIG_DN" ] && [ "$CFG_COUNT" -gt 1 ]; then
125
+ NAMES=$(echo "$CFG_JSON" | jq -r '[.result.records[].DeveloperName] | join(", ")')
126
+ emit_blocked "Multiple OmniSupervisorConfigs exist ($NAMES); pass one as the config_developer_name argument."
127
+ fi
128
+ CONFIG_ID=$(echo "$CFG_JSON" | jq -r '.result.records[0].Id')
129
+ CONFIG_DN=$(echo "$CFG_JSON" | jq -r '.result.records[0].DeveloperName')
130
+
131
+ # Step 3 - read existing surface rows (idempotency baseline). A failed/inconclusive read must
132
+ # NOT fall through to an empty baseline, or we would re-insert rows that already exist. Require a
133
+ # definitive .result.records array; otherwise block.
134
+ EX_ACTIONS_JSON=$(sf data query --target-org "$ORG" --json \
135
+ --query "SELECT OmniSupervisorActionType, DisplayOrder FROM OmniSupervisorConfigAction WHERE OmniSupervisorConfigId='$CONFIG_ID'" 2>/dev/null) \
136
+ || emit_blocked "Failed to read existing OmniSupervisorConfigAction rows (inconclusive; refusing to insert to avoid duplicates)."
137
+ echo "$EX_ACTIONS_JSON" | jq -e '.result.records' >/dev/null 2>&1 \
138
+ || emit_blocked "OmniSupervisorConfigAction baseline read returned no records array (inconclusive; refusing to insert to avoid duplicates)."
139
+ EX_TABS_JSON=$(sf data query --target-org "$ORG" --json \
140
+ --query "SELECT OmniSupervisorTabType, DisplayOrder FROM OmniSupervisorConfigTab WHERE OmniSupervisorConfigId='$CONFIG_ID'" 2>/dev/null) \
141
+ || emit_blocked "Failed to read existing OmniSupervisorConfigTab rows (inconclusive; refusing to insert to avoid duplicates)."
142
+ echo "$EX_TABS_JSON" | jq -e '.result.records' >/dev/null 2>&1 \
143
+ || emit_blocked "OmniSupervisorConfigTab baseline read returned no records array (inconclusive; refusing to insert to avoid duplicates)."
144
+ EXIST_ACTIONS=$(echo "$EX_ACTIONS_JSON" | jq -r '[.result.records[]?.OmniSupervisorActionType] | @json' 2>/dev/null || echo '[]')
145
+ EXIST_TABS=$(echo "$EX_TABS_JSON" | jq -r '[.result.records[]?.OmniSupervisorTabType] | @json' 2>/dev/null || echo '[]')
146
+ MAX_ACTION_ORDER=$(echo "$EX_ACTIONS_JSON" | jq -r '[.result.records[]?.DisplayOrder // 0] | max // 0' 2>/dev/null || echo 0)
147
+ MAX_TAB_ORDER=$(echo "$EX_TABS_JSON" | jq -r '[.result.records[]?.DisplayOrder // 0] | max // 0' 2>/dev/null || echo 0)
148
+
149
+ has_existing() { echo "$1" | jq -e --arg v "$2" 'index($v) != null' >/dev/null 2>&1; }
150
+
151
+ # PLAN_ONLY - report intent, no write.
152
+ if [ "${PLAN_ONLY:-}" = "1" ]; then
153
+ EMITTED=1
154
+ jq -n --arg cdn "$CONFIG_DN" --arg cid "$CONFIG_ID" \
155
+ --argjson ra "$(printf '%s\n' "${REQ_ACTIONS[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \
156
+ --argjson rt "$(printf '%s\n' "${REQ_TABS[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \
157
+ --argjson ea "$EXIST_ACTIONS" --argjson et "$EXIST_TABS" \
158
+ '{skill:"service-omni-supervisor-surface-deploy",status:"action_needed",plan_mode:true,
159
+ config:{developer_name:$cdn,id:$cid},
160
+ plan_detail:("Would ensure \($ra|length) action type(s) and \($rt|length) tab type(s) on OmniSupervisorConfig \($cdn); existing actions=\($ea|length), tabs=\($et|length). Inserts only the missing types."),
161
+ actions:{requested:$ra,created:[],reused:$ea,count:($ea|length)},
162
+ tabs:{requested:$rt,created:[],reused:$et,count:($et|length)},
163
+ manual_actions:[],blocking_issue:null}'
164
+ exit 0
165
+ fi
166
+
167
+ # Step 4 - insert missing action rows
168
+ CREATED_ACTIONS="[]"; REUSED_ACTIONS="[]"; ORDER=$MAX_ACTION_ORDER
169
+ for a in "${REQ_ACTIONS[@]:-}"; do
170
+ [ -z "$a" ] && continue
171
+ if has_existing "$EXIST_ACTIONS" "$a"; then
172
+ REUSED_ACTIONS=$(echo "$REUSED_ACTIONS" | jq --arg v "$a" '. + [$v]')
173
+ continue
174
+ fi
175
+ ORDER=$((ORDER + 1))
176
+ RES=$(sf data create record --target-org "$ORG" --sobject OmniSupervisorConfigAction \
177
+ --values "OmniSupervisorConfigId=$CONFIG_ID OmniSupervisorActionType=$a DisplayOrder=$ORDER" --json 2>/dev/null || true)
178
+ OKID=$(echo "$RES" | jq -r '.result.id // ""' 2>/dev/null)
179
+ if [ -z "$OKID" ] || [ "$OKID" = "null" ]; then
180
+ MSG=$(echo "$RES" | jq -r '.message // "unknown insert error"' 2>/dev/null | tr -d '\n' | head -c 300)
181
+ emit_blocked "Failed to insert OmniSupervisorConfigAction '$a': $MSG"
182
+ fi
183
+ CREATED_ACTIONS=$(echo "$CREATED_ACTIONS" | jq --arg v "$a" '. + [$v]')
184
+ done
185
+
186
+ # Step 5 - insert missing tab rows
187
+ CREATED_TABS="[]"; REUSED_TABS="[]"; ORDER=$MAX_TAB_ORDER
188
+ for t in "${REQ_TABS[@]:-}"; do
189
+ [ -z "$t" ] && continue
190
+ if has_existing "$EXIST_TABS" "$t"; then
191
+ REUSED_TABS=$(echo "$REUSED_TABS" | jq --arg v "$t" '. + [$v]')
192
+ continue
193
+ fi
194
+ ORDER=$((ORDER + 1))
195
+ RES=$(sf data create record --target-org "$ORG" --sobject OmniSupervisorConfigTab \
196
+ --values "OmniSupervisorConfigId=$CONFIG_ID OmniSupervisorTabType=$t DisplayOrder=$ORDER" --json 2>/dev/null || true)
197
+ OKID=$(echo "$RES" | jq -r '.result.id // ""' 2>/dev/null)
198
+ if [ -z "$OKID" ] || [ "$OKID" = "null" ]; then
199
+ MSG=$(echo "$RES" | jq -r '.message // "unknown insert error"' 2>/dev/null | tr -d '\n' | head -c 300)
200
+ emit_blocked "Failed to insert OmniSupervisorConfigTab '$t': $MSG"
201
+ fi
202
+ CREATED_TABS=$(echo "$CREATED_TABS" | jq --arg v "$t" '. + [$v]')
203
+ done
204
+
205
+ CREATED_TOTAL=$(( $(echo "$CREATED_ACTIONS" | jq 'length') + $(echo "$CREATED_TABS" | jq 'length') ))
206
+ STATUS="reused"; [ "$CREATED_TOTAL" -gt 0 ] && STATUS="created"
207
+
208
+ EMITTED=1
209
+ jq -n --arg status "$STATUS" --arg cdn "$CONFIG_DN" --arg cid "$CONFIG_ID" \
210
+ --argjson ca "$CREATED_ACTIONS" --argjson rua "$REUSED_ACTIONS" \
211
+ --argjson ct "$CREATED_TABS" --argjson rut "$REUSED_TABS" \
212
+ '{skill:"service-omni-supervisor-surface-deploy",status:$status,
213
+ config:{developer_name:$cdn,id:$cid},
214
+ actions:{requested:(($ca+$rua)|sort),created:$ca,reused:$rua,count:(($ca+$rua)|length)},
215
+ tabs:{requested:(($ct+$rut)|sort),created:$ct,reused:$rut,count:(($ct+$rut)|length)},
216
+ manual_actions:[],blocking_issue:null}'
@@ -0,0 +1,95 @@
1
+ """Shared paths + fake-`sf` harness for the supervisor-surface-deploy contract suite.
2
+
3
+ The fake serves the org guard query, config resolution, existing-surface reads, and record
4
+ inserts from env tunables. Org-free and network-free.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import subprocess
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ SKILLS_ROOT = Path(__file__).resolve().parents[3]
14
+ SKILL_DIR = "service-omni-supervisor-surface-deploy"
15
+ SCRIPT = f"{SKILL_DIR}/scripts/deploy-and-report.sh"
16
+
17
+ # Tunables (env):
18
+ # FAKE_SANDBOX = true|false (safe_to_write guard; default true)
19
+ # FAKE_CFG_COUNT = 0|1|2 (OmniSupervisorConfig rows; default 1)
20
+ # FAKE_EXIST_ACTIONS = JSON array of types (existing OmniSupervisorConfigAction; default [])
21
+ # FAKE_EXIST_TABS = JSON array of types (existing OmniSupervisorConfigTab; default [])
22
+ # FAKE_CREATE_OK = true|false (record insert result; default true)
23
+ _FAKE_SF = r"""#!/usr/bin/env bash
24
+ sub="${1:-} ${2:-}"; args="$*"
25
+ SANDBOX="${FAKE_SANDBOX:-true}"
26
+ CFG_COUNT="${FAKE_CFG_COUNT:-1}"
27
+ EXIST_ACTIONS="${FAKE_EXIST_ACTIONS:-[]}"
28
+ EXIST_TABS="${FAKE_EXIST_TABS:-[]}"
29
+ CREATE_OK="${FAKE_CREATE_OK:-true}"
30
+
31
+ case "$sub" in
32
+ "org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
33
+ "data query")
34
+ if printf '%s' "$args" | grep -q "FROM Organization"; then
35
+ if [ "$SANDBOX" = "true" ]; then OT="Developer Edition"; else OT="Enterprise Edition"; fi
36
+ echo "{\"status\":0,\"result\":{\"records\":[{\"IsSandbox\":$SANDBOX,\"TrialExpirationDate\":null,\"OrganizationType\":\"$OT\"}]}}"
37
+ elif printf '%s' "$args" | grep -q "FROM OmniSupervisorConfig\b"; then
38
+ case "$CFG_COUNT" in
39
+ 0) echo '{"status":0,"result":{"records":[]}}';;
40
+ 2) echo '{"status":0,"result":{"records":[{"Id":"0Q2xx0000000001AAA","DeveloperName":"Omni_Supervisor"},{"Id":"0Q2xx0000000002AAA","DeveloperName":"Second_Config"}]}}';;
41
+ *) echo '{"status":0,"result":{"records":[{"Id":"0Q2xx0000000001AAA","DeveloperName":"Omni_Supervisor"}]}}';;
42
+ esac
43
+ elif printf '%s' "$args" | grep -q "FROM OmniSupervisorConfigAction"; then
44
+ echo "{\"status\":0,\"result\":{\"records\":$(printf '%s' "$EXIST_ACTIONS" | jq -c 'to_entries | map({OmniSupervisorActionType:.value, DisplayOrder:(.key+1)})')}}"
45
+ elif printf '%s' "$args" | grep -q "FROM OmniSupervisorConfigTab"; then
46
+ echo "{\"status\":0,\"result\":{\"records\":$(printf '%s' "$EXIST_TABS" | jq -c 'to_entries | map({OmniSupervisorTabType:.value, DisplayOrder:(.key+1)})')}}"
47
+ else
48
+ echo '{"status":0,"result":{"records":[]}}'
49
+ fi
50
+ exit 0;;
51
+ "data create")
52
+ if [ "$CREATE_OK" = "true" ]; then
53
+ echo '{"status":0,"result":{"id":"0Q3xx0000000001AAA","success":true}}'
54
+ else
55
+ echo '{"status":1,"name":"FIELD_INTEGRITY_EXCEPTION","message":"bad value for restricted picklist field"}'
56
+ fi
57
+ exit 0;;
58
+ *) echo '{"status":0,"result":{}}'; exit 0;;
59
+ esac
60
+ """
61
+
62
+
63
+ def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
64
+ env = dict(os.environ)
65
+ if scrub_sf:
66
+ kept = [d for d in env.get("PATH", "").split(os.pathsep)
67
+ if d and not os.path.exists(os.path.join(d, "sf"))]
68
+ env["PATH"] = os.pathsep.join(kept)
69
+ if path_prefix:
70
+ env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
71
+ if extra_env:
72
+ env.update({k: str(v) for k, v in extra_env.items()})
73
+ proc = subprocess.run(
74
+ ["bash", str(SKILLS_ROOT / SCRIPT), *args],
75
+ capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
76
+ )
77
+ return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
78
+
79
+
80
+ class FakeSf:
81
+ def __init__(self, body=_FAKE_SF):
82
+ self._body = body
83
+ self._tmp = None
84
+
85
+ def __enter__(self):
86
+ self._tmp = tempfile.mkdtemp(prefix="supsurface-fake-sf-")
87
+ sf = Path(self._tmp) / "sf"
88
+ sf.write_text(self._body)
89
+ sf.chmod(0o755)
90
+ return self._tmp
91
+
92
+ def __exit__(self, *exc):
93
+ import shutil
94
+ if self._tmp:
95
+ shutil.rmtree(self._tmp, ignore_errors=True)
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """Contract tests for service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
11
+ from _bootstrap import FakeSf, _FAKE_SF, run # noqa: E402
12
+
13
+
14
+ def _last_json(out: str) -> dict:
15
+ dec = json.JSONDecoder()
16
+ obj, i, n = None, 0, len(out)
17
+ while i < n:
18
+ if out[i] in "{[":
19
+ try:
20
+ cand, end = dec.raw_decode(out, i)
21
+ if isinstance(cand, dict):
22
+ obj = cand
23
+ i = end
24
+ continue
25
+ except json.JSONDecodeError:
26
+ pass
27
+ i += 1
28
+ assert obj is not None, f"no JSON object found in output:\n{out}"
29
+ return obj
30
+
31
+
32
+ def _detect(env=None, args=("org",)):
33
+ with FakeSf(_FAKE_SF) as p:
34
+ return run(list(args), path_prefix=p, extra_env=env or {})
35
+
36
+
37
+ class InputContractTests(unittest.TestCase):
38
+ def test_missing_args_rejected(self):
39
+ rc, out = run([], scrub_sf=True)
40
+ self.assertEqual(rc, 1, out)
41
+ self.assertIn("Usage", out)
42
+
43
+ def test_bad_config_name_rejected(self):
44
+ rc, out = run(["org", "1badname"], scrub_sf=True)
45
+ self.assertEqual(rc, 1, out)
46
+ self.assertIn("Invalid config_developer_name", out)
47
+
48
+ def test_reference_required_action_blocked(self):
49
+ rc, out = _detect({"SUPERVISOR_ACTIONS": "AllAgents.CustomAction"})
50
+ self.assertEqual(rc, 1, out)
51
+ v = _last_json(out)
52
+ self.assertEqual(v["status"], "blocked", out)
53
+ self.assertIn("external reference", v["blocking_issue"], out)
54
+
55
+ def test_reference_required_tab_blocked(self):
56
+ rc, out = _detect({"SUPERVISOR_TABS": "FlexipageType"})
57
+ self.assertEqual(rc, 1, out)
58
+ v = _last_json(out)
59
+ self.assertEqual(v["status"], "blocked", out)
60
+
61
+ def test_unsupported_type_blocked(self):
62
+ rc, out = _detect({"SUPERVISOR_TABS": "NotARealTab"})
63
+ self.assertEqual(rc, 1, out)
64
+ v = _last_json(out)
65
+ self.assertEqual(v["status"], "blocked", out)
66
+ self.assertIn("Unsupported tab type", v["blocking_issue"], out)
67
+
68
+
69
+ class SafetyAndResolutionTests(unittest.TestCase):
70
+ def test_production_org_blocked(self):
71
+ rc, out = _detect({"FAKE_SANDBOX": "false"})
72
+ self.assertEqual(rc, 1, out)
73
+ v = _last_json(out)
74
+ self.assertEqual(v["status"], "blocked", out)
75
+ self.assertIn("production", v["blocking_issue"], out)
76
+
77
+ def test_no_config_blocks_with_pointer(self):
78
+ rc, out = _detect({"FAKE_CFG_COUNT": "0"})
79
+ self.assertEqual(rc, 1, out)
80
+ v = _last_json(out)
81
+ self.assertEqual(v["status"], "blocked", out)
82
+ self.assertTrue(any(a.get("target_skill") == "service-omni-supervisor-config-deploy"
83
+ for a in v["manual_actions"]), out)
84
+
85
+ def test_multiple_configs_without_name_blocks(self):
86
+ rc, out = _detect({"FAKE_CFG_COUNT": "2"})
87
+ self.assertEqual(rc, 1, out)
88
+ v = _last_json(out)
89
+ self.assertEqual(v["status"], "blocked", out)
90
+ self.assertIn("Multiple", v["blocking_issue"], out)
91
+
92
+
93
+ class StateModelTests(unittest.TestCase):
94
+ def test_created_on_empty_surface(self):
95
+ rc, out = _detect({"FAKE_EXIST_ACTIONS": "[]", "FAKE_EXIST_TABS": "[]"})
96
+ self.assertEqual(rc, 0, out)
97
+ v = _last_json(out)
98
+ self.assertEqual(v["status"], "created", out)
99
+ self.assertEqual(len(v["actions"]["created"]), 5, out)
100
+ self.assertEqual(len(v["tabs"]["created"]), 5, out)
101
+ self.assertEqual(v["actions"]["reused"], [], out)
102
+
103
+ def test_reused_when_all_present(self):
104
+ acts = json.dumps(["AllAgents.ChangeQueues", "AllAgents.ChangeSkills",
105
+ "AllAgents.ChangeGroups", "AllAgents.AssignLearning",
106
+ "QueuesBacklog.ManageQueues"])
107
+ tabs = json.dumps(["Wallboard", "Agents", "QueuesBacklog", "AssignedWork", "SkillsBacklog"])
108
+ rc, out = _detect({"FAKE_EXIST_ACTIONS": acts, "FAKE_EXIST_TABS": tabs})
109
+ self.assertEqual(rc, 0, out)
110
+ v = _last_json(out)
111
+ self.assertEqual(v["status"], "reused", out)
112
+ self.assertEqual(v["actions"]["created"], [], out)
113
+ self.assertEqual(v["tabs"]["created"], [], out)
114
+
115
+ def test_partial_diff_inserts_only_missing(self):
116
+ rc, out = _detect({
117
+ "FAKE_EXIST_ACTIONS": json.dumps(["AllAgents.ChangeQueues"]),
118
+ "FAKE_EXIST_TABS": json.dumps(["Wallboard"]),
119
+ })
120
+ self.assertEqual(rc, 0, out)
121
+ v = _last_json(out)
122
+ self.assertEqual(v["status"], "created", out)
123
+ self.assertIn("AllAgents.ChangeQueues", v["actions"]["reused"], out)
124
+ self.assertNotIn("AllAgents.ChangeQueues", v["actions"]["created"], out)
125
+ self.assertEqual(len(v["actions"]["created"]), 4, out)
126
+ self.assertEqual(len(v["tabs"]["created"]), 4, out)
127
+
128
+ def test_plan_only_makes_no_writes(self):
129
+ rc, out = _detect({"PLAN_ONLY": "1"})
130
+ self.assertEqual(rc, 0, out)
131
+ v = _last_json(out)
132
+ self.assertEqual(v["status"], "action_needed", out)
133
+ self.assertTrue(v["plan_mode"], out)
134
+ self.assertEqual(v["actions"]["created"], [], out)
135
+
136
+ def test_insert_failure_blocks(self):
137
+ rc, out = _detect({"FAKE_CREATE_OK": "false"})
138
+ self.assertEqual(rc, 1, out)
139
+ v = _last_json(out)
140
+ self.assertEqual(v["status"], "blocked", out)
141
+ self.assertIn("Failed to insert", v["blocking_issue"], out)
142
+
143
+ def test_custom_tab_set_honored(self):
144
+ rc, out = _detect({"SUPERVISOR_TABS": "Wallboard,Reports,Alerts",
145
+ "SUPERVISOR_ACTIONS": "AllAgents.ChangeQueues"})
146
+ self.assertEqual(rc, 0, out)
147
+ v = _last_json(out)
148
+ self.assertEqual(v["status"], "created", out)
149
+ self.assertEqual(sorted(v["tabs"]["created"]), ["Alerts", "Reports", "Wallboard"], out)
150
+ self.assertEqual(v["actions"]["created"], ["AllAgents.ChangeQueues"], out)
151
+
152
+
153
+ if __name__ == "__main__":
154
+ unittest.main(verbosity=2)
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: service-omni-supervisor-users-create
3
+ description: "Use to create N Omni-Channel supervisor users on a Salesforce org via Anonymous Apex, using the supervisor{i}.<suffix>@example.com pattern with SOQL-based idempotency (re-runs skip existing usernames). Passwords are set via System.setPassword and handled fail-closed: the wrapper proves no active debug TraceFlag before setting a password, and otherwise leaves the user ACTIVE and reset_required. Triggers: create supervisor users, provision supervisor accounts, scaffold supervisor personas. Do not use on production customer orgs (blocked by the safe_to_write guard), to bind supervisors to OmniSupervisorConfig (service-omni-supervisor-config-deploy), or to assign the ContactCenterSupervisor permission set (service-omni-supervisor-permset-assign)."
4
+ allowed-tools: Bash Read Write Edit Glob Grep
5
+ metadata:
6
+ version: "1.0"
7
+ domains: ["Service"]
8
+ minApiVersion: "66.0"
9
+ relatedSkills:
10
+ - "service-omni-agent-users-create"
11
+ - "service-omni-supervisor-config-deploy"
12
+ - "service-omni-supervisor-permset-assign"
13
+ accessCheck:
14
+ - type: license
15
+ value: ServiceCloud
16
+ cliTools:
17
+ - tool: ["jq"]
18
+ semver: ">=1.6"
19
+ - tool: ["sf"]
20
+ semver: ">=2.139.6"
21
+ ---
22
+
23
+ # service-omni-supervisor-users-create
24
+
25
+ Create N supervisor users on a Salesforce org for the classic Omni-Channel Supervisor Configuration (`OmniSupervisorConfig`), which binds named user records via `OmniSupervisorConfigUser`. Users follow a deterministic `supervisor{i}.<suffix>@example.com` pattern so the coordinator can rediscover them across runs. It is the supervisor counterpart to `service-omni-agent-users-create` and shares its detection, password, and idempotency model; the only differences are the username/alias prefixes and the debug-log marker. Binding these users into a config (`service-omni-supervisor-config-deploy`) and granting them supervisor access (`service-omni-supervisor-permset-assign`) are separate leaves.
26
+
27
+ ## Inputs
28
+
29
+ Confirm once, up front:
30
+
31
+ - `org-alias` (required, no default) — must resolve via `sf org display`.
32
+ - Supervisor count (optional, default `1`, range `1..5`; the coordinator typically requests 1).
33
+ - Profile name (optional, default `Standard User`). The coordinator overrides this to a Service Cloud profile so supervisors consume Service Cloud licenses; supervisor access itself comes from the standard `ContactCenterSupervisor` permission set assigned later.
34
+
35
+ Usernames and passwords are never accepted from the operator — both are generated (usernames from the org suffix, passwords via Anonymous Apex).
36
+
37
+ ## Preconditions and safety
38
+
39
+ - Target org authenticated via `sf` CLI (My Domain URL, not `.lightning.force.com`), Service Cloud license present, `sf` CLI ≥ 2.139.6.
40
+ - The executing user has `PermissionsModifyAllData` and `PermissionsManagePasswordPolicies` (standard on System Administrator).
41
+ - **Production guardrail:** the detect script computes `safe_to_write` as `IsSandbox` OR `TrialExpirationDate != null` OR `OrganizationType` in {Developer Edition, Base Edition}, and the skill blocks with no override when it is false. CDOs, scratch orgs, and dev orgs are permitted.
42
+
43
+ **Password handling (fail-closed).** Passwords are set by Anonymous Apex `System.setPassword` (`sf user password generate` cannot target Apex-inserted users). The literal appears in the inline executeAnonymous debug log and, only when a debug-log TraceFlag is active for the running user, in a queryable `ApexLog`. The wrapper therefore fails closed *before* the first `System.setPassword`: it proves via a SOQL-filtered Tooling API query (`ExpirationDate > now`) that no active TraceFlag exists. If safety cannot be positively proven, it sets no password at all; the user is left ACTIVE, flagged `password_status:"reset_required"`, and a `security_warning` explains why. It never deletes logs. A user whose password could not be set is kept ACTIVE and flagged for reset — never deactivated.
44
+
45
+ ## Run
46
+
47
+ ```bash
48
+ # read-only preview (never writes)
49
+ bash scripts/detect-and-create.sh plan <org-alias> [count] [profile-name]
50
+ # detect, enforce safe_to_write, then insert only the missing supervisors
51
+ bash scripts/detect-and-create.sh run <org-alias> [count=1] [profile-name="Standard User"]
52
+ ```
53
+
54
+ `detect-and-create.sh` is the canonical entry point: it re-runs detection, enforces the production guard, and only then inserts. Do not call `scripts/run-create.sh` directly — it is internal and does not enforce the guard.
55
+
56
+ ## Behavior
57
+
58
+ **Detection.** The detector derives an 8-char suffix from `Organization.Id`, resolves the profile by name, and queries `User` for `supervisor{i}.<suffix>@example.com`, splitting occupied slots into active `existing_users` and `inactive_users`.
59
+
60
+ **Insertion.** The Apex loads `assets/create-supervisors.apex.template`, substitutes `__COUNT__`/`__PROFILE_ID__`/`__SUFFIX__`, and inserts only the missing indexes, re-checking inside the transaction to prevent a single run from double-inserting; across *concurrent* runs this check is not a guarantee (both can pass their pre-query before either commits), so duplicate protection there relies on the global username-uniqueness constraint plus the `DUPLICATE_USERNAME` retry (see references/apex-template-notes.md). Created users get the Service Cloud feature (`UserPermissionsSupportUser=true`); if the profile's license does not allow it, the Apex strips the flag and retries (the permset assign will then block until the user is on a suitable license). Each created user is reported via `SUPERVISOR_CREATED|<id>|<username>|<email>` (no password in the marker — it is set by the separate `System.setPassword` submission).
61
+
62
+ **Inactive occupants.** An inactive user occupying a supervisor slot is not a reusable supervisor and cannot be recreated (usernames are globally unique). It is surfaced as a required manual reactivation and never counted toward the requested slots — counting it would under-provision the config.
63
+
64
+ **Verification.** After insertion the detector re-runs and must show `missing_count == 0`.
65
+
66
+ ## Output contract
67
+
68
+ `detect-and-create.sh` emits a single JSON object with `status` ∈ `created` | `partial` | `reused` | `action_needed` | `blocked`, plus `detect`, `create` (`null` when nothing was created), `created_count`, `reused_count`, `total_present_after`, `users_needing_password_reset`, `action_required`, and `safe_to_write`.
69
+
70
+ - `created` — every missing index landed with a working password.
71
+ - `partial` — some landed but not all, any `System.setPassword` failed (kept ACTIVE, listed in `users_needing_password_reset`), or a slot is occupied by an inactive user needing reactivation.
72
+ - `reused` — all requested slots already existed; no DML.
73
+ - `action_needed` — plan mode only.
74
+ - `blocked` — precondition failed.
75
+
76
+ `create.created_users[].password` is populated only for users created this run whose `System.setPassword` succeeded. `create.rolled_back_users` is always empty (this skill never deactivates a user). Generated passwords are a secret: the returned JSON is the only place they appear; a caller that persists stdout must write it only to a restricted `CREDENTIALS.json` (mode 0600), redact it elsewhere, and delete it after distribution — the coordinator does this automatically.
77
+
78
+ ## Limitations
79
+
80
+ - Username pattern and org suffix are fixed and never operator-configurable — that determinism is what enables idempotent re-runs.
81
+ - Creates only supervisor users; it never deletes or deactivates users, including orphaned supervisors from prior runs.
82
+ - Common User errors (`DUPLICATE_USERNAME`, `INVALID_EMAIL`, `LICENSE_LIMIT_EXCEEDED`) are translated into operator-friendly messages rather than surfaced raw.
83
+
84
+ ## References
85
+
86
+ | File | When to read |
87
+ |---|---|
88
+ | `references/apex-patterns.md` | Before running the Apex — Apex structure, User field defaults, password policy, and the profile-localization risk |
89
+ | `references/apex-template-notes.md` | When creation returns a duplicate, license, password, or trace-safety error |
90
+ | `assets/create-supervisors.apex.template` | Loaded by `scripts/run-create.sh` when missing supervisor users must be inserted |
91
+ | `scripts/detect-existing.sh` | Loaded by `scripts/detect-and-create.sh` for the read-only org, profile, safety, and existing-user checks |
92
+ | `scripts/run-create.sh` | Internal writer loaded by `scripts/detect-and-create.sh` only after guard checks pass |
@@ -0,0 +1,72 @@
1
+ // service-omni-supervisor-users-create - Anonymous Apex asset template. Tokens __COUNT__/__PROFILE_ID__/__SUFFIX__ resolved by run-create.sh. Idempotency, password/TraceFlag security note, output contract: references/apex-template-notes.md.
2
+
3
+ Integer requested = __COUNT__;
4
+ String profileId = '__PROFILE_ID__';
5
+ String suffix = '__SUFFIX__';
6
+
7
+ Map<String, Integer> expectedByUsername = new Map<String, Integer>();
8
+ for (Integer i = 1; i <= requested; i++) {
9
+ expectedByUsername.put('supervisor' + i + '.' + suffix + '@example.com', i);
10
+ }
11
+
12
+ Set<String> existingUsernames = new Set<String>();
13
+ for (User u : [SELECT Username FROM User WHERE Username IN :expectedByUsername.keySet()]) {
14
+ existingUsernames.add(u.Username.toLowerCase());
15
+ }
16
+
17
+ List<User> toInsert = new List<User>();
18
+
19
+ for (Integer i = 1; i <= requested; i++) {
20
+ String uname = 'supervisor' + i + '.' + suffix + '@example.com';
21
+ if (existingUsernames.contains(uname.toLowerCase())) {
22
+ continue;
23
+ }
24
+
25
+ // Alias must be <= 8 chars - construct as omsu{i}{last-3-of-suffix}
26
+ // For i=1..9: 'omsu' (4) + i (1) + last-3-suffix (3) = 8 chars. OK.
27
+ String aliasStr = 'omsu' + i + suffix.substring(suffix.length() - 3);
28
+
29
+ User u = new User(
30
+ FirstName = 'Omni',
31
+ LastName = 'Supervisor' + i,
32
+ Email = uname,
33
+ Username = uname,
34
+ Alias = aliasStr,
35
+ ProfileId = profileId,
36
+ IsActive = true,
37
+ // Service Cloud feature license - REQUIRED for the ContactCenterSupervisor permset
38
+ // (IsContactCenterSupervisor is license-gated). Only allowed on user licenses that
39
+ // support it (e.g. Salesforce); the try/catch strips it and retries when the profile's
40
+ // user license rejects it (e.g. the limited "Service Cloud" user license). In that
41
+ // fallback the supervisor permset assign will later block until the user is moved to a
42
+ // Salesforce license + Service Cloud feature.
43
+ UserPermissionsSupportUser = true,
44
+ TimeZoneSidKey = 'America/Los_Angeles',
45
+ LocaleSidKey = 'en_US',
46
+ EmailEncodingKey = 'UTF-8',
47
+ LanguageLocaleKey = 'en_US'
48
+ );
49
+ toInsert.add(u);
50
+ }
51
+
52
+ if (toInsert.isEmpty()) {
53
+ System.debug('SUPERVISORS_ALL_EXIST - no inserts required');
54
+ return;
55
+ }
56
+
57
+ try {
58
+ insert toInsert;
59
+ System.debug('SUPERVISORS_SERVICE_CLOUD_FEATURE|enabled');
60
+ } catch (DmlException e) {
61
+ if (e.getMessage() != null && e.getMessage().contains('Service Cloud User')) {
62
+ for (User u : toInsert) { u.UserPermissionsSupportUser = false; }
63
+ insert toInsert;
64
+ System.debug('SUPERVISORS_SERVICE_CLOUD_FEATURE|skipped_license_not_allowed');
65
+ } else {
66
+ throw e;
67
+ }
68
+ }
69
+
70
+ for (User u : toInsert) {
71
+ System.debug('SUPERVISOR_CREATED|' + u.Id + '|' + u.Username + '|' + u.Email);
72
+ }