executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
modernize-flp-sandboxUse when migrating UI5 apps from legacy FLP sandbox to the new sandbox (New Sandbox). Triggers on /modernize-flp-sandbox, mentions of "flpSandbox.html", "sap-ushell-config", or requests to update sandbox configuration.
Independent GitHub snapshot · Automated classification · Original documentation governs
Migrate a UI5 application from the legacy FLP sandbox to New Sandbox. You analyze, transform, verify, and report. The skill is fully automatic for the core migration; for adjacent concerns (see scope below) it asks the consumer before acting.
Read references/operations.md now — it is the operational reference for
all HTML / JSON / hook-module transformations.
Read references/sandbox-config-surface.md now — it is the ground-truth
description of what New Sandbox allows, blocks, and silently overrides.
All §6 rules cite this file rather than restating constraints inline.
The skill operates in three layers. Each §6 subsection below is tagged with one of them so it is always clear what is core and what is adjacent.
Core migration — actions that are necessary for the app to boot under New Sandbox. The skill performs them unconditionally and fails the migration if they cannot be performed. Triggers: §6a, §6b, §6c, §6f.
Test infrastructure — actions that get the existing test suite running again under New Sandbox without changing test semantics. The skill performs the safe ones (resource-root rebind, OPA bootstrap extraction). For anything that touches test logic, it detects, reports, and offers to apply on consumer request. Triggers: §6d, §6e, §6g, §6h, §6j, §6l.
Adjacent migrations & advisories — concerns that are surfaced by the New Sandbox migration but are not part of it. The skill detects and reports only; no rewrites are applied. Triggers:
Note: deprecated ushell service usage (§6k) is detected as part of the core pass and reported as "Manual action required" — it is a hard runtime block, not an advisory.
If the user provided a path argument, use it. Otherwise, detect from CWD:
package.json + webapp/ in CWD and parent directories/modernize-flp-sandbox ~/path/to/app"Set APP_ROOT to the resolved absolute path. All file operations use this as the base.
Before touching any files, secure a rollback point.
If git repository detected (git -C $APP_ROOT rev-parse --git-dir succeeds):
cd $APP_ROOT
git stash push -m "modernize-flp-sandbox: pre-migration backup $(date +%Y%m%d-%H%M%S)"
Save the stash ref for rollback. If git stash fails (nothing to stash), note it — the
working tree is already clean.
If no git repository:
mkdir -p $APP_ROOT/webapp/test/.migration-backup
cp $APP_ROOT/webapp/test/flpSandbox*.html $APP_ROOT/webapp/test/.migration-backup/ 2>/dev/null
cp $APP_ROOT/webapp/test/fioriSandboxAppConfig.json $APP_ROOT/webapp/test/.migration-backup/ 2>/dev/null
Save the backup path for rollback.
Read all HTML files under $APP_ROOT/webapp/test/ (recursive, including subdirectories) and build a pattern inventory:
Detect legacy sandbox HTML files — a file is a legacy sandbox HTML if it contains ANY of:
id="sap-ushell-bootstrap" (sandbox bootstrap script tag), ORwindow["sap-ushell-config"] assignment, ORsrc= referencing sandbox2.js (intermediate New Sandbox old-style bootstrap — predates SandboxBootTask.js)The sandbox2.js pattern indicates a partial/old-style New Sandbox migration that still needs updating to the current SandboxBootTask.js-based approach. Treat it identically to the other legacy patterns.
OPA test-page (does NOT receive SandboxBootTask / boot-manifest): A file matches the legacy sandbox detection criteria above AND ALSO contains ALL of:
QUnit.config or QUnit.config.autostart assignment, OR window.QUnit = { patternsap.ui.require( with entries from "sap/ui/qunit/, "sap/ui/test/, or "sap/ui/thirdparty/qunitThese files are NOT added to the legacy HTML migration list. Instead, they receive a different treatment composed of two pieces: (a) §6g extracts the inline OPA bootstrap into a sibling JS file and rewrites the HTML attributes, (b) §6i detects QUnit 1.x patterns in the journey/page modules and hands off to the modernize-flp-sandbox-qunit sub-skill on consumer consent. The HTML edits performed in §6g specifically:
<script src="../ushellConfig.js"> and <script src="...sandbox.js" id="sap-ushell-bootstrap"> includes (these are no longer needed once the primary FLP HTML is migrated)<script> block with direct script-tag loading (qunit-2.css, qunit-2.js, qunit-junit.js) + <script src="AllJourneys.js">AllJourneys.js from the inline bootstrap content (see §6g below)data-sap-ui-preload="async" to data-sap-ui-async="true" and rename data-sap-ui-compatVersion → data-sap-ui-compat-version and data-sap-ui-resourceroots → data-sap-ui-resource-rootsSandboxBootTask.js, boot-manifest, or <div id="canvas">Externalized ushell config detection:
After scanning HTML files for legacy sandbox detection, also check: for
each <script src="..."> tag in any HTML file under webapp/test/, if
the src attribute resolves to a file (default: .js extension; the
extension is not required — accept any src whose contents are loaded
as JavaScript) that:
(a) is reachable from the HTML's directory (resolve src against the
HTML location; the file may live anywhere under $APP_ROOT/, not
only under webapp/test/), AND
(b) contains an assignment into the global ushell-config object via
any of these write patterns:
- window["sap-ushell-config"] = …
- globalThis["sap-ushell-config"] = …
- self["sap-ushell-config"] = …
- any Object.assign(<global>["sap-ushell-config"], …) where
<global> is one of the three above
- any <global>["sap-ushell-config"].<prop> = … (property-write
after a prior ??=/||= initializer in the same file)
A file that only *reads* `window["sap-ushell-config"]` does NOT
qualify — bare reads are common in app code that consults the
config and must not be deleted.
Record each such file as an externalUshellConfigFile. For migration:
window["sap-ushell-config"] value from the external file identically to inline config (same rules for extracting applications, rootIntent, etc.)<script src="..."> include tag from ALL HTML files that reference itFor each legacy HTML file found, record:
MockServer or use-mockserver or locate-reuse-libs)fakeLrep or FakeLrepConnector or xx-flexibilityServices)bootstrapPlugins with entries other than RuntimeAuthoringPlugin)Personalization, LaunchPage, NavTargetResolution in services config)locate-reuse-libs.js? (non-standard pattern — flag for Gap-Report)data-sap-ui-libs value (carry over)data-sap-ui-resourceroots / data-sap-ui-resource-roots value (carry over, rename to kebab-case)data-sap-ui-theme value (carry over)data-sap-ui-language value (carry over)Check for existing New Sandbox artifacts:
fioriSandboxAppConfig.json — exists? (will be created/overwritten)sandbox/ directory — exists? For each *.js file inside, classify:
legacy if file contains Container.createRendererInternal OR attachRendererCreatedEvent OR file does NOT contain the literal token execute:. Capture the filename for in-place rewrite (Step 6c).modern if file exports { execute: ... }. Reuse untouched.legacy file, capture top-level namespace declarations as preservedDeclarations[]. Match the regex ^\s*sap(\.[a-zA-Z0-9_]+)+\s*\?\?=\s*\{\} and analogous globalThis.sap/window.sap chains. Conservative fallback: if a non-recognised top-of-file prelude exists, copy lines verbatim from start of file until the first sap.ui.define( or (function() IIFE.execute: export
treatment. Only one file needs to be wired via beforeFlpStart — if one file's name
suggests it is the primary init (heuristic: name contains Init and not Mock/MockServer),
prefer the primary one. Emit a Gap-Report note listing all rewritten files so the developer
can verify the correct one is referenced.fakeLrep.json / fakeLRep.json, but any
*Lrep*.json / *LRep*.json under webapp/test/ qualifies) — exists?
Note exact filename for the rta reference.Check for OPA test wiring:
webapp/test/integration/opaTests.qunit.html; fallback: any
*qunit.html under webapp/test/integration/ whose
data-sap-ui-resource-roots attribute references the legacy mockserver
HTML) — exists? Read the attribute. Record any value matching
"../flpSandboxMockServer" (without .html, possibly with trailing
path) — this triggers Step 6d.webapp/test/integration/pages/Common.js; fallback: any *.js under
webapp/test/integration/ containing both iStartMyAppInAFrame( or
getFrameUrl( AND sap.ui.require.toUrl() — exists? Record whether
it matches the iframe-URL-builder pattern — this triggers Step 6e.Scan application code for deprecated ushell service usage:
This is the analysis-time inventory pass. The action — reporting as
"Manual action required" in the Gap-Report — is in §6k.
The canonical 12-entry table with successors and reasons lives in
references/sandbox-config-surface.md §5; do not duplicate it here.
Search all *.js files under webapp/ excluding test/, resources/,
and *.min.js for getService(<name>) / getServiceAsync(<name>)
where <name> is one of the 12 deprecated services in the reference.
Per match record file path, line number, service name, and (if visible)
the variable bound to the resolved service.
If at least one match is found, surface the inventory in the Gap-Report during §8 and let §6k drive the follow-up. If no matches: note "No deprecated service usage found in app code."
Check UI5 framework version:
Read ui5.yaml and ui5-local.yaml (if present) for the framework.version field.
New Sandbox requires UI5 >= 1.147. If the version is below 1.147, automatically bump it:
npm info @sapui5/distribution-metadata versions --json 2>/dev/null | python3 -c "import json,sys; vs=[v for v in json.load(sys.stdin) if not 'SNAPSHOT' in v]; vs.sort(key=lambda v: [int(x) for x in v.split('.')]); print(vs[-1])" to find the latest stable SAPUI5 version >= 1.147.ui5.yaml (replace the framework.version value in-place).<old> to <new> (New Sandbox requires >= 1.147)."Note this in the Gap-Report as an automatic change that was applied.
This step identifies eager sap/ushell/* imports anywhere in the transitive AMD graph reachable from the beforeFlpStart hook. Such imports trigger Hard Constraint H2 from references/sandbox-config-surface.md §1 — the app never renders. The architectural detail and the rewrite recipe live in references/operations.md §6f.
This step builds the inventory only. The actual graph walk and rewrite/report happens in Section 6f (after the hook is generated in 6c).
Inventory items to record now (during Step 3):
webapp/test/fioriSandboxAppConfig.json exists with beforeFlpStart, resolve that module path. Otherwise the entry is the legacy hook detected in Step 3 (typically webapp/test/sandbox/fioriSandboxInit.js or whatever the legacy mockserver init file is). On a first-run migration, the entry is whichever module Section 6c will write/rewrite.hookEntryModule for Section 6f.hookGraphRoots[] — additional modules the hook will require directly (e.g. localService/mockserver). These come from inspecting the legacy hook's body. Section 6f recurses from each.The graph walk itself (depth-first traversal, dependency extraction, classification) runs at the start of Section 6f, after Sections 6a–6e have produced the post-migration hook content.
Skip this step if only one legacy sandbox HTML was found in step 3.
Context: New Sandbox uses a single shared fioriSandboxAppConfig.json. Multiple HTML files
cannot have independent configurations. The solution is to consolidate into one HTML file with
a URL parameter that selects the scenario at runtime.
Non-interactive mode: Before prompting, check for $APP_ROOT/webapp/test/wizard-answers.json.
If it exists, use its values directly. This lets callers pre-fill answers for unattended runs
(CI, scripted migrations). Skip the questions below if found.
{
"scenarios": [
{ "file": "flpSandbox.html", "id": "default", "description": "Live backend" },
{ "file": "flpSandboxMockServer.html", "id": "mockserver", "description": "Mock server" }
],
"defaultScenario": "mockserver",
"targetHtml": "flpSandbox.html",
"paramName": "scenario"
}
Fields: scenarios (all legacy HTMLs with their assigned ids), defaultScenario (id of the
scenario that runs when no URL param is present), targetHtml (the HTML file that survives),
paramName (URL parameter name — optional, defaults to "scenario" if absent).
Ask the user:
"Found multiple legacy sandbox files: <list with detected characteristics>.
What scenario does each file represent?" (e.g., "mockserver", "cdm", "default/no-mock")
"Which scenario should be the default when no URL parameter is set?
(Recommendation: mockserver — it works offline without a proxy, which is the
common case for local development. Other scenarios often require Fiori Tools or
a network proxy.)"
"Which HTML filename should be kept for the migrated file?"
"What URL parameter name should be used to select the scenario?
(Default: scenario. For apps that need to avoid collisions with other URL parameters,
a project-specific name like myAppScenario is recommended — document it in the README.)"
Record:
[{id: "mockserver", description: "..."}, {id: "default", ...}]mockserver)"scenario"; user may override to avoid collisions)URL Parameter Convention:
The default parameter name is "scenario". It is short and human-readable.
If multiple apps share the same browser context, use a project-specific prefix to avoid
parameter collisions (e.g. "myAppScenario"). Document the chosen name in the project's
README or migration guide.
| Scenario | Example URL (default param name) |
|---|---|
| Use default (no param) | flpSandbox.html |
| Explicit mockserver | flpSandbox.html?scenario=mockserver |
| Live backend | flpSandbox.html?scenario=default |
| Custom named scenario | flpSandbox.html?scenario=cdm |
If parameter is absent → default scenario runs (recommended default: mockserver).
Before writing any files, verify:
APP_ROOT is set (Step 1 completed)All good → proceed to Step 6.
Execute all transformations. Work file by file. If any write operation fails, immediately jump to ROLLBACK (Section 10).
SCOPE: Modify only the files listed below. Files explicitly out of scope:
unitTests.qunit.html, files underunit/,*.qunit.js.Files in scope:
- All
flp*.htmlfiles detected in §3 → full transformation (§6a-§6c)- OPA test-page HTMLs (default
webapp/test/integration/Journey.qunit.html) → bootstrap extraction (§6g)- OPA test runner HTML (default
webapp/test/integration/opaTests.qunit.html) → resource-root rebind (§6d)- QUnit test suite index (default
webapp/test/testsuite.qunit.html) → journey-URL rewrite (§6g)- OPA Common-helper file (default
webapp/test/integration/pages/Common.js) → URL-param injection (§6e)webapp/test/fioriSandboxAppConfig.json→ create / overwrite (§6b)webapp/test/sandbox/*.jslegacy hook files → rewrite in-place (§6c)- Files reachable from the
beforeFlpStarttransitive AMD graph → auto-apply transformations fromreferences/native-replacements.mdonly (§6f)- OPA test code under
webapp/test/integration/and (for §6h) app code underwebapp/→ mechanical rewrites against the rules in §6h–§6l; §6i is detect-only (advisory). §6k is detect-only (manual required — hard runtime block, no sub-skill offered).When
flpSandboxMockServer.htmlis referenced byopaTests.qunit.html, migrate it normally (§6a) AND rebind the OPA reference (§6d). Both must happen together.
Detail. Each subsection below states what the step does, when it
runs, and which layer it belongs to. The full algorithm — detection
patterns, regex, edge cases, recipes — lives in
references/operations.md. Each
subsection links into the matching section there.
Layer: core. Performed unconditionally on every legacy sandbox HTML detected in §3.
What it does: removes the legacy ushell-config write, the
sap-ushell-bootstrap script tag, and obsolete bootstrap attributes;
adds SandboxBootTask.js (or its CDN variant), the boot-manifest
attributes, and the <div id="canvas"> invariant; merges resource
roots across HTML variants and always adds the sandbox root.
→ See references/operations.md §6a.
fioriSandboxAppConfig.jsonLayer: core. The sandbox honors only the keys listed in
references/sandbox-config-surface.md §2
— emit none beyond that set.
What it does: builds the JSON from the parsed legacy
window["sap-ushell-config"]. Splits each application key on the
first - into semanticObject + action. Strips query strings out
of rootPath and ensures a trailing slash. Aggregates tiles across
all HTML variants when the Wizard ran. Adds beforeFlpStart,
rta, plugins only when the inputs warrant it.
→ See references/operations.md §6b.
Layer: core. Backs beforeFlpStart and runs before
applyUshellConfig. The contract lives in
references/sandbox-config-surface.md §8:
the hook may not touch globalThis["sap-ushell-config"] and must not
eagerly require sap/ushell/*.
What it does: produces a single-scenario hook (mock-server only) or a multi-scenario hook (URL-parameter switch) depending on whether the Wizard ran. Rewrites legacy hook modules in place, preserving any top-of-file namespace declarations. Falls back to a minimal no-op when no scenario actually needs a mock server.
→ See references/operations.md §6c.
Layer: test-infra. Mechanical resource-root rewrite — applied unconditionally because it does not change test semantics.
What it does: in the OPA test runner HTML (default
webapp/test/integration/opaTests.qunit.html), rewrites any
data-sap-ui-resource-roots value pointing at the legacy mockserver
HTML so it points at the migrated target HTML.
→ See references/operations.md §6d.
Layer: test-infra. Only triggered when the Wizard consolidated multiple HTMLs into one and the chosen URL-parameter convention has to be threaded through the iframe URL builder.
What it does: in the OPA Common-helper file, injects the scenario
parameter into the iframe URL before the # hash is appended (so
window.location.search still sees it).
→ See references/operations.md §6e.
Layer: core. Hard runtime block H2 (see
references/sandbox-config-surface.md §1)
— no sap/ushell/* may be required eagerly from the hook's
transitive AMD graph.
What it does: walks the AMD closure starting at
hookEntryModule (§3a), classifies every sap/ushell/* hit as
trivial-replaceable / architectural-relocatable / manual-required,
and either auto-applies a recipe from
references/native-replacements.md
or emits a Gap-Report entry with a concrete suggestion.
→ See references/operations.md §6f.
Layer: test-infra. Mechanical extraction of the OPA bootstrap
block out of an OPA test HTML so the page survives the loss of the
legacy sandbox.js include.
What it does: extracts the inline <script> block content into a
sibling .js file (default name AllJourneys.js). Converts
QUnit 1.x globals and jQuery.sap.getUriParameters into modern
equivalents. Updates the test-suite index (testsuite.qunit.html)
to use URLSearchParams-style journey URLs.
→ See references/operations.md §6g.
Layer: test-infra (auto-fix where mechanical, report otherwise).
Identify code that hard-codes values which were free-form in the
legacy window["sap-ushell-config"] but are now derived from a
different source (or fixed entirely) under New Sandbox. Auto-fix the
safe, mechanical cases; report everything else.
A migrated app boots cleanly under New Sandbox even when this rule does nothing — the consequence is a test failure or runtime mismatch later, not a boot crash. That is why this is test-infra, not core: the source change has already been performed by §6a (which removes the legacy ushell-config write), and §6h only cleans up downstream fall-out.
The set of values is enumerated in
references/sandbox-config-surface.md §3 + §4.
Categories: A tile-derived (auto-fix), B user-profile (strict mode,
auto-fix), C personalization (strict mode, auto-fix), D fixed-by-
default (report only).
→ See references/operations.md §6h.
Layer: advisory. The actual rewrite lives in the
modernize-flp-sandbox-qunit sub-skill.
The main skill detects, reports, and offers to invoke the sub-skill
on consumer request.
Why surfaced by the migration: §6a removes
data-sap-ui-preload="async" and adds data-sap-ui-async="true" on
the test bootstrap. In that load-timing mode, QUnit 1.x globals
(module, ok, equal, …) and the direct-call form
QUnit.ok(...) are no longer defined when journey/page modules
execute. Bare references throw ReferenceError at module load.
The root cause is independent of the sandbox migration — the test code does not match the QUnit version. The migration just exposes it. That is why the actual rewrite is delegated to a stand-alone sub-skill the consumer can also invoke directly on apps that have not migrated.
→ See references/operations.md §6i.
Layer: test-infra (reporting only — no auto-rewrite). Detect OPA test code that targets FLP shell controls directly and surface concrete fix suggestions in the §8 report. Never rewrite test logic without consumer consent.
Why surfaced by the migration: the FLP shell renders increasingly
through UI5 Web Components in newer UI5 versions. App-level controls
and the app's own behavior are unaffected, but OPA tests that match
shell controls by sap.m.* control type or that call non-WebComponent
APIs on the shell header break silently or throw at runtime. The
exact version at which a given shell control switches is a moving
target — this rule does not encode specific version thresholds, it
just identifies the test patterns that are at risk.
Patterns: S1 = sap.m control + shell-only id, S2 = type:"Back"
matcher, S3 = non-WebComponent calls on the shell header
(getHeadItems, header.$("icon").click()).
→ See references/operations.md §6j.
Layer: core detection. Every deprecated service is a hard runtime
block at getService() / getServiceAsync() time — see
references/sandbox-config-surface.md §5.
An app that still consumes any of them does not boot under New Sandbox.
The skill scans app code, records every hit, and surfaces all findings in the Gap-Report under "Manual action required". No rewrite is attempted and no sub-skill is offered — the rewrite is non-trivial (sync→async transitions, API shape changes per service) and must be done by the developer.
→ See references/operations.md §6k.
Layer: test-infra (detection + ask). Detect journey tests whose behavior depends on the surrounding FLP shell, surface the list to the consumer, and — only if a recognised iframe-toggle pattern is present in the app — offer to apply it. Never modify a test without consent.
Signals: (a) shell-control selectors from the §6j catalog, (b) calls to deprecated-or-successor shell services from the §6k list, (c) convention-named page-object methods (heuristic).
Probes: P1 = existing startMyAppInAFrame flag, P2 =
iStartMyAppInAFrame helper, P3 = bespoke iframe URL builder.
→ See references/operations.md §6l.
Always emit the Gap-Report, whether successful or failed:
== New Sandbox Migration Report ==
App: <app name from package.json>
Path: <APP_ROOT>
Status: ✓ SUCCESSFUL | ✗ FAILED
── Automatically applied ──────────────────────────────────────────────
✓/✗ webapp/test/<filename>.html (HTML transformed)
✓/✗ webapp/test/fioriSandboxAppConfig.json (created)
✓/— webapp/test/sandbox/<hookModule>.js (created / reused / rewrote-legacy)
✓/— rta: "<fakeLRep filename>" (written to config)
✓/— opaTests.qunit.html (resource root rebound)
✓/— Common.js (scenario URL param injected)
✓/— Hook-graph: <N> trivial replacement(s) auto-applied
── Manual action required ─────────────────────────────────────────────
✗ <ServiceName> (deprecated — hard runtime error): <file>:<line>
→ <successor or removal instruction from sandbox-config-surface.md §5>
(one line per finding; section omitted if no deprecated services found)
✗ Custom FakeLrep connector at <path>
→ Own connector code may have sap/ushell dependencies incompatible
with beforeFlpStart. Migrate manually.
(omitted if not present)
✗ Hook-graph — architectural-relocatable: <module> uses <sap/ushell/X>
→ [diff sketch]
(one entry per hit; omitted if none)
✗ Hook-graph — manual-required: <module> uses <sap/ushell/X> at load time
→ [textual sketch]
(one entry per hit; omitted if none)
✗ <Layer-2 step> — pattern not recognised
→ [description of what was expected and what was found instead]
(one entry per unrecognised pattern; omitted if all Layer-2 steps ran cleanly)
── Advisory (no migration blocker) ───────────────────────────────────
ℹ <N> QUnit 1.x pattern(s) in <M> file(s) under webapp/test/integration/
(omitted if none)
ℹ Shell-control test patterns flagged: <N> in <M> files (S1: ?, S2: ?, S3: ?)
(omitted if none)
ℹ locate-reuse-libs.js: script tag removed — deprecated Fiori Tools pattern,
removal is safe. (omitted if not present)
ℹ <description of any other non-standard pattern and how it was handled>
→ Consider documenting this pattern in the Migration Guide.
(omitted if none)
[On failure: "Rolled back to original state. Partial attempt saved to webapp/test/.migration-attempt/"]
This step is mandatory. Execute it regardless of whether the migration completed successfully or failed.
externalUshellConfigFile[] (external ushell config files whose content has already been parsed and moved to fioriSandboxAppConfig.json).rm <obsolete-file-1> <obsolete-file-2> ...
Verify: confirm with ls webapp/test/flp*.html that only the migrated target file remains.
On any failure during step 6 (Migrate):
Save partial attempt:
mkdir -p $APP_ROOT/webapp/test/.migration-attempt
cp $APP_ROOT/webapp/test/*.html $APP_ROOT/webapp/test/.migration-attempt/ 2>/dev/null
cp $APP_ROOT/webapp/test/fioriSandboxAppConfig.json $APP_ROOT/webapp/test/.migration-attempt/ 2>/dev/null
Restore original state:
If git stash was used:
cd $APP_ROOT
git checkout -- webapp/test/
git stash pop
If backup directory was used:
cp $APP_ROOT/webapp/test/.migration-backup/* $APP_ROOT/webapp/test/
Emit the Gap-Report with status FAILED and note what failed.