@salesforce/afv-skills 1.40.0 → 1.41.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.
- package/package.json +1 -1
- package/skills/automation-flow-generate/SKILL.md +32 -43
- package/skills/experience-portal-create/SKILL.md +497 -0
- package/skills/experience-portal-create/assets/report-template.md +30 -0
- package/skills/experience-portal-create/references/mcp-invocation.md +288 -0
- package/skills/experience-portal-create/references/post-creation-activate-publish.md +165 -0
- package/skills/experience-portal-create/references/templates.md +253 -0
- package/skills/experience-ui-bundle-features-generate/SKILL.md +5 -1
- package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -0
- package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -0
- package/skills/platform-datamask-run/SKILL.md +345 -0
- package/skills/platform-datamask-run/references/api-surface.md +130 -0
- package/skills/platform-datamask-run/references/policy-authoring.md +185 -0
- package/skills/platform-datamask-run/references/run-and-abort.md +116 -0
- package/skills/platform-datamask-run/scripts/poll-job.sh +115 -0
- package/skills/platform-dataspace-access-configure/SKILL.md +51 -3
- package/skills/platform-dataspace-access-configure/scripts/inspect-dataspace-scopes.sh +56 -0
- package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +1 -0
- package/skills/platform-sandbox-configure/SKILL.md +17 -2
- package/skills/platform-trial-org-create/SKILL.md +175 -0
- package/skills/platform-trial-org-create/examples/create_request.json +9 -0
- package/skills/platform-trial-org-create/examples/error_response.json +41 -0
- package/skills/platform-trial-org-create/examples/success_response.json +27 -0
- package/skills/platform-trial-org-create/references/error_codes.md +42 -0
- package/skills/platform-trial-org-create/references/signup_request_fields.md +69 -0
- package/skills/platform-trial-org-create/scripts/create_signup_request.sh +175 -0
- package/skills/platform-trial-org-create/scripts/get_signup_request.sh +155 -0
- package/skills/platform-widget-generate/SKILL.md +47 -6
- package/skills/platform-widget-generate/examples/conditional.json +3 -3
- package/skills/platform-widget-generate/examples/list-with-foreach.json +2 -2
- package/skills/platform-widget-generate/examples/single-object.json +2 -2
- package/skills/platform-widget-generate/references/widget-bundle-layout.md +1 -1
- package/skills/service-agentforce-channel-configure/SKILL.md +271 -0
- package/skills/service-agentforce-channel-configure/references/agent-wiring.md +97 -0
- package/skills/service-agentforce-channel-configure/references/channel-branch-email.md +145 -0
- package/skills/service-agentforce-channel-configure/references/channel-branch-voice.md +69 -0
- package/skills/service-agentforce-channel-configure/references/channel-types.md +61 -0
- package/skills/service-agentforce-channel-configure/references/live-traffic-gate.md +86 -0
- package/skills/service-agentforce-channel-configure/references/queue-resolution.md +135 -0
- package/skills/service-agentforce-channel-configure/references/routing-flow.md +384 -0
- package/skills/service-catalog-template-deploy/SKILL.md +310 -0
- package/skills/service-catalog-template-deploy/references/cli-invocation.md +258 -0
- package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +164 -0
- package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +94 -0
- package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +331 -0
- package/skills/service-catalog-template-search/SKILL.md +212 -0
- package/skills/service-catalog-template-search/references/cli-invocation.md +128 -0
- package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +205 -0
- package/skills/service-concierge-portal-generate/SKILL.md +126 -0
- package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1428 -0
- package/skills/service-digital-engagement-channel-configure/SKILL.md +46 -6
- package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +2 -1
- package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -1
- package/skills/service-helpagent-coordinate/README.md +8 -2
- package/skills/service-helpagent-coordinate/SKILL.md +126 -130
- package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +70 -53
- package/skills/service-helpagent-coordinate/references/agent-script.md +571 -457
- package/skills/service-helpagent-coordinate/references/channel-voice.md +38 -9
- package/skills/service-helpagent-coordinate/references/channel-web-chat.md +173 -49
- package/skills/service-helpagent-coordinate/references/output-report-format.md +126 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +153 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +79 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +35 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +271 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md +265 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-enable-plan.mjs +220 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-final-report.mjs +102 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/record-enable-result.mjs +73 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +206 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/references/cli-invocation.md +194 -0
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/scripts/classify-readiness.mjs +223 -0
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +45 -7
- package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +45 -5
- package/skills/service-itsm-agentic-setup-configure/SKILL.md +116 -0
- package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +64 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +158 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +361 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/error-taxonomy.md +44 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +66 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +66 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/specialized-templates.md +148 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +169 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/build-create-body.mjs +116 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +185 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +168 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/create-scratch-dir.mjs +58 -0
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +197 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +186 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/action-availability.md +51 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +345 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/error-taxonomy.md +44 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +63 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +44 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +149 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/build-create-body.mjs +110 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-action-availability.mjs +201 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-activate-result.mjs +135 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +194 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +158 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/create-scratch-dir.mjs +58 -0
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +191 -0
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +133 -0
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +71 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +308 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +23 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +193 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +57 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +394 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +266 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/cli-invocation.md +106 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/helper-contracts.md +142 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/permset-topology.md +82 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-action-surface.mjs +137 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-assignment-state.mjs +99 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-permset-availability.mjs +120 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/resolve-target-user.mjs +86 -0
- package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +284 -0
- package/skills/service-itsm-agentic-setup-uel-user-create/references/mcp-invocation.md +302 -0
- package/skills/service-itsm-channels-coordinate/SKILL.md +472 -0
- package/skills/service-itsm-incident-mgmt-configure/SKILL.md +212 -0
- package/skills/service-itsm-incident-mgmt-configure/references/mcp-invocation.md +225 -0
- package/skills/service-itsm-incident-priority-configure/SKILL.md +53 -12
- package/skills/service-itsm-swarming-configure/SKILL.md +212 -0
- package/skills/service-itsm-teams-configure/SKILL.md +395 -0
- package/skills/service-itsm-teams-configure/references/azure-credential-population.md +213 -0
- package/skills/service-itsm-teams-configure/references/gotchas.md +23 -0
- package/skills/service-itsm-teams-coordinate/SKILL.md +175 -0
- package/skills/service-itsm-teams-coordinate/examples/output-templates.md +85 -0
- package/skills/service-itsm-teams-debug/SKILL.md +144 -0
- package/skills/service-itsm-teams-debug/references/configuration-checklists.md +277 -0
- package/skills/service-itsm-teams-debug/references/report-generation.md +95 -0
- package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +139 -0
- package/skills/service-itsm-teams-employee-agent-configure/assets/Teams_AgentForce.EmbeddedServiceConfig-meta.xml +43 -0
- package/skills/service-itsm-teams-employee-agent-configure/references/teams-embedded-employee-agent.md +480 -0
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +232 -0
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +391 -0
package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# CLI invocation reference — service-itsm-agentic-setup-agentforce-studio-configure
|
|
2
|
+
|
|
3
|
+
This skill uses the **Salesforce CLI (`sf`)** as its only transport. It is
|
|
4
|
+
**write-capable**: it reads the *Agentforce for IT Service* Salesforce Go
|
|
5
|
+
feature toggles via Connect API (`sf api request rest`), computes a
|
|
6
|
+
dependency-ordered enable plan with a Node helper script, and — after explicit
|
|
7
|
+
user confirmation — enables each disabled toggle via the same Connect API
|
|
8
|
+
route. It extracts no access tokens.
|
|
9
|
+
|
|
10
|
+
## Why `sf api request rest`, never curl + token
|
|
11
|
+
|
|
12
|
+
`sf api request rest` authenticates using the CLI's stored session for the
|
|
13
|
+
`--target-org` alias — the CLI mints/refreshes the token internally and never
|
|
14
|
+
exposes it. **Do not** do:
|
|
15
|
+
|
|
16
|
+
<!-- skill-validate: ignore -->
|
|
17
|
+
```bash
|
|
18
|
+
# FORBIDDEN — leaks a bearer token into shell context, bypasses the CLI session
|
|
19
|
+
TOKEN=$(sf org display --json -o <alias> | jq -r '.result.accessToken')
|
|
20
|
+
curl -X POST -H "Authorization: Bearer $TOKEN" \
|
|
21
|
+
https://.../connect/setup/discovery/feature/sales-cloud-agent-studio/enable
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every call this skill makes — read and write — is a plain
|
|
25
|
+
`/services/data/v67.0/connect/...` Connect API path, exactly what
|
|
26
|
+
`sf api request rest` proxies. There is no reason to fall back to curl.
|
|
27
|
+
|
|
28
|
+
## The feature toggles and dependency order
|
|
29
|
+
|
|
30
|
+
| UI toggle | featureApiName | Depends on |
|
|
31
|
+
|-----------|----------------|------------|
|
|
32
|
+
| Einstein Generative AI | `sales-cloud-einstein-generative-ai` | none |
|
|
33
|
+
| Turn on Agentforce Studio | `sales-cloud-agent-studio` | Einstein GenAI |
|
|
34
|
+
| Agentforce for IT Service (parent umbrella) | `service-cloud-agentforce-for-itsm` | Studio, Einstein GenAI |
|
|
35
|
+
| IT Service Fulfiller (template) | `service-cloud-it-fulfiller-agent` | parent umbrella |
|
|
36
|
+
| IT Service Employee (template) | `service-cloud-requestor-agent` | parent umbrella |
|
|
37
|
+
| Specialized Agent Templates for Employee | `service-cloud-it-service-employee-agent` | parent umbrella |
|
|
38
|
+
|
|
39
|
+
Enable order: Einstein GenAI → Studio → parent umbrella → path-specific
|
|
40
|
+
template(s). The read response echoes unmet dependencies in
|
|
41
|
+
`dependencyStatuses[]`; the classifier encodes this same order in `ORDER` so
|
|
42
|
+
the enable loop never attempts a child before its parent.
|
|
43
|
+
|
|
44
|
+
## Target org, API version, and the `--json` rule
|
|
45
|
+
|
|
46
|
+
- **Target org**: always `--target-org <alias>` (or `-o <alias>`).
|
|
47
|
+
- **API version**: pinned in the URL path (`/services/data/v67.0/...`). Do not
|
|
48
|
+
hand-edit below `metadata.minApiVersion` (`67.0`).
|
|
49
|
+
- **`--json`**: do **not** add `--json` to `sf api request rest` — it is
|
|
50
|
+
unsupported on some Connect endpoints and errors; the raw stdout body is
|
|
51
|
+
already JSON.
|
|
52
|
+
|
|
53
|
+
## Read — feature status
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sf api request rest "/services/data/v67.0/connect/setup/discovery/features/status" \
|
|
57
|
+
--method POST \
|
|
58
|
+
--body '{"featureApiNames":["sales-cloud-einstein-generative-ai","sales-cloud-agent-studio","service-cloud-agentforce-for-itsm","service-cloud-it-fulfiller-agent","service-cloud-requestor-agent","service-cloud-it-service-employee-agent"]}' \
|
|
59
|
+
--target-org <alias> > /tmp/enable-status-before.json 2>/tmp/enable-status-before.err
|
|
60
|
+
echo $? > /tmp/enable-status-before.exit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Response shape (abridged):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"items": [
|
|
68
|
+
{ "apiName": "sales-cloud-einstein-generative-ai", "status": "ENABLED", "enableBlockedReasons": [], "dependencyStatuses": [] },
|
|
69
|
+
{ "apiName": "sales-cloud-agent-studio", "status": "NOT_ENABLED", "enableBlockedReasons": [], "dependencyStatuses": [{ "apiName": "sales-cloud-einstein-generative-ai", "status": "ENABLED" }] }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `status: "ENABLED"` ⇒ already on ⇒ skip the write (idempotent).
|
|
75
|
+
- `status: "NOT_ENABLED"` with empty `enableBlockedReasons` ⇒ enable-able.
|
|
76
|
+
- `status: "NOT_ENABLED"` with non-empty `enableBlockedReasons` ⇒ **do not
|
|
77
|
+
attempt to enable** — report the blocker.
|
|
78
|
+
- A confirmed `404`/NOT_FOUND error body ⇒ whole read is CANNOT-CONFIRM (gate
|
|
79
|
+
not wired).
|
|
80
|
+
- An empty body (CLI exited non-zero) or an auth/permission/unexpected error
|
|
81
|
+
body ⇒ whole read is ERROR — surface `rawError` and stop.
|
|
82
|
+
|
|
83
|
+
## Write — enable a feature
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
sf api request rest "/services/data/v67.0/connect/setup/discovery/feature/sales-cloud-agent-studio/enable" \
|
|
87
|
+
--method POST \
|
|
88
|
+
--body '{}' \
|
|
89
|
+
--target-org <alias>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Replace the path segment with the exact `featureApiName`. The endpoint itself
|
|
93
|
+
takes **no meaningful body** — but `sf api request rest --method POST` with no
|
|
94
|
+
`--body` flag at all fails with `Error (SfError): No 'mode' found in 'body'
|
|
95
|
+
entry` (verified on CLI 2.140.6 and 2.145.6 — this is a CLI-parser quirk, not
|
|
96
|
+
version-specific). Always pass `--body '{}'` explicitly. An optional
|
|
97
|
+
`solutionApiName` query param can bind the feature to a specific SKU/license
|
|
98
|
+
solution (omit for default — not used by this skill).
|
|
99
|
+
|
|
100
|
+
Response shape:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "success": true }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`{success:true}` = enabled (or was already enabled). If the response is an
|
|
107
|
+
error (e.g. `enableBlockedReasons` still present, missing license, unmet
|
|
108
|
+
dependency), record it verbatim and continue to the next toggle in the plan —
|
|
109
|
+
do not let one failure block the rest.
|
|
110
|
+
|
|
111
|
+
Capture every `/enable` response to a file and immediately hand it to
|
|
112
|
+
`scripts/record-enable-result.mjs` (below) — do not eyeball the JSON body in
|
|
113
|
+
prose to decide ENABLED vs FAILED.
|
|
114
|
+
|
|
115
|
+
## Re-check before every toggle — never trust one snapshot for the whole loop
|
|
116
|
+
|
|
117
|
+
A toggle whose dependency is still off at read time carries a non-empty
|
|
118
|
+
`enableBlockedReasons` — but that dependency may be enabled by an *earlier*
|
|
119
|
+
step in this same loop. If the loop trusts the Phase-1/Phase-2 snapshot for
|
|
120
|
+
every toggle, a child that becomes enable-able mid-loop is permanently
|
|
121
|
+
written off as blocked. **Re-read `features/status` and re-run
|
|
122
|
+
`classify-enable-plan.mjs` immediately before each toggle attempt**, and
|
|
123
|
+
branch on that toggle's *current* signal:
|
|
124
|
+
|
|
125
|
+
- `PASS` → already enabled, skip.
|
|
126
|
+
- `FAIL` with empty `enableBlockedReasons` → POST `/enable`, then record the
|
|
127
|
+
result.
|
|
128
|
+
- `FAIL` with non-empty `enableBlockedReasons` → still genuinely blocked,
|
|
129
|
+
skip to the next toggle.
|
|
130
|
+
- `CANNOT-CONFIRM`/`ERROR` on that specific re-read → stop the loop and
|
|
131
|
+
surface the failure; do not guess.
|
|
132
|
+
|
|
133
|
+
One failed or blocked toggle does not stop the loop from attempting the
|
|
134
|
+
remaining toggles in `order`.
|
|
135
|
+
|
|
136
|
+
## Enablement is confirm-gated — never a silent write
|
|
137
|
+
|
|
138
|
+
This skill mutates org state. Before any `/enable` POST, the exact `pending`
|
|
139
|
+
list (from the classifier) must be presented to the user via
|
|
140
|
+
`AskUserQuestion` and explicitly confirmed. Do not skip this checkpoint even
|
|
141
|
+
when the skill was reached via a hand-off from the validate skill's
|
|
142
|
+
NOT-READY report — that report names what is off, it does not constitute
|
|
143
|
+
confirmation to write.
|
|
144
|
+
|
|
145
|
+
## The classifier — `scripts/classify-enable-plan.mjs`
|
|
146
|
+
|
|
147
|
+
The deterministic plan/verdict logic lives in the script, not in the workflow
|
|
148
|
+
prose (authoring standard A9). Invoke it with the captured file, the agent
|
|
149
|
+
type, and the captured exit status, using the skill's **absolute** directory:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
node "<skill_dir>/scripts/classify-enable-plan.mjs" /tmp/enable-status-before.json <fulfiller|employee> "$(cat /tmp/enable-status-before.exit)"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Output (to stdout):
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"agentType": "fulfiller | employee",
|
|
160
|
+
"readState": "ok | not-wired | error",
|
|
161
|
+
"features": { "<apiName>": { "status": "ENABLED|NOT_ENABLED|UNKNOWN", "signal": "PASS|FAIL|CANNOT-CONFIRM|ERROR", "enableBlockedReasons": [] } },
|
|
162
|
+
"order": ["<apiName>", "..."],
|
|
163
|
+
"alreadyEnabled": ["<apiName>", "..."],
|
|
164
|
+
"pending": ["<apiName>", "..."],
|
|
165
|
+
"blocked": ["<apiName>", "..."],
|
|
166
|
+
"unconfirmed": ["<apiName>", "..."],
|
|
167
|
+
"verdict": "ALL-ENABLED | NEEDS-ENABLE | CANNOT-CONFIRM | ERROR",
|
|
168
|
+
"reasons": ["..."],
|
|
169
|
+
"rawError": "null | <error snippet>"
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`unconfirmed` lists any required `apiName` that was missing from the response
|
|
174
|
+
or carried a status the classifier doesn't recognize — neither confirmed
|
|
175
|
+
`ENABLED` nor `NOT_ENABLED`. `verdict` is `NEEDS-ENABLE` whenever `pending` is
|
|
176
|
+
non-empty; otherwise `CANNOT-CONFIRM` whenever `unconfirmed` is non-empty; only
|
|
177
|
+
`ALL-ENABLED` when both are empty. An empty `pending` alone does **not** mean
|
|
178
|
+
every toggle is enabled — always check `unconfirmed` too.
|
|
179
|
+
|
|
180
|
+
Call the script at least **three times** across the flow: once on the Phase-2
|
|
181
|
+
read (before any write) to compute the confirm checkpoint and seed
|
|
182
|
+
`alreadyEnabled`; then **once per toggle, immediately before each attempt**,
|
|
183
|
+
inside the Phase-4 loop (see re-check rule above); and once more on the
|
|
184
|
+
Phase-5 re-read (after the enable loop) to feed `classify-final-report.mjs`.
|
|
185
|
+
|
|
186
|
+
## Recording each enable attempt — `scripts/record-enable-result.mjs`
|
|
187
|
+
|
|
188
|
+
Deterministic per-toggle response classifier/accumulator (authoring standard
|
|
189
|
+
A9) — call it once per toggle, right after that toggle's `/enable` POST, from
|
|
190
|
+
inside the Bash loop:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
node "<skill_dir>/scripts/record-enable-result.mjs" /tmp/enable-<apiName>.json <apiName> /tmp/enable-results.json
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- `<response.json>` — the raw stdout captured from that toggle's `/enable`
|
|
197
|
+
POST.
|
|
198
|
+
- `<apiName>` — the featureApiName just attempted.
|
|
199
|
+
- `<results.json>` — an accumulator file read (if present) and rewritten each
|
|
200
|
+
call, keyed by `apiName`.
|
|
201
|
+
|
|
202
|
+
Emits/writes `{ "<apiName>": { "status": "ENABLED|FAILED", "reason": "null | <snippet>" } }` merged with any prior entries. `success:true` in the response body ⇒ `ENABLED`; any error body, empty body, or unexpected shape ⇒ `FAILED` with the reason captured verbatim. Never left unrecorded — even a malformed response becomes a `FAILED` entry, not a skipped one.
|
|
203
|
+
|
|
204
|
+
## Final verdict — `scripts/classify-final-report.mjs`
|
|
205
|
+
|
|
206
|
+
Deterministic final-report aggregator (authoring standard A9) — run once,
|
|
207
|
+
after the Phase-5 re-read, combining the Phase-2 plan, the Phase-4 results
|
|
208
|
+
accumulator, and the Phase-5 re-classification:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
node "<skill_dir>/scripts/classify-final-report.mjs" /tmp/enable-plan-before.json /tmp/enable-results.json /tmp/enable-plan-after.json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Pass `-` for the results arg if Phase 4 never ran (Phase-2 verdict was already
|
|
215
|
+
`ALL-ENABLED` or `CANNOT-CONFIRM`/`ERROR`). Output:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"features": { "<apiName>": { "finalStatus": "ALREADY-ENABLED|ENABLED|FAILED|CANNOT-CONFIRM|ERROR", "reason": "..." } },
|
|
220
|
+
"order": ["<apiName>", "..."],
|
|
221
|
+
"overall": "SUCCESS | PARTIAL | FAILED | CANNOT-CONFIRM | ERROR",
|
|
222
|
+
"reasons": ["..."]
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Render `features` and `overall` directly into the Output Format — do not
|
|
227
|
+
re-derive the per-feature or aggregate verdict in prose.
|
|
228
|
+
|
|
229
|
+
## Error taxonomy
|
|
230
|
+
|
|
231
|
+
- **Auth error / `401 Unauthorized`** — session expired or wrong alias.
|
|
232
|
+
Re-run `sf org login web`; there is no token to refresh by hand.
|
|
233
|
+
- **`403 Forbidden`** — the user/org lacks the Agentforce license
|
|
234
|
+
(`accessCheck` gate). Surface verbatim.
|
|
235
|
+
- **`404 Not Found`** on `features/status` — the feature-discovery surface is
|
|
236
|
+
not wired on the org tier. The classifier maps a confirmed 404/NOT_FOUND
|
|
237
|
+
error body to CANNOT-CONFIRM.
|
|
238
|
+
- **Empty body / unexpected shape** — the classifier maps these to ERROR (not
|
|
239
|
+
CANNOT-CONFIRM): surface `rawError` and stop.
|
|
240
|
+
- **`enableBlockedReasons` non-empty** on a toggle — an unmet dependency or a
|
|
241
|
+
purchase/licensing gate; the classifier keeps the toggle out of the enable
|
|
242
|
+
loop (`blocked`) and reports the reason. Do not retry the POST — but do
|
|
243
|
+
re-check on the next loop iteration in case an earlier toggle's `/enable`
|
|
244
|
+
just unblocked it.
|
|
245
|
+
- **A required `apiName` missing from the response, or an unrecognized
|
|
246
|
+
`status` string** — neither confirmed `ENABLED` nor `NOT_ENABLED`; the
|
|
247
|
+
classifier lists it in `unconfirmed` and the verdict is `CANNOT-CONFIRM`
|
|
248
|
+
even if `pending` is empty. Never report `ALL-ENABLED`/`SUCCESS` while
|
|
249
|
+
`unconfirmed` is non-empty.
|
|
250
|
+
|
|
251
|
+
## Gotchas
|
|
252
|
+
|
|
253
|
+
| Issue | Resolution |
|
|
254
|
+
|-------|------------|
|
|
255
|
+
| `sf api request rest --json` errors | Don't pass `--json` — the raw stdout body is already JSON |
|
|
256
|
+
| `sf api request rest --method POST` with no `--body` flag errors `No 'mode' found in 'body' entry` | The `/enable` endpoint takes no meaningful body, but the CLI still requires the flag — always pass `--body '{}'` explicitly (verified on CLI 2.140.6 and 2.145.6) |
|
|
257
|
+
| Bare `./scripts/classify-enable-plan.mjs` "not found" | Use the skill's absolute `<skill_dir>` in the `node` invocation |
|
|
258
|
+
| Enabling a child before its dependency | Always enable in the classifier's `order` — Einstein GenAI → Studio → parent → child template(s) |
|
|
259
|
+
| Attempting to enable a toggle whose *current* re-checked `enableBlockedReasons` is non-empty | Never — report the reasons instead of POSTing; the write would fail |
|
|
260
|
+
| Trusting the Phase-2 `blocked`/`pending` snapshot for the whole Phase-4 loop | Re-read + reclassify immediately before each toggle — a child blocked only because an earlier dependency was off becomes enable-able the instant that dependency's `/enable` succeeds |
|
|
261
|
+
| Reporting `ALL-ENABLED`/`SUCCESS` because `pending` is empty | Also check `unconfirmed` — a missing/unrecognized feature status is neither ENABLED nor NOT_ENABLED |
|
|
262
|
+
| Re-deriving the per-toggle or final aggregate verdict in prose | Use `record-enable-result.mjs` (per toggle) and `classify-final-report.mjs` (final) — deterministic scripts, not model judgment (authoring standard A9) |
|
|
263
|
+
| Skipping the confirm-to-write checkpoint on a hand-off from validate | Never — a NOT-READY report names what is off, it is not user confirmation to write |
|
|
264
|
+
| `updateOrgPref` → 500 `Invalid prefName` for an agent pref | Wrong write path — IPCManagement `updateOrgPref` is for IPC Management (Incident/Problem/Change) prefs only. Use `feature/{apiName}/enable` |
|
|
265
|
+
| Tempted to token+curl for "just a POST write" | Never — `sf api request rest` handles auth for `/services/data/...` paths |
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic enable-plan classifier for Agentforce for IT Service.
|
|
3
|
+
//
|
|
4
|
+
// Consumes the raw JSON body of a single `sf api request rest` read of the
|
|
5
|
+
// Salesforce Go feature-status endpoint and, for a chosen agent path
|
|
6
|
+
// (fulfiller | employee), computes a dependency-ordered enable plan and (on a
|
|
7
|
+
// second call, after the enable loop) the final per-feature verdict. This is
|
|
8
|
+
// the deterministic decision table that MUST NOT be interpreted by the model
|
|
9
|
+
// in prose (authoring standard A9) — same contract as the sibling
|
|
10
|
+
// service-itsm-agentic-setup-agentforce-studio-validate skill's
|
|
11
|
+
// classify-readiness.mjs, extended with enable ordering.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// node classify-enable-plan.mjs <features-status.json> <agentType> [exitStatus]
|
|
15
|
+
//
|
|
16
|
+
// <features-status.json> is a FILE PATH to the raw stdout captured from:
|
|
17
|
+
// sf api request rest "/services/data/vXX.0/connect/setup/discovery/features/status" \
|
|
18
|
+
// --method POST --body '{"featureApiNames":[...]}' --target-org <org> > file.json
|
|
19
|
+
// The body shape is { items: [ { apiName, status, enableBlockedReasons[],
|
|
20
|
+
// dependencyStatuses[] } ] } where status is "ENABLED" | "NOT_ENABLED". The
|
|
21
|
+
// file may instead hold the CLI error payload or be empty on a non-zero exit.
|
|
22
|
+
//
|
|
23
|
+
// <exitStatus> is the OPTIONAL numeric exit code of the `sf` command ($? in
|
|
24
|
+
// the shell) — distinguishes a confirmed 404 ("gate not wired" ->
|
|
25
|
+
// CANNOT-CONFIRM) from an auth/permission/transport failure (-> ERROR).
|
|
26
|
+
//
|
|
27
|
+
// <agentType> is one of: fulfiller | employee. Selects which path-specific
|
|
28
|
+
// template toggle is required in addition to the three shared toggles.
|
|
29
|
+
//
|
|
30
|
+
// Emits a single JSON object to stdout:
|
|
31
|
+
// { agentType, readState,
|
|
32
|
+
// features: { <apiName>: { status, signal, enableBlockedReasons } },
|
|
33
|
+
// order: [<apiName>...], // dependency-first order for the REQUIRED set
|
|
34
|
+
// alreadyEnabled: [<apiName>...],
|
|
35
|
+
// pending: [<apiName>...], // NOT_ENABLED, in dependency order — the enable plan
|
|
36
|
+
// blocked: [<apiName>...], // NOT_ENABLED with non-empty enableBlockedReasons
|
|
37
|
+
// unconfirmed: [<apiName>...], // missing from the response or an unexpected status
|
|
38
|
+
// verdict, reasons: [...], rawError }
|
|
39
|
+
// where verdict is ALL-ENABLED | NEEDS-ENABLE | CANNOT-CONFIRM | ERROR.
|
|
40
|
+
// Call this script BEFORE enabling (verdict=NEEDS-ENABLE -> drive the confirm
|
|
41
|
+
// list + enable loop from `pending`, in `order`) and AGAIN AFTER the enable
|
|
42
|
+
// loop on a fresh read (verdict=ALL-ENABLED -> success; anything left in
|
|
43
|
+
// `blocked` -> report enableBlockedReasons verbatim as FAILED for that toggle;
|
|
44
|
+
// anything in `unconfirmed` -> report CANNOT-CONFIRM for that toggle rather
|
|
45
|
+
// than assuming success — a required feature can be missing from the
|
|
46
|
+
// response or carry a status this classifier doesn't recognize, and an empty
|
|
47
|
+
// `pending` in that case does NOT mean every required toggle is ENABLED).
|
|
48
|
+
// Exit code is always 0 on a usable agentType; the verdict is in the payload.
|
|
49
|
+
|
|
50
|
+
import { readFileSync } from 'node:fs';
|
|
51
|
+
|
|
52
|
+
// Real Salesforce Go featureApiNames behind the "Agentforce for IT Service"
|
|
53
|
+
// setup page toggles — same apiNames the sibling validate skill classifies.
|
|
54
|
+
const EINSTEIN = 'sales-cloud-einstein-generative-ai'; // base layer (no deps)
|
|
55
|
+
const STUDIO = 'sales-cloud-agent-studio'; // "Turn on Agentforce Studio"
|
|
56
|
+
const PARENT = 'service-cloud-agentforce-for-itsm'; // "Agentforce for IT Service" umbrella
|
|
57
|
+
const FULFILLER = 'service-cloud-it-fulfiller-agent'; // "IT Service Fulfiller"
|
|
58
|
+
const REQUESTOR = 'service-cloud-requestor-agent'; // "IT Service Employee"
|
|
59
|
+
const SPECIALIZED = 'service-cloud-it-service-employee-agent'; // "Specialized Agent Templates for Employee"
|
|
60
|
+
|
|
61
|
+
// Dependency-first order per path: Einstein GenAI -> Studio -> parent umbrella -> child template(s).
|
|
62
|
+
const ORDER = {
|
|
63
|
+
fulfiller: [EINSTEIN, STUDIO, PARENT, FULFILLER],
|
|
64
|
+
employee: [EINSTEIN, STUDIO, PARENT, REQUESTOR, SPECIALIZED],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const LABEL = {
|
|
68
|
+
[EINSTEIN]: 'Einstein Generative AI',
|
|
69
|
+
[STUDIO]: 'Agentforce Studio',
|
|
70
|
+
[PARENT]: 'Agentforce for IT Service (parent)',
|
|
71
|
+
[FULFILLER]: 'IT Service Fulfiller template',
|
|
72
|
+
[REQUESTOR]: 'IT Service Employee template',
|
|
73
|
+
[SPECIALIZED]: 'Specialized Agent Templates for Employee',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function readJson(path) {
|
|
77
|
+
try {
|
|
78
|
+
const text = readFileSync(path, 'utf8').trim();
|
|
79
|
+
if (!text) return { ok: false, empty: true };
|
|
80
|
+
return { ok: true, data: JSON.parse(text) };
|
|
81
|
+
} catch {
|
|
82
|
+
return { ok: false, empty: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function asErrorArray(data) {
|
|
87
|
+
if (Array.isArray(data)) return data.filter((e) => e && (e.errorCode || e.message));
|
|
88
|
+
if (data && (data.errorCode || data.message)) return [data];
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isNotFoundError(errs) {
|
|
93
|
+
return errs.some((e) => {
|
|
94
|
+
const code = String(e?.errorCode ?? '').toUpperCase();
|
|
95
|
+
const msg = String(e?.message ?? '').toUpperCase();
|
|
96
|
+
return code.includes('NOT_FOUND') || code.includes('NOT_EXIST') ||
|
|
97
|
+
msg.includes('NOT FOUND') || msg.includes('DOES NOT EXIST') ||
|
|
98
|
+
msg.includes('COULD NOT BE FOUND');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function errorSnippet(errs) {
|
|
103
|
+
return errs.map((e) => `${e.errorCode ?? 'ERROR'}: ${e.message ?? ''}`.trim()).join(' | ').slice(0, 500);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readFeatureStatus(res, exitStatus) {
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const why = res.empty
|
|
109
|
+
? (Number.isFinite(exitStatus) && exitStatus !== 0
|
|
110
|
+
? `The read command exited ${exitStatus} with an empty body (auth / permission / transport failure).`
|
|
111
|
+
: 'The read produced an empty body — the response could not be read.')
|
|
112
|
+
: 'The read produced a non-JSON body that could not be parsed.';
|
|
113
|
+
return { readState: 'error', statusMap: null, rawError: why };
|
|
114
|
+
}
|
|
115
|
+
const d = res.data;
|
|
116
|
+
const errs = asErrorArray(d);
|
|
117
|
+
if (errs.length) {
|
|
118
|
+
if (isNotFoundError(errs)) return { readState: 'not-wired', statusMap: null, rawError: null };
|
|
119
|
+
return { readState: 'error', statusMap: null, rawError: errorSnippet(errs) };
|
|
120
|
+
}
|
|
121
|
+
const items = Array.isArray(d?.items) ? d.items : null;
|
|
122
|
+
if (!items) {
|
|
123
|
+
return { readState: 'error', statusMap: null, rawError: 'The response had no "items" array (unexpected shape).' };
|
|
124
|
+
}
|
|
125
|
+
const map = new Map();
|
|
126
|
+
for (const it of items) {
|
|
127
|
+
if (it?.apiName) {
|
|
128
|
+
map.set(it.apiName, {
|
|
129
|
+
status: String(it.status ?? '').toUpperCase(),
|
|
130
|
+
enableBlockedReasons: Array.isArray(it.enableBlockedReasons) ? it.enableBlockedReasons : [],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { readState: 'ok', statusMap: map, rawError: null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function classifyFeature(apiName, statusMap) {
|
|
138
|
+
const label = LABEL[apiName] ?? apiName;
|
|
139
|
+
if (!statusMap || !statusMap.has(apiName)) {
|
|
140
|
+
return { status: 'UNKNOWN', signal: 'CANNOT-CONFIRM', enableBlockedReasons: [], reason: `${label} (${apiName}) — no status returned.` };
|
|
141
|
+
}
|
|
142
|
+
const { status, enableBlockedReasons } = statusMap.get(apiName);
|
|
143
|
+
if (status === 'ENABLED') {
|
|
144
|
+
return { status, signal: 'PASS', enableBlockedReasons: [], reason: `${label} (${apiName}) → ENABLED.` };
|
|
145
|
+
}
|
|
146
|
+
if (status === 'NOT_ENABLED') {
|
|
147
|
+
const blocked = enableBlockedReasons.length > 0;
|
|
148
|
+
return {
|
|
149
|
+
status, signal: 'FAIL', enableBlockedReasons,
|
|
150
|
+
reason: blocked
|
|
151
|
+
? `${label} (${apiName}) → NOT_ENABLED, blocked: ${enableBlockedReasons.map((r) => (typeof r === 'string' ? r : (r?.message ?? JSON.stringify(r)))).join('; ')}.`
|
|
152
|
+
: `${label} (${apiName}) → NOT_ENABLED (ready to enable).`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { status, signal: 'CANNOT-CONFIRM', enableBlockedReasons: [], reason: `${label} (${apiName}) → unexpected status "${status}".` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const [statusPath, rawAgentType, rawExit] = process.argv.slice(2);
|
|
159
|
+
const agentType = String(rawAgentType ?? '').toLowerCase();
|
|
160
|
+
if (!statusPath || !ORDER[agentType]) {
|
|
161
|
+
process.stderr.write('usage: node classify-enable-plan.mjs <features-status.json> <fulfiller|employee> [exitStatus]\n');
|
|
162
|
+
process.exit(2);
|
|
163
|
+
}
|
|
164
|
+
const exitStatus = rawExit === undefined ? NaN : Number(rawExit);
|
|
165
|
+
const order = ORDER[agentType];
|
|
166
|
+
|
|
167
|
+
const { readState, statusMap, rawError } = readFeatureStatus(readJson(statusPath), exitStatus);
|
|
168
|
+
|
|
169
|
+
const features = {};
|
|
170
|
+
const reasons = [];
|
|
171
|
+
const alreadyEnabled = [];
|
|
172
|
+
const pending = [];
|
|
173
|
+
const blocked = [];
|
|
174
|
+
|
|
175
|
+
if (readState === 'error') {
|
|
176
|
+
for (const apiName of order) {
|
|
177
|
+
features[apiName] = { status: 'UNKNOWN', signal: 'ERROR', enableBlockedReasons: [] };
|
|
178
|
+
}
|
|
179
|
+
reasons.push(`Feature status could not be read: ${rawError} Surface the raw response and stop — do not treat as a wiring gap.`);
|
|
180
|
+
process.stdout.write(JSON.stringify({
|
|
181
|
+
agentType, readState, features, order, alreadyEnabled, pending, blocked, unconfirmed: order.slice(), verdict: 'ERROR', reasons, rawError,
|
|
182
|
+
}, null, 2) + '\n');
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (readState === 'not-wired') {
|
|
187
|
+
for (const apiName of order) {
|
|
188
|
+
features[apiName] = { status: 'UNKNOWN', signal: 'CANNOT-CONFIRM', enableBlockedReasons: [] };
|
|
189
|
+
}
|
|
190
|
+
reasons.push('The feature-discovery surface returned a confirmed 404 — not wired on this org tier.');
|
|
191
|
+
process.stdout.write(JSON.stringify({
|
|
192
|
+
agentType, readState, features, order, alreadyEnabled, pending, blocked, unconfirmed: order.slice(), verdict: 'CANNOT-CONFIRM', reasons, rawError,
|
|
193
|
+
}, null, 2) + '\n');
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const unconfirmed = [];
|
|
198
|
+
for (const apiName of order) {
|
|
199
|
+
const c = classifyFeature(apiName, statusMap);
|
|
200
|
+
features[apiName] = { status: c.status, signal: c.signal, enableBlockedReasons: c.enableBlockedReasons };
|
|
201
|
+
reasons.push(c.reason);
|
|
202
|
+
if (c.signal === 'PASS') alreadyEnabled.push(apiName);
|
|
203
|
+
if (c.signal === 'FAIL') {
|
|
204
|
+
pending.push(apiName);
|
|
205
|
+
if (c.enableBlockedReasons.length) blocked.push(apiName);
|
|
206
|
+
}
|
|
207
|
+
if (c.signal === 'CANNOT-CONFIRM') unconfirmed.push(apiName);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A required feature missing from the response, or carrying an unexpected
|
|
211
|
+
// status string, is neither confirmed ENABLED nor NOT_ENABLED — an empty
|
|
212
|
+
// `pending` list in that case must NOT read as ALL-ENABLED.
|
|
213
|
+
let verdict;
|
|
214
|
+
if (pending.length > 0) verdict = 'NEEDS-ENABLE';
|
|
215
|
+
else if (unconfirmed.length > 0) verdict = 'CANNOT-CONFIRM';
|
|
216
|
+
else verdict = 'ALL-ENABLED';
|
|
217
|
+
|
|
218
|
+
process.stdout.write(JSON.stringify({
|
|
219
|
+
agentType, readState, features, order, alreadyEnabled, pending, blocked, unconfirmed, verdict, reasons, rawError,
|
|
220
|
+
}, null, 2) + '\n');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic final-report aggregator for the enable flow (authoring
|
|
3
|
+
// standard A9). Combines the Phase-2 (before-write) plan, the Phase-4
|
|
4
|
+
// per-toggle enable results (from record-enable-result.mjs), and the Phase-5
|
|
5
|
+
// (after-write) re-classification into one final per-feature verdict plus an
|
|
6
|
+
// overall SUCCESS/PARTIAL/FAILED/CANNOT-CONFIRM/ERROR summary — a fixed
|
|
7
|
+
// comparison/aggregation that must not be eyeballed by the model across a
|
|
8
|
+
// variable-length feature list.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node classify-final-report.mjs <before.json> <results.json|-> <after.json>
|
|
12
|
+
//
|
|
13
|
+
// <before.json> — stdout of classify-enable-plan.mjs from Phase 2 (pre-write).
|
|
14
|
+
// <results.json> — stdout of record-enable-result.mjs accumulated across
|
|
15
|
+
// Phase 4 (one entry per toggle actually POSTed to /enable).
|
|
16
|
+
// Pass "-" if Phase 4 never ran (Phase-2 verdict was already
|
|
17
|
+
// ALL-ENABLED or CANNOT-CONFIRM/ERROR).
|
|
18
|
+
// <after.json> — stdout of classify-enable-plan.mjs from Phase 5 (post-write).
|
|
19
|
+
//
|
|
20
|
+
// Emits a single JSON object to stdout:
|
|
21
|
+
// { features: { <apiName>: { finalStatus, reason } }, order: [...], overall, reasons: [...] }
|
|
22
|
+
// where finalStatus is ALREADY-ENABLED | ENABLED | FAILED | CANNOT-CONFIRM | ERROR
|
|
23
|
+
// and overall is SUCCESS | PARTIAL | FAILED | CANNOT-CONFIRM | ERROR.
|
|
24
|
+
// Exit code is always 0 on usable args.
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from 'node:fs';
|
|
27
|
+
|
|
28
|
+
const [beforePath, resultsPath, afterPath] = process.argv.slice(2);
|
|
29
|
+
if (!beforePath || !resultsPath || !afterPath) {
|
|
30
|
+
process.stderr.write('usage: node classify-final-report.mjs <before.json> <results.json|-> <after.json>\n');
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readJson(path) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const before = readJson(beforePath);
|
|
43
|
+
const after = readJson(afterPath);
|
|
44
|
+
const results = resultsPath === '-' ? {} : (readJson(resultsPath) ?? {});
|
|
45
|
+
|
|
46
|
+
if (!after || !Array.isArray(after.order)) {
|
|
47
|
+
process.stderr.write(`error: could not read/parse a usable "after" report from ${afterPath}\n`);
|
|
48
|
+
process.exit(3);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const alreadyEnabledBefore = new Set(Array.isArray(before?.alreadyEnabled) ? before.alreadyEnabled : []);
|
|
52
|
+
const order = after.order;
|
|
53
|
+
const features = {};
|
|
54
|
+
const reasons = [];
|
|
55
|
+
|
|
56
|
+
for (const apiName of order) {
|
|
57
|
+
const afterFeature = after.features?.[apiName];
|
|
58
|
+
const recorded = results?.[apiName];
|
|
59
|
+
const signal = afterFeature?.signal;
|
|
60
|
+
|
|
61
|
+
// The Phase-2 "already enabled" shortcut only applies if the Phase-5
|
|
62
|
+
// post-write read actually confirms it — a failed/unconfirmed re-read must
|
|
63
|
+
// never be silently overridden by the pre-write snapshot, or a broken
|
|
64
|
+
// verification read reports false SUCCESS for every already-enabled toggle.
|
|
65
|
+
if (alreadyEnabledBefore.has(apiName) && signal === 'PASS') {
|
|
66
|
+
features[apiName] = { finalStatus: 'ALREADY-ENABLED', reason: `${apiName} was already ENABLED before this run.` };
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (signal === 'PASS') {
|
|
71
|
+
features[apiName] = { finalStatus: 'ENABLED', reason: `${apiName} is ENABLED after this run.` };
|
|
72
|
+
} else if (signal === 'FAIL') {
|
|
73
|
+
const enableBlockedReasons = afterFeature?.enableBlockedReasons ?? [];
|
|
74
|
+
const recordedReason = recorded?.status === 'FAILED' ? recorded.reason : null;
|
|
75
|
+
features[apiName] = {
|
|
76
|
+
finalStatus: 'FAILED',
|
|
77
|
+
reason: recordedReason
|
|
78
|
+
? `${apiName} — the /enable call failed: ${recordedReason}`
|
|
79
|
+
: enableBlockedReasons.length
|
|
80
|
+
? `${apiName} — still blocked after the enable loop: ${JSON.stringify(enableBlockedReasons)}`
|
|
81
|
+
: `${apiName} — still NOT_ENABLED after the enable loop (not attempted or attempt did not persist).`,
|
|
82
|
+
};
|
|
83
|
+
} else if (signal === 'CANNOT-CONFIRM') {
|
|
84
|
+
features[apiName] = { finalStatus: 'CANNOT-CONFIRM', reason: `${apiName} — status could not be confirmed on the post-write read.` };
|
|
85
|
+
} else {
|
|
86
|
+
features[apiName] = { finalStatus: 'ERROR', reason: `${apiName} — the post-write read failed (${afterFeature?.reason ?? 'unknown error'}).` };
|
|
87
|
+
}
|
|
88
|
+
reasons.push(features[apiName].reason);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const statuses = Object.values(features).map((f) => f.finalStatus);
|
|
92
|
+
const hasError = statuses.includes('ERROR');
|
|
93
|
+
const hasFailed = statuses.includes('FAILED');
|
|
94
|
+
const hasCannotConfirm = statuses.includes('CANNOT-CONFIRM');
|
|
95
|
+
const hasEnabled = statuses.includes('ENABLED');
|
|
96
|
+
|
|
97
|
+
let overall;
|
|
98
|
+
if (hasError) overall = 'ERROR';
|
|
99
|
+
else if (hasFailed || hasCannotConfirm) overall = hasEnabled ? 'PARTIAL' : (hasFailed ? 'FAILED' : 'CANNOT-CONFIRM');
|
|
100
|
+
else overall = 'SUCCESS';
|
|
101
|
+
|
|
102
|
+
process.stdout.write(JSON.stringify({ features, order, overall, reasons }, null, 2) + '\n');
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic per-toggle enable-result recorder (authoring standard A9).
|
|
3
|
+
//
|
|
4
|
+
// Classifying a single `/enable` POST response as ENABLED vs FAILED, and
|
|
5
|
+
// accumulating that into a running results file across the enable loop, is a
|
|
6
|
+
// fixed decision — it must not be left to the model to eyeball each response
|
|
7
|
+
// in prose across a variable-length loop.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node record-enable-result.mjs <response.json> <apiName> <results.json>
|
|
11
|
+
//
|
|
12
|
+
// <response.json> is a FILE PATH to the raw stdout captured from a single:
|
|
13
|
+
// sf api request rest ".../feature/<apiName>/enable" --method POST --body '{}' --target-org <alias>
|
|
14
|
+
// <results.json> is a FILE PATH this script reads (if present) and rewrites —
|
|
15
|
+
// an accumulator object keyed by apiName, so it can be called once per
|
|
16
|
+
// toggle, immediately after each individual (still-visible) Bash enable call.
|
|
17
|
+
//
|
|
18
|
+
// Prints the updated accumulator object to stdout on success. Exit code is
|
|
19
|
+
// always 0 on usable args — a failed enable is a recorded FAILED result, not
|
|
20
|
+
// a script failure.
|
|
21
|
+
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
23
|
+
|
|
24
|
+
const [responsePath, apiName, resultsPath] = process.argv.slice(2);
|
|
25
|
+
if (!responsePath || !apiName || !resultsPath) {
|
|
26
|
+
process.stderr.write('usage: node record-enable-result.mjs <response.json> <apiName> <results.json>\n');
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(path) {
|
|
31
|
+
try {
|
|
32
|
+
const text = readFileSync(path, 'utf8').trim();
|
|
33
|
+
if (!text) return { ok: false, empty: true };
|
|
34
|
+
return { ok: true, data: JSON.parse(text) };
|
|
35
|
+
} catch {
|
|
36
|
+
return { ok: false, empty: false };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function asErrorArray(data) {
|
|
41
|
+
if (Array.isArray(data)) return data.filter((e) => e && (e.errorCode || e.message));
|
|
42
|
+
if (data && (data.errorCode || data.message)) return [data];
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function errorSnippet(errs) {
|
|
47
|
+
return errs.map((e) => `${e.errorCode ?? 'ERROR'}: ${e.message ?? ''}`.trim()).join(' | ').slice(0, 500);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const res = readJson(responsePath);
|
|
51
|
+
let entry;
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
entry = { status: 'FAILED', reason: res.empty ? 'Enable call produced an empty body.' : 'Enable call produced a non-JSON body.' };
|
|
54
|
+
} else {
|
|
55
|
+
const errs = asErrorArray(res.data);
|
|
56
|
+
if (errs.length) {
|
|
57
|
+
entry = { status: 'FAILED', reason: errorSnippet(errs) };
|
|
58
|
+
} else if (res.data?.success === true) {
|
|
59
|
+
entry = { status: 'ENABLED', reason: null };
|
|
60
|
+
} else {
|
|
61
|
+
entry = { status: 'FAILED', reason: `Unexpected response shape (no success:true, no error body): ${JSON.stringify(res.data).slice(0, 500)}` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let results = {};
|
|
66
|
+
if (existsSync(resultsPath)) {
|
|
67
|
+
const existing = readJson(resultsPath);
|
|
68
|
+
if (existing.ok && existing.data && typeof existing.data === 'object') results = existing.data;
|
|
69
|
+
}
|
|
70
|
+
results[apiName] = entry;
|
|
71
|
+
|
|
72
|
+
writeFileSync(resultsPath, JSON.stringify(results, null, 2), 'utf8');
|
|
73
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|