| name | boot-and-verify |
| description | Use to take a transformed Spryker project from 'files written' to 'verified running' — the first boot at project start (the wizard's last mandatory step and commit point), re-applying data changes on an already-booted stack, or standalone per-store verification of storefront, Back Office, search, and queues. |
boot-and-verify
You take the project from "files written" to "verified running". You are the commit point: nothing here half-succeeds silently. Read real command output; a green boot is the authority.
DIAGNOSE YOUR OWN DELTA FIRST. When something that works in the fresh, un-transformed demoshop breaks after the transformation (a missing menu, an empty homepage, a failing add-to-cart, a 500), your own recent change is the prime suspect — the data/config you just wrote — NOT vendor or core code. The demoshop shipped this working; you changed it; the change is guilty until proven otherwise. So, in this order, BEFORE opening a single vendor/core file: (1) diff your delta — git diff --cached -- data/import config src and the import-tree layout — the dropped/renamed file is usually right there; (2) re-verify data completeness/consistency — did every needed entity import, are the references intact (preflight, refs, per-store×locale counts); (3) only then consider code. "It worked before my changes" from the developer is ground truth — treat the delta as the cause, do not argue it or go spelunking vendor/. A missing menu or empty page after a transformation is far more often a dropped/renamed import file than a vendor or core defect — check the delta before investigating an uninstalled widget or vendor package. Reporting "fixed" on DB/KV evidence while the developer's screen is unchanged is not a fix — verify by fetching the rendered page, not the read model (a correct read model is not evidence the page renders).
Locate the validate tool: the script is at .claude/skills/spryker-import-tools/scripts/validate.php (setup install, relative to the project cwd) or ${CLAUDE_PLUGIN_ROOT}/skills/spryker-import-tools/scripts/validate.php (plugin install); docker php:8.3-cli fallback if no host php. Invoke it by that literal path from the project cwd. $VALIDATE below is shorthand for that path — resolve it once in reasoning and substitute it inline; never set a shell variable (VALIDATE=…) and never cd — both prompt on every call (full rules: spryker-import-tools → Invocation & command discipline). sudo is never run by you — surface it (the /etc/hosts line) for the developer. docker/sdk is allowlisted only if the developer's allowlist already grants it — then boot/console run unattended; otherwise each docker/sdk prompts (fine — never work around it, and never edit their settings). rm (deleting anything) is deliberately not allowlisted → it prompts. Either way, never work around a denied command.
1. Validate before boot (cheap; a bad row aborts a 30–60 min install)
The shipped demoshop is NOT preflight-clean — it carries pre-existing quirks (shipped URL dups "not yours to fix") that read as self-inflicted damage. So the wizard's pre-flight should capture a baseline on the untouched clone — run preflight <manifest> plainly and Write its JSON to .ai-dev/preflight-baseline.json with the Write tool (not a > redirect, which can't be allowlisted and prompts) — and every later preflight here runs with --baseline .ai-dev/preflight-baseline.json — only NEW findings gate. The baseline NEVER suppresses a finding whose severity depends on the project's own config. The canonical case is tax_set_name: once the project's store countries differ from the shipped DE/AT, a tax_set_name mismatch is a live tax defect, not a shipped quirk — the importer auto-creates a referenced-but-undefined tax set rateless and checkout silently falls back to the core 19% default — a wrong-country VAT charged on the project's own store. Same test for any locale/country/store-derived finding: if the shipped value was correct for DE/AT and your project is not DE/AT, it is yours. Fix it per ../project-data/references/adapt.md step 3. Lead with php "$VALIDATE" preflight <import-config.yml> — one call sweeps url-<locale> global uniqueness, is_searchable.<locale> non-blank, price completeness (value_gross/value_net empty-or-0), and base-before-relation import order across every file the manifest imports, plus any missing source:. Then the targeted checks preflight doesn't cover: manifest-refs <import-config.yml> for the distinctive-key relations in one sweep (orphaned offer→product/merchant references, *_store→parent, sales-unit refs, etc.) — cheap first net, but NOT the whole FK graph: generic-column relations (product-label-store→label by name, stock-address→stock by warehouse_name) are invisible to it, so for every entity whose parent set the project replaced (labels, sales units, warehouses, attributes) also run a targeted refs --ref-file against the project's own parent, threshold-glossary <import-config.yml> --locales <project locales> (every Sales-Order-Threshold row's derived message key resolves in each project locale's glossary — the pre-boot form of the add-to-cart gate below), on every (store values ⊆ declared), for parent-in-store tuples, and on the must-be-clean files (, → zero hits). ( subsumes the old standalone // sweeps.) A // error → stop and fix; do NOT boot on red. over broader config is triage (intentional keeps like are fine) — not an automatic boot blocker.
2. Bootstrap
docker/sdk bootstrap deploy.dev.yml. It prints a sudo … >> /etc/hosts line listing the project's hosts (region+domain dependent). Capture that exact line. The boot and the HTTP-reachability probes run without it (they use curl --resolve), but the browser verification in §4c cannot — Chrome needs DNS. So /etc/hosts is a prerequisite for browser-based verification: surface the line as a ⚠ ACTION NEEDED item (per the Communication rules) before the §4c gate. If the developer adds it, the browser ACs run; if not, they are marked BLOCKED and the step still completes in the terminal state §4c defines (done (browser ACs BLOCKED — /etc/hosts declined)) — never a permanent in-progress.
The destructive-op gates (§3b) and any data defect the boot surfaces still consult the developer per the wizard's honesty rule.
3. Boot — unattended, in a detached pseudo-terminal
The first docker/sdk up needs two things at once: a real TTY (the mutagen mount and other steps run docker … -it) and detachment (it takes 30–60 min, far past the 10-minute tool cap). Foreground alone times out; background alone has no TTY. Get both by running it inside script (a real pty) via run_in_background. Boot does not read keyboard input — it runs to success or failure on its own — so a pty with no live input is enough.
- Start it (one command, allowlisted): on macOS —
script -q .ai-dev/boot.log docker/sdk up -t with run_in_background: true. (Linux variant: script -q -c 'docker/sdk up -t' .ai-dev/boot.log.) script supplies the TTY; run_in_background detaches it and re-invokes you when it exits. Do not hand-roll a PTY (python pty.spawn) — script is the purpose-built tool.
- Boot with
-t, always — not only when you intend to run tests. -t provides the testing container (so docker/sdk testing … exists at all) and sets SPRYKER_TESTING_ENABLED=1, which is what lets codeception inherit the real APPLICATION_ENV=docker.dev — whose overlay already configures the dummy OAuth provider that the ACP / MessageBroker suites need. On a plain up there is no testing container, docker/sdk testing is a silent no-op (exit 0, nothing run), and tests/_bootstrap.php falls back to a phantom devtest env for which this demoshop ships no config_default-devtest.php, so whole suites lose their service config. Signature: Unable to parse URI: https:///oauth/token plus translator-null errors → pure harness errors that read as project failures. An already-running plain stack upgrades non-destructively: just re-run up -t — volumes, DB and demodata all survive, so this is NOT a clean-data / §3b-rung-3 situation and needs no gate. Tried and rejected — do not re-derive: a config_default-devtest.php OAuth overlay, and a TranslatorServiceHelper for the translator errors. Both make the symptom disappear; both mask this cause.
- Scope
script to up and reset — the only docker/sdk commands that allocate a TTY. docker/sdk console … (data:import, queue:worker:start, publish:trigger-events) and npx cypress run need no TTY — run them directly. Wrapping them fails with and leaves an , which reads as an import or test run that died instantly: a wrapper failure misdiagnosed as a project failure. Do not generalize by imitation.
3b. Re-applying data changes on a booted stack — the iteration ladder (do NOT full-teardown)
Once the stack is up, a change to import data (an adapt fix, a catalog removal, a localization apply) must NOT be tested by tearing everything down. Full teardown rebuilds images + composer + frontend (~30–60 min); it is for code/deploy/service changes only. Climb the cheapest rung that does the job:
Destructive-command gate (reset, clean-data, any DB/volume drop) — explain, then ask, every time. Before you run one, tell the developer in ONE plain line what it will destroy — e.g. "reset on this SDK drops the database and search volumes: all imported data is wiped, and this is NOT recoverable by git" — and get an explicit go-ahead. Run it only after that yes, even if the allowlist would let it through without a prompt. Never run a destructive command unannounced.
- Validate the edit first — cheap, no teardown:
docker/sdk console data:import -c <config>. It re-runs the import, collects every importer failure, and runs to the end — so one pass surfaces every remaining orphan/missing-ref at once (read the Overall Import status + the abort strings above). This is the fast feedback loop for "did my CSV edit leave a dangling reference?" — use it to iterate to a clean import before touching the DB — don't spend a reset until the import is clean.
- Adds/updates only, no deletions: targeted
docker/sdk console data:import -c <config> (it upserts, and the importers emit publish events automatically via event-behavior) → drain the queue workers (§4a) so the KV/search read models catch up → re-verify. Nothing dropped. Do NOT routinely run publish:trigger-events — the import already queued the events; a manual re-publish is a recovery tool (see below), not a normal step. Two importer caveats (both cost a reset to discover):
- Not every importer upserts — some are INSERT-only.
discount-amount is confirmed insert-only (spy_discount_amount keyed (fk_discount, fk_currency), no store dim); re-importing existing rows fails with Unable to execute INSERT. cms-block is insert-only too — re-importing a block whose template changed fails with Unable to execute INSERT … spy_cms_block_glossary_key_mapping. If a re-import throws a unique-constraint INSERT error on an entity you did not add rows to, that entity is insert-only — it needs rung 3 (reset), not a data fix. Don't read the constraint failure as a CSV defect and start "fixing" correct data. And do NOT reach for a targeted SQL DELETE to work around an insert-only importer: SQL mutations bypass the event-behaviour that queues publish events, so the read model goes stale and publish:trigger-events does NOT rebuild it (after a SQL delete + re-import, kv:cms_block:… still holds the pre-delete payload through a targeted re-publish, a full publish:trigger-events, and every queue drained to 0 — only a DB drop + re-import restores coherence). Insert-only + a needed change → rung 3.
- Changing the VALUE of an already-imported row is NOT an update here — it duplicates. has no unique key on (price_product, store, currency), so a re-import of a changed price a second row instead of updating (symptom: row counts grow after a re-import meant to update in place). — only genuinely new rows are safe via targeted import.
demodata is the last install section, so an abort there means every build/frontend/scheduler/storage section already completed — iterating the import is cheap (no rebuild). Use rung 1 (data:import -c) to iterate the CSVs to a clean Overall Import status: OK. Do not then hand-run the remaining post-import steps individually as the final state: the recipe runs them in a defined order, and replaying a subset after an abort leaves an inconsistent stack (a later step whose earlier dependency never ran, or read-model state auto-created with the wrong shape). Once the data imports clean under rung 1, re-boot clean so the whole recipe runs in order — docker/sdk clean-data then a full script -q .ai-dev/boot.log docker/sdk up -t (background pty). A green first boot means the recipe ran to completion, not that the tail was hand-assembled. (Rung 1 iterates the fix; the clean re-boot produces the final state.)
Rule of thumb: adds → targeted data:import+publish (no teardown, no TTY); deletions → a DB drop via reset (weight SDK-dependent — light re-import or full destructive teardown; always TTY-pty + background, never a plain background shell); code/deploy change → clean-data+up. Always run the cheap data:import -c validation rung first, and confirm what reset does in this SDK before spending it.
Frontend-asset changes and the sync/cache discipline (a separate axis from the data ladder above):
- A
.scss/.ts change re-applies with docker/sdk up --assets — it re-runs only the build-static* sections on the running stack (non-destructive: no volumes, no DB, demodata stays [LOADED]). Do not use console frontend:*:build (it dies Cannot find module — node_modules isn't in the running cli container) and do not clean-data+up for an asset change. Twig-only changes need none of this — templates are read from the synced /data on the next request.
- Do NOT
npm install inside cli — /data is mutagen-synced, so it would push a large node_modules onto the host sync.
- After ANY command that restarts containers (
up, up --assets, up --build, restart), confirm the mutagen sync is alive before trusting anything — mutagen sync list → the project session must be Connected: Yes on both alpha and beta (not stuck Connecting to beta). A restart can leave the daemon down with no error, and then every host edit silently never reaches the container — invalidating every verification in that window (green console output run against code that isn't there). Running mutagen sync list itself restarts a dead daemon, so the first call is also the repair.
- Verify a file's presence in the CONTAINER, not on the host, before acting on it —
docker exec <namespace>_cli_1 ls /data/<path>; a host-side ls proves nothing about what the application sees.
- New project class override — strict order: write the file → confirm it exists in the container →
docker/sdk console cache:class-resolver:build → confirm the FQCN is in src/Generated/Shared/Kernel/<Orgs>/resolvableClassCache*.php. Rebuilding the resolver cache before the sync lands bakes a cache WITHOUT the class, which then keeps the override inactive even after the file finally syncs.
4. Verify per store (HTTP via --resolve; no /etc/hosts needed)
The gates run in a FIXED order — never probe ahead of an unpassed gate: volumes proven new → no stale generated artifacts → publish queues drained & error-free → per-store search-doc count > 0 → per-store HTTP (every kept app) + add-to-cart → the full store×locale×product grid. The mechanics of every gate and probe — exact commands, clients, schema notes, and the known traps — are in references/verify-recipes.md. Read it before running the probes and follow its recipes rather than hand-building commands (a hand-built probe easily produces a false verdict of its own).
4a0 — volumes are NEW (before trusting ANY KV or search assertion). docker/sdk up recreates the DATABASE but REUSES named volumes — a namespace collision leaves a fresh DB beside a prior project's KV/search/broker read models, and every signal reads correct while carrying foreign data (a severe false green — a fresh DB beside stale read models). Check age (KV _timestamp / index creation_date vs boot start) and identity (spy_store ids vs ids embedded in KV payloads) per the recipes. On a hit, resolve it via the wizard's ranked fix hierarchy (developer removes their own stale volumes / override only the deploy namespace: / a different project name — never force a rename), all strictly better than clean-data/reset. No KV/search verdict until this passes.
4a-gen — NAMED GATE: no generated artifact keyed by a token the transformation RENAMED. docker/sdk cli "find src/Generated data/cache -name '*<OLD_TOKEN>*'" must return nothing — run it once per renamed store / region / code-bucket / namespace token. Two seconds, and it catches the whole class: src/Generated/** and data/cache/** are gitignored build output, so no literal sweep, no git diff and no preflight sees them. Signature: after a fully green boot every API Platform request 500s with Validation cache is enabled, but cache file is not found → a validation<OLD_TOKEN>.cache left behind by a renamed code bucket. On a hit, delete the stale artifact and re-run the generator that owns it, then re-probe; never patch the consumer to accept the old name.
- Build-section-ordering trap — the pattern, with its three known instances. A recipe step in the
build section runs before the prerequisite it needs exists (an init'd DB, generated transfers, the resolved bucket), exits 0, and produces nothing or a wrong-keyed artifact — so the boot log is green and the artifact is absent. Named instances: rest-request-validation-cache-warmup (this gate), generate-transfer-databuilders (the DataBuilders check in the test-infra section below), configuration-sync (../project-starter-wizard/references/pitfalls.md). Any build-section step whose output you depend on is verified by its ARTIFACT after boot — never by the boot log.
4a — publish queues drained and error-free (before ANY HTTP probe). A green boot ≠ publish-and-sync finished; a storefront hit before the queues settle is a false 500, not a project defect. Check counts against the project vhost, not / (the default vhost lists nothing — a dangerous false "drained"); drain with docker/sdk console queue:worker:start --stop-when-empty and re-check to 0; inspect error queues (a small STABLE publish.*.error count can be a benign duplicate-key race — verify the underlying rows exist before treating it as a failure). Commands + traps: the recipes.
4a-search — NAMED GATE: per-store *_page product-doc count > 0 (via /<index>/_count, never _cat). The only signal that catches an unapproved / 0-search-doc catalog (missing product-approval-status → the publisher writes NOTHING while import, queues, and DB all read perfect). A 0 here with a green everything-else = an invisible unsellable catalog — fail the boot on it.
- NO FREE ATTRIBUTION TO "PRE-EXISTING". On a generate-mode run, "search dead + PDP dead while everything else is green" is the exact published signature of a DELTA defect, not a platform one (
../project-data/references/generate.md calls the missing product-approval-status "the nastiest… it defeats every signal the skills say to trust"). The top-of-file rule says your own change is guilty until proven otherwise; this is what proving takes. Rule out the delta in this order: (1) product-approval-status present and non-NULL for every abstract; (2) category_store rows for every category; (3) an abstract-level price row per store×currency (a concrete-only price filters the product out of catalog and search); (4) per-store *_page /_count. Then produce a POSITIVE CONTROL — reproduce the same failure on an untransformed demoshop, or on a freshly rebuilt index. The words "pre-existing", "platform bug" and "unrelated to our data" may not appear in any report until that control exists — with no control they are free to assert.
Per-store probes — the list is driven by what the project KEPT, symmetrically. Read services.applications_disabled from the state file and run an own functional probe for EVERY application that is NOT in it (probing a disabled app is a false failure; not probing a kept one is a false green). Glue is not the fallback for a disabled Yves — a kept glue is probed even when yves is kept, and glue-backend is on the list in its own right: total Storefront-API breakage (500 on every API Platform endpoint) passed every gate in this skill because Glue was modelled as the headless alternative. Per the recipes:
- if
yves kept: /<STORE>/<lang> → 200 with the anchored <html lang=; login/register → 200; the storefront logo renders (see the logo gate below); and a real add-to-cart per store×currency — follow the recipe exactly (log in first — the action is guest-restricted; pick a company user that holds a role; take the POST URL + field names from the rendered form; POST with -L on one cookie jar and read the flash). Add-to-cart is green only when the item is persisted: after the POST, re-fetch the cart (or the quote via authenticated call / SQL) and confirm the SKU is present and the quote is non-empty. A 200 is not success — a 200 with an error flash or an empty quote is a FAIL (a missing store×currency sales-order-threshold.* glossary key throws MissingTranslationException; a mis-valued threshold rejects the item silently, both while returning 200). Assert with anchored selectors (data-qa=, itemprop="price"), never bare substrings; verify prices via the KV key or an authenticated call, never guest HTML (B2B hides guest prices by design).
- Logo gate (
yves kept). brand-project sets theme:logos:logos:yves_logo_url to the project's served logo asset. Confirm the storefront header renders that asset, not Spryker's build-with-logo fallback icon — fetch the rendered header and check the logo src points at the project asset path. If it still shows the fallback, the config value didn't take (empty :root { } = configuration:sync ran before the DB existed — re-run it; a type: file setting may otherwise need the Back Office media upload); resolve it rather than noting "logo missing". Then assert the logo's rendered box is non-zero (getBoundingClientRect() width/height > 0 in the browser), not just that the asset returns 200: the theme's configured-logo branch renders a bare unsized <img> (only the SVG-icon fallback branch carries a size via .icon--logo), so a correctly-configured logo can still render at 0×0 with every server-side signal green and the page showing nothing. The fix — a logo__image class + explicit dimensions in logo.scss — is brand-project's; the gate is measuring the rendered box.
- if
yves kept, additionally — one GUEST CHECKOUT JOURNEY for the primal store (cart → checkout → place order → spy_sales_order row + OMS initial state + one transactional mail rendered). The add-to-cart probe stops short of the three things this skill set curates but otherwise never exercises — checkout tax, the bodies, and the T&C page. Primal store only (cost-bounded); recipe + honesty rules in the recipes file.
Verify at the granularity that can BREAK — never trust an aggregate total as "verified". The single most important verification rule, and the easiest to get wrong — an "all verified ✅" on an aggregate routinely hides a broken per-locale or per-product slice. Assert the full grid: per store × each locale (localized attributes + *_page search docs non-zero and roughly-equal per locale) and per store × product for prices (empty-or-0 = missing). When you write "verified", name the grid you checked (<stores> × <locales> × <products>), not a total. A rendered widget is NOT evidence its backing data imported — confirm by row count, never by the DOM.
The URL contract is /<STORE>/<lang>/… — on the one region host every store is browsable via its path prefix; a bare /<lang>/… silently falls back to the DEFAULT store+locale and produces convincing false negatives. Never conclude a store unreachable until you've probed it WITH its store prefix (mechanics + the Glue host-resolution contrast: the recipes).
Recovery (only when a read model is genuinely out of sync): publish:trigger-events is a recovery tool, never a routine post-import step — and it can NOT rebuild a storage whose events were never emitted (a direct SQL mutation orphans the read model; only a DB drop + re-import restores coherence, see §3b). Search-index rebuild steps and the resource-name trap (price_product_abstract, not product_abstract, for price KV) are in the recipes.
4c. Independent verification gate — the authoritative PASS/FAIL
The 4a–4b probes are the executor's own checks. The authoritative verdict comes from an independent verifier sub-agent, so a green result doesn't depend on the same agent that wrote the data judging its own work (read models can read correct while the rendered page is broken).
- Spawn the
spryker-verifier agent (shipped in this plugin's agents/; it drives the app via the bundled spryker-qa-coverage + spryker-runtime skills). Give it only the acceptance criteria derived from .ai-dev/project-setup.md (stores, locales, currencies, kept apps, theme, whether a logo was provided) — not the executor's own conclusions. It asserts from scratch and returns PASS / FAIL / BLOCKED per AC with raw evidence; it never fixes.
- ACs to hand it (per store, per the store's locales/currency where relevant): storefront reachable per store×locale; catalog/search returns products (rendered product-item count > 0, not just HTTP 200 — the abstract-price / approval-status traps make this a real failure surface); PDP renders with the correct-currency price; add-to-cart persists an item per store×currency (re-fetch the cart — a 200 with an empty quote or a
MissingTranslationException/threshold flash is a FAIL); the storefront logo renders the project asset, not the Spryker fallback. Plus:
- Homepage content: the storefront root renders ≥ N slot blocks (the
slt-2 carousel's slides + ≥ 1 below-the-fold section, anchored selectors). A 200 with zero rendered blocks is a FAIL.
- Listing images: on a catalog listing page the first product tile's
img src resolves to a served asset (fetch it: 200 + non-zero body) — "tiles render" / "product-item count > 0" is satisfied by an all-broken-thumbnail listing.
- Authorization boundary: anonymous
/{STORE}/{locale}/customer/overview 302s to login — the redirect asserted, not "not 500" — for every locale, plus no bare dotted.glossary.key in the visible text of any customer-facing page.
- Every kept app, individually (per
services.applications_disabled, one functional probe each, not a root status): yves; backoffice login → dashboard + a non-empty product/variant list; merchant-portal root + login form; glue → POST /access-tokens returns a JSON:API token (not the raw OAuth /token) + one authenticated resource 200; glue-backend → token + one authenticated registered backend resource 200. 406 on the root and on the root are CORRECT — not findings.
Test-infrastructure checks — mandatory checklist items, on par with the endpoint checks (both run post-boot, once the boot's composer install/npm ci have provided the tooling):
- PHP / codeception (only if a custom namespace was set — skip for keep-Pyz).
configure-codebase created the runnable seed module tests/<Ns>Test/Shared/Example/ pre-boot but could not execute it (no composer install yet). Close the loop in two steps — the seed run is a pre-check, the full suite is the gate:
- Fast pre-check (seed only):
docker/sdk cli vendor/bin/codecept build, then docker/sdk cli "vendor/bin/codecept run -c tests/<Ns>Test/Shared/Example Example" → expect OK. A green run means the namespace's suite is discovered, its actor/support autoloads (test-autoload.php branch), and projectNamespaces: ['<Ns>','Pyz'] resolves src/<Ns> — and nothing more. It proves only the seed suite: codeception cannot reach PyzTest from here, because passing an explicit suite name suppresses include: traversal and -c re-roots onto the seed module's own config, which has no include: key. A green pre-check is not a test verdict — never report it as "tests pass".
- NAMED GATE — the full inherited functional suite:
docker/sdk testing codecept run -c tests/codeception.ci.functional.yml. Assertion: 0 errors and 0 failures, with only upstream skips — and report the counts (tests / assertions / errors / skips), never the word "green" alone. This gate runs BOTH test trees — PyzTest/*/* AND <Ns>Test/*/* — and a run covering only one of them is a misconfiguration, not a pass. Both are in scope only because configure-codebase added <Ns>Test/*/* to this config's include (and src/<Ns> to its whitelist) alongside the shipped PyzTest/*/*, and swept projectNamespaces — that skill owns the greps that prove it. An unpatched aggregate config runs the inherited suites against src/Pyz config while the app runs src/<Ns>, i.e. green on the wrong values.
- Two prerequisites — check BOTH before you interpret a red result: (1) the
-t boot (§3 — without it docker/sdk testing is a silent no-op and this gate reports a phantom pass); (2) the ( — it is kept because the tests need it). / missing-service-config errors → the boot lacked ; translator-null / errors → the glossary layer was removed. Fix the prerequisite and re-run; do not report it as a project failure and never "fix" the tests. A red run that matches neither, and a red/empty pre-check, are findings — report them, don't paper over them. (Needs at boot.)
5. Close
- Stage the changed/created project files (
git add); never branch or commit. Exclude the transient run artifacts — do not git add -A .ai-dev (it stages boot.log/di.log/reset.log and decision-log.md, which are run artifacts/dev scaffolding, not project files; staging them forces a git reset). Stage only .ai-dev/project-setup.md from that dir, or gitignore the rest. The state file's version control is the developer's decision — say so.
- Update the
boot-and-verify step in the state file. Write done only when §4c's independent verifier returns all-PASS. The 4a–4b probes (volumes-new, no stale generated artifacts, queues drained, per-store×locale search-docs > 0, per-store HTTP for every kept app, add-to-cart persisted, the logo gate, the homepage-content gate, the anonymous/boundaries pass) and the post-boot test-infra checks (the full codeception.ci.functional.yml suite + cy:run) are necessary but not sufficient on their own. If any gate is red, unverified, or the verifier returned a FAIL (or a wizard-clearable BLOCKED), the status is failed (where) or in-progress (what's left), not done — the sole terminal exception is a developer-declined /etc/hosts, which finishes as done (browser ACs BLOCKED — /etc/hosts declined) per §4c. A 200-but-empty cart is not a working shop — do not report success on read-model evidence while a rendered path (add-to-cart, logo, menu) is broken.
- OPERATOR CONSENT CANNOT CLEAR A RED GATE. A red gate on a customer-facing surface (homepage blocks, listing/PDP images, search, PDP, add-to-cart, logo, menu, the authorization boundary) is never resolvable by asking permission. Do not offer "fix them now / leave as known issues", do not present a known-broken surface as a decision, and do not read a "leave it" answer as closing the step — that converts a FAIL into a closed run by asking, which is exactly the escape hatch this section forbids. The step records
failed (where) / in-progress (what's left) and the defect goes onto the ## Go-live debt list. "Leave as a known issue" is not an available option: an operator can decide to stop working on it, but cannot make a red gate done. The only sanctioned terminal-with-caveat state remains the developer-declined case above.
Reuse
Step 4 (verify) is reusable standalone by later live operations (add-store, cleanup reboot, go-live language checks) — same probes, no boot.