From 6563262dec6c4fbc00d3ff11c63ecea839499d28 Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 24 Sep 2026 13:24:18 -0700 Subject: [PATCH 01/84] Add the Foundry spec for this flight docs/SPEC.md wraps tests/smoke/SPEC.md in the shape the Foundry planner reads, adding the repository's documentation, journal, and no-publish rules. docs/foundry.json is pre-seeded with the verify commands and auto permission mode for an unattended run. --- docs/SPEC.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++ docs/foundry.json | 15 ++++++ 2 files changed, 141 insertions(+) create mode 100644 docs/SPEC.md create mode 100644 docs/foundry.json diff --git a/docs/SPEC.md b/docs/SPEC.md new file mode 100644 index 0000000..c66e79e --- /dev/null +++ b/docs/SPEC.md @@ -0,0 +1,126 @@ +# WP-CLI smoke test — Specification + +Version: 1.0 +Status: ready + +This is the Foundry wrapper for this flight. **The design lives in `tests/smoke/SPEC.md`.** Read it in full. +It is authoritative for every behaviour, file name, and test it names. Where it and this file +disagree on process, this file wins. Where they disagree on design, `tests/smoke/SPEC.md` wins. + +Background, read before planning: `docs/decisions/0008-the-trac-ticket-replaces-thread-confirmation.md`, +`docs/decisions/0007-fail-closed-on-a-broken-drop-in.md`, `docs/spec/providers-and-keyrings.md`, +`docs/spec/extension-points.md`, `docs/journal/test-coverage-gaps.md`, and +`docs/journal/2026-09-04-0-1-0-is-public.md` (the voice for the journal entry). + +## 1. Overview + +Build the WP-CLI smoke test described in the detailed spec: a dependency-free bash harness that drives a real `wp` binary against a throwaway install. It covers subcommand and flag registration, the 0/1/2 exit-code contract, rotation from end to end, drop-in loading through the real loader, and a multisite pass. It joins `make ci` and gets its own CI job. + +Done means the detailed spec's "Done when" section is met, except for the steps it marks as +human or live-cloud, which are left as `Manual check: NOT VERIFIED (human)`. The branch must also +carry the documentation and journal entry described in §2. + +## 2. Goals and non-goals + +- Goal: every deliverable in `tests/smoke/SPEC.md`, with tests written in the same task as the code. +- Goal: the documentation matches the code. Update every page under `docs/` whose statements + this work changes: spec pages ("As built" and "Why"), the journal tracking pages + (`open-questions.md`, `test-coverage-gaps.md`, `proposal-questions.md`), `examples/README.md`, + `README.md`, and `docs/index.md` for any new page. Regenerate `docs/reference/` whenever a + docblock changes. +- Goal: **one dev journal entry** for this piece of work, at + `docs/journal/YYYY-MM-DD-testing-the-cli-for-real.md`, dated the day it is written, with frontmatter + `title`, `description`, and `date`. Write it in the voice of + `docs/journal/2026-09-04-0-1-0-is-public.md`: first person, plain, specific. Cover what was + built, what it found (especially anything that changed `src/` or an interface), what was + deliberately left out, and what it means for the Trac patch. Link to the example or test and to + ADR 0008. Do not use the `/journal-entry` skill, because it reads and clears the shared + `_drafts/notes.md`. +- Non-goal: anything the detailed spec lists as out of scope. +- Non-goal: the Trac patch itself, a release, a tag, or publishing the docs site. + +## 3. Engineering principles + +- **Read first.** Before any task, read `CONTRIBUTING.md` and the "Working in this repository" section of `CLAUDE.md`. Both bind every task. +- **Keep the existing `CLAUDE.md` content.** When the plan stage writes `CLAUDE.md`, keep the current `# Working in this repository` section verbatim at the top and add the Foundry headings below it. Removing or rewording that section is a review failure. +- **`src/` is copy-ready for core.** Use core's coding standard, the `default` text domain, and `@since 7.2.0`, with no `function_exists()` guards. `tests/phpunit/test-architecture.php` enforces this. `plugin/` and `cli/` are never copied into core. +- **Errors, not exceptions.** Public API functions return `WP_Error` (or `false`) and never throw. A caller error is `_doing_it_wrong()` plus `WP_Error( WP_SECRETS_ERROR_INVALID_ARGUMENT )`. +- **No plaintext in output.** A plaintext secret or raw key material never appears in a log line, a `WP_Error` message, CLI output (except `get --reveal`), a test failure message, or a persistent cache. The reviewer checks this by reading. +- **Tests only get stronger.** Never delete or weaken an existing test. Never skip a test except for an environment gate, such as multisite-only. Every `phpcs:ignore` and every new `phpcs.xml.dist` exclusion carries a reason on the same line. +- **Generated reference.** `docs/reference/` is generated. When a docblock changes, run `make reference` and commit the result in the same task. `make reference-check` must pass. +- **Spec pages** under `docs/spec/` keep exactly three sections, in this order: **As proposed**, **As built**, **Why**. A behaviour change updates "As built", and "Why" if the code now departs from the proposal. +- **ADRs.** A new design decision gets an ADR under `docs/decisions/`, numbered `NNNN-slug.md` after the highest existing number, with number, title, date, status, context, decision, and consequences, in the existing ADRs' style. Two other flights may also add ADRs, so pick the next number and expect it to be renumbered at merge. +- **Nothing private in `docs/`.** Never mention employers, customers, or internal channels there. +- **Never publish.** Never run `sf publish`, never create or push a tag, and never touch Spacefast settings. Publishing happens after merge, by a human. +- **Commit style.** Commit messages follow `git log`: an imperative title, then a body explaining why, wrapped at 72 columns. Foundry's `: ` title prefix is fine. +- **Examples are single files.** Each example under `examples/` is a single-file drop-in with no Composer and no SDK, and stays excluded from `make ci`'s lint. It is written to be read from top to bottom. +- **Parallel flights.** Two other branches are being built from the same `main` at the same time: `build/kms-keyring` (which adds `wp secret rotate --from` and `make test-examples`) and `build/vault-provider`. Do not do their work. Keep edits to shared files (`Makefile`, `.github/workflows/ci.yml`, `examples/README.md`, the `docs/journal/*.md` tracking pages, `docs/index.md`) additive and confined to your own section or entry, to make the merge easy. + +## 4. Architecture + +- `src/wp-includes/`: the API as it will ship in core. Change it only where the detailed spec + says to. +- `plugin/`: the plugin-only upgrade path from the prototype. Do not touch it. +- `cli/`: the WP-CLI commands, which are plugin-only. +- `tests/phpunit/` and `tests/includes/`: the PHPUnit suite and its shared base classes, mocks, + and conformance suites. +- `examples//`: single-file platform drop-ins, plus their own `README.md` and `tests/`. +- `docs/`: the published documentation site's source (`site/` only renders it). +- `bin/`: developer and CI scripts. + +## 5. Data and configuration + +Every configuration constant and default is named in `tests/smoke/SPEC.md`. There are no tunables to invent. +If a timeout or limit is not given there, mark it `⚠️ ASSUMPTION`, give it a named constant in the +example file, and justify it in a comment. + +## 6. Interfaces + +`WP_Secrets_Provider`, `WP_Secrets_Keyring`, and `WP_Secrets_Store` in `src/wp-includes/`. The +provider contract is also documented in `docs/spec/extension-points.md`. The conformance suite +lives in `tests/includes/class-wp-secrets-provider-conformance.php`. Do not change an interface's +method signatures. A docblock clarification is allowed where the detailed spec calls for one. + +## 7. Commands + +- Full verification: `bin/ci-local.sh --keep`, which runs lint, compat, phpstan, and the + single-site and multisite PHPUnit suites inside this worktree's own wp-env. Give it a 30-minute + timeout. Then `make reference-check`. +- This worktree's wp-env ports are 8930 and 8931, set in the git-ignored + `.wp-env.override.json`, which already exists. Never edit or commit it. Never run + `wp-env destroy`. +- Run a single test file fast with + `npx @wordpress/env run --env-cwd=wp-content/plugins/cli-smoke tests-cli vendor/bin/phpunit `. +- Service containers: No extra containers. The smoke install needs a MySQL database called `wordpress_smoke`. When it runs through `bin/ci-local.sh`, use wp-env's MySQL from inside the `cli` container. In CI, use the job's MySQL service. Never use `wordpress_test`. +- `docs/foundry.json` has been pre-seeded. The plan stage must keep `baseBranch: "main"`, + `branchPrefix: "build/"`, `permissionMode: "auto"`, and the two `verify` commands with their + timeouts exactly as they are. It may add `extraVerify` entries only for make targets that + already exist when the entry is first exercised. + +## 8. Phases + +1. **Install harness:** `bin/smoke-install.sh` with a pinned `wp-cli.phar` and its SHA-256, `.smoke/` added to `.gitignore`, and a `make smoke` target that for now only provisions the install. +2. **Registration cases (A)** and the TAP-style helpers in `tests/smoke/smoke.sh`. +3. **Behaviour and exit-code cases (B).** +4. **Rotation (C) and drop-in loading (D)**, including the `EXIT` trap cleanup. +5. **Multisite pass (E).** Wire `smoke` into `make ci`, into `bin/ci-local.sh`, and into a `smoke` CI job on PHP 7.4 and 8.3. +6. **Regression proof.** For each of the three historical bugs in the detailed spec's "Done when", reintroduce it on a scratch commit, confirm `make smoke` fails, revert, and record the evidence in the task's commit message. Never commit the reintroduced bug to the branch's history in a passing state. +7. **Documentation and journal.** See §2. Update `docs/journal/test-coverage-gaps.md` as the detailed spec's "Done when" says, add `make smoke` to `docs/reference/ci.md` if that page documents make targets, and cover it in `README.md`. Manual check: `make smoke` on a clean checkout, marked NOT VERIFIED (human). + +Every phase ends by pushing the branch. Each phase's manual check is whatever the detailed spec +lists as human or live-cloud. Mark it `NOT VERIFIED (human)` and move on. + +## 9. Open questions + +- The shared examples harness (KMS spec §5) is built by `build/kms-keyring`. Where another + flight also needs it, it builds a compatible subset with identical names, and the two are + reconciled when the branches merge. Decision: accept that merge cost rather than serialise the + flights. +- If a finding would change an interface's signature, stop and record it. Write an entry in + `docs/journal/open-questions.md` and say so in the journal entry, rather than changing the + signature. That is for the Trac ticket to decide. + +## Appendix + +Only this flight's detailed spec, `tests/smoke/SPEC.md`, is in scope. Everything else in `docs/` is +published documentation, to read for context and update as §2 requires. diff --git a/docs/foundry.json b/docs/foundry.json new file mode 100644 index 0000000..bf5a94e --- /dev/null +++ b/docs/foundry.json @@ -0,0 +1,15 @@ +{ + "verify": [ + { + "cmd": "bin/ci-local.sh --keep", + "timeoutMs": 1800000 + }, + { + "cmd": "make reference-check", + "timeoutMs": 120000 + } + ], + "baseBranch": "main", + "branchPrefix": "build/", + "permissionMode": "auto" +} From 77dcb0bcefc7e0918310938e258e57aaf91a1ebe Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 24 Sep 2026 13:46:13 -0700 Subject: [PATCH 02/84] chore: pipeline friction (plan) --- .foundry/feedback.jsonl | 1 + 1 file changed, 1 insertion(+) create mode 100644 .foundry/feedback.jsonl diff --git a/.foundry/feedback.jsonl b/.foundry/feedback.jsonl new file mode 100644 index 0000000..18ae92c --- /dev/null +++ b/.foundry/feedback.jsonl @@ -0,0 +1 @@ +{"at":"2026-09-24T20:46:13.325Z","stage":"plan","round":0,"category":"ambiguous-prompt","message":"The plan-build skill says to set docs/foundry.json baseBranch to the branch the planner is on (here build/cli-smoke), but the project's docs/SPEC.md §7 orders the plan stage to keep the pre-seeded baseBranch \"main\". The two directives contradict when the operator pre-creates a build/ worktree; I followed SPEC and kept \"main\". The skill should say which wins when SPEC pre-seeds foundry.json.","source":"agent"} From 91bc3d31e7fbcb35372cf3cc5a7134a6b1a91764 Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 24 Sep 2026 13:46:18 -0700 Subject: [PATCH 03/84] plan: derive build plan from SPEC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twenty tasks in seven phases, following docs/SPEC.md §8. The design is tests/smoke/SPEC.md; every task cites the section it implements. The pre-seeded docs/foundry.json keeps baseBranch, branchPrefix, permissionMode, and both verify commands unchanged and gains thirteen self-testing constraints. CLAUDE.md keeps the existing 'Working in this repository' section verbatim and adds the Foundry headings below it. --- CLAUDE.md | 83 +++++++++ docs/PLAN.md | 439 ++++++++++++++++++++++++++++++++++++++++++++++ docs/PROGRESS.md | 28 +++ docs/foundry.json | 108 +++++++++++- 4 files changed, 657 insertions(+), 1 deletion(-) create mode 100644 docs/PLAN.md create mode 100644 docs/PROGRESS.md diff --git a/CLAUDE.md b/CLAUDE.md index 4340969..d0dcd9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,3 +26,86 @@ those do not. automatically through `.github/workflows/docs-publish.yml`. - Never change Space access settings or domains from an agent session. Those are the owner's decisions, made in the Spacefast dashboard. + +## Principles + +- Read `CONTRIBUTING.md` and the section above before every task. Both bind every task. +- The design is `tests/smoke/SPEC.md`; the process is `docs/SPEC.md`. Design conflicts go to the + first, process conflicts to the second. Read the task's cited sections before writing. +- `src/` is copy-ready for core: core's standard, `default` text domain, `@since 7.2.0`, no + `function_exists()` guards. `plugin/` and `cli/` are never copied. Do not touch `plugin/`. +- Errors, not exceptions: public API functions return `WP_Error` or `false`, never throw. +- No plaintext in output: a secret value or key never appears in a log line, error message, CLI + output (except `get --reveal`), test diagnostic, or cache. +- Tests only get stronger: never delete, weaken, or skip one except for an environment gate. +- Tests land in the same commit as the code they cover. One task, one commit, small and scoped. +- Absent, present, and broken are three states. `get` exits 0, 1, 2. Never collapse them. +- Never publish: no `sf publish`, no tag, no Spacefast changes. A human does that after merge. +- Parallel flights `build/kms-keyring` (adds `rotate --from`, `make test-examples`) and + `build/vault-provider` build from the same `main`. Do not do their work; keep edits to + `Makefile`, `ci.yml`, `examples/README.md`, `docs/index.md`, and the journal tracking pages + additive and confined to this flight's own lines. + +## Commands + +- Full verification (30-minute timeout): `bin/ci-local.sh --keep`, then `make reference-check`. +- Smoke install inside wp-env: + `npx @wordpress/env run --env-cwd=wp-content/plugins/cli-smoke cli env DB_HOST=mysql DB_USER=root DB_PASS=password bash bin/smoke-install.sh` +- Smoke run inside wp-env: + `npx @wordpress/env run --env-cwd=wp-content/plugins/cli-smoke cli bash tests/smoke/smoke.sh` +- One PHPUnit file: `npx @wordpress/env run --env-cwd=wp-content/plugins/cli-smoke tests-cli vendor/bin/phpunit ` +- On a host with MySQL (no Docker): `make smoke` (`SMOKE_DB_NAME`, `DB_USER`, `DB_PASS`, `DB_HOST`). +- Regenerate reference docs after a docblock change: `make reference`. +- This worktree's wp-env ports are in the git-ignored `.wp-env.override.json`. Never edit or + commit it. Never run `wp-env destroy`. + +## Module map + +- `src/wp-includes/`: the API as it ships in core. `secrets.php` (functions, error codes, + provider resolution), `WP_Secret`, `WP_Secret_Version`, the three interfaces + (`WP_Secrets_Provider`, `WP_Secrets_Store`, `WP_Secrets_Keyring`), the libsodium provider, + option store, config keyring, key manager, cipher, and the three `Broken_*` fail-closed classes. +- `src/wp-admin/includes/secrets-site-health.php`: Site Health tests and debug info. +- `secrets-api.php`: bootstrap, the no-op gate, `wp_secrets_api_load_dropin()`. +- `plugin/`: prototype legacy reader, migrator, fallback store. Do not touch. +- `cli/`: `WP_CLI_Secret_Command` (11 subcommands) and its network subclass. +- `tests/phpunit/`, `tests/includes/`: PHPUnit suite, mocks, conformance suite. +- `tests/smoke/smoke.sh`: the bash harness this flight builds. `bin/smoke-install.sh`: its + throwaway install in `.smoke/` (git-ignored). +- `examples//`: single-file drop-ins, excluded from lint. `docs/`: the published site source. + +## Constraints + +Each line that is a single-line pattern is also a rule in `docs/foundry.json` `constraints`. + +- No `set -x` / `xtrace` in `bin/smoke-install.sh` or `tests/smoke/smoke.sh`. +- No bash-4-only syntax (`declare -A`, `mapfile`, `readarray`, `${x,,}`) in the smoke scripts. +- No bare `wp` in the smoke scripts; only `"${WP[@]}"` (pinned phar, `--path`, `-d display_errors=stderr`). +- No `wp-env` or `npx` inside the smoke scripts. +- The smoke database is never assigned or defaulted to `wordpress_test`. +- A TAP diagnostic (`not_ok`, `diag`) never interpolates `$OUT`, a `VALUE*`, `KEY*`, `OLD`, or `NEW`. +- No `wp-env destroy` and no literal `8930`/`8931` in `bin/`, `tests/`, `Makefile`, `.github/`. +- No `sf publish`, `git tag`, or `git push --tags` in `bin/`, `tests/`, `Makefile`, `ci.yml`. +- Every `uses:` in `.github/workflows/` is a 40-hex SHA pin. +- No `apply_filters(` and no `function_exists('wp_…')`/`class_exists('WP_…')` under `src/`. +- Every `phpcs:ignore`/`phpcs:disable` carries ` -- reason` on the same line. +- Reviewer checks by reading: no plaintext or key in any output path; the three interface files + have no signature change (`git diff main -- src/wp-includes/interface-*.php` is empty or + docblock-only); spec pages keep exactly three sections; the `# Working in this repository` + section above is verbatim; edits to shared files are additive; no test weakened; `.smoke/` + and `.wp-env.override.json` are untracked (`git ls-files`); `docs/reference/{functions, + classes,hooks,wp-cli}.md` change only via `make reference`; nothing private under `docs/`. + +## Commit template + +Title: `: `. Body wrapped at 72 columns: + +``` +Goal: +Tests: +Interpretation: +Measurement: +Manual check: +``` + +`docs/SPEC.md` and `tests/smoke/SPEC.md` win over `docs/PLAN.md`, which wins over code comments. diff --git a/docs/PLAN.md b/docs/PLAN.md new file mode 100644 index 0000000..86d7d43 --- /dev/null +++ b/docs/PLAN.md @@ -0,0 +1,439 @@ +# WP-CLI smoke test build plan +Derived from docs/SPEC.md v1.0 on 2026-09-24. SPEC.md wins over this file. + +`docs/SPEC.md` is the process wrapper; the design is `tests/smoke/SPEC.md` (called "the +detailed spec" below). Where the two disagree on design, the detailed spec wins; on process, +`docs/SPEC.md` wins. Every task below cites one or both. + +## Decisions + +- Shared examples harness (docs/SPEC.md §9) → not needed by this flight. Nothing here runs an + example's provider through the CLI (detailed spec "Out of scope"), so no subset is built and + there is nothing to reconcile at merge. +- A finding that would change an interface signature (docs/SPEC.md §9) → never change the + signature. Record it under a new heading in `docs/journal/open-questions.md`, say so in the + journal entry, and leave the decision to the Trac ticket. P7-01 and P7-03 carry this rule. +- `rotate --from=config` (detailed spec case C, third bullet) → deferred. It belongs to + `build/kms-keyring`. P4-01 leaves a one-line comment naming the case; nothing else. +- Flag table check (case A, second bullet) → exact-set comparison. The set of `--flags` parsed + from the SYNOPSIS section of `wp help secret ` must equal the table row exactly, in both + directions. That is the only reading under which "a new flag without a row fails loudly" is + true. When `build/kms-keyring` merges and adds `--from` to `rotate`, the row gains `--from`; + that is the accepted merge cost. +- Where the multisite conversion lives → inside `tests/smoke/smoke.sh`, after the single-site + pass. `make smoke` is then `bin/smoke-install.sh` followed by `tests/smoke/smoke.sh`, and + `bin/ci-local.sh` runs those same two scripts inside the wp-env `cli` container, which has no + `make`. One code path for both routes. +- How `wp` is invoked → only ever through the array + `WP=( php -d display_errors=stderr -d log_errors=0 "$SMOKE_DIR/wp-cli.phar" --path="$SMOKE_DIR/wordpress" --allow-root )`. + The two `-d` flags make a PHP fatal land on stderr and nowhere else, which case D's + uncatchable-fatal row relies on. `--allow-root` is harmless when not root. +- Database creation → a `php -r` snippet using `mysqli` (guaranteed wherever WordPress runs) + that drops and recreates `$SMOKE_DB_NAME`. No `wp db` subcommand and no `mysql` client + binary, so the scripts stay dependency-free beyond `wp` and `php`. +- WordPress version for the smoke install → `WP_VERSION`, default `latest`, the same variable + and default as `make install`. Not pinned, matching the PHPUnit jobs. +- WP-CLI pin → the implementer resolves the newest stable `wp-cli/wp-cli` release at build + time, verifies the download against the release's published checksum, computes its SHA-256, + and pins both version and SHA-256 as constants in `bin/smoke-install.sh`. The commit message + records the resolution. +- Smoke install URL and identities → `SMOKE_URL` default `http://smoke.test`; admin user + `smoke` with a random, never-printed password; multisite site 2 slug `smoke2`. No DNS is + needed: nothing serves the install over HTTP. +- "`dropin` reports it broken" (case D) → assert stdout contains + `Provider: WP_Secrets_Broken_Provider`. That line is what the command prints when the loader + set `wp_secrets_dropin_broken`. +- Case A registration for `network-secret` → `wp cli has-command "network-secret "` for + all 11 subcommands on the single-site install (WP-CLI instantiates a command class only at + invocation, so the constructor's multisite refusal does not fire). The help flag table is + checked for `secret` only, exactly as the detailed spec words it. +- No ADR for this flight. The design decisions are already recorded in `tests/smoke/SPEC.md` + and ADR 0008; an ADR is written only if a finding changes the API's design, which + docs/SPEC.md §9 routes to `open-questions.md` instead. +- `docs/reference/ci.md` is hand-maintained (the generator writes only `functions.md`, + `classes.md`, `hooks.md`, `wp-cli.md`), so P7-02 edits it directly, as docs/SPEC.md §8.7 asks. +- No ⚠️ ASSUMPTION exists in either spec. If a task finds it needs a timeout or limit, it names + it as an upper-case `SMOKE_*` variable at the top of the script with a justifying comment and + says so in the commit message. No tuning tasks are planned because there is nothing to tune. +- Regression proof (P6-01) → edits are made in the working tree only, reverted with + `git checkout -- `, and never committed. `git stash` is not used. +- Phase numbering follows docs/SPEC.md §8 (Phase 1 to Phase 7). Task IDs are `P-`. + +## Conventions + +Commit title `: `; body wrapped at 72 columns with the headings +`Goal:`, `Tests:`, `Interpretation:`, `Manual check:` (and `Measurement:` only for a tuning +task). See `CLAUDE.md` "Commit template". + +Shared vocabulary every task uses; restated here so no task depends on another's text: + +- `SMOKE_DIR`: `.smoke` at the repository root, git-ignored. Holds `wp-cli.phar`, `cache/` + (`WP_CLI_CACHE_DIR`), and `wordpress/`. +- `WP` array: see Decisions. Both scripts define it identically and never call a bare `wp`. +- Environment read by `bin/smoke-install.sh` (defaults in parentheses): `SMOKE_DB_NAME` + (`wordpress_smoke`), `DB_USER` (`root`), `DB_PASS` (empty), `DB_HOST` (`127.0.0.1`), + `WP_VERSION` (`latest`), `SMOKE_URL` (`http://smoke.test`). +- Inside wp-env, the smoke scripts run in the `cli` container with + `DB_HOST=mysql DB_USER=root DB_PASS=password` (wp-env's own development MySQL). The + container's working directory is `/var/www/html/wp-content/plugins/cli-smoke`. The + one-liner to run a script there is + `npx @wordpress/env run --env-cwd=wp-content/plugins/cli-smoke cli env DB_HOST=mysql DB_USER=root DB_PASS=password bash