Skip to content

[Add] Run the watcher and accept the terminal scripts of the site's own target (#251) - #470

Merged
juanmaguitar merged 2 commits into
juanmaguitar/251-project-typefrom
juanmaguitar/251-build-per-type
Sep 17, 2026
Merged

juanmaguitar merged 2 commits into
juanmaguitar/251-project-typefrom
juanmaguitar/251-build-per-type

Conversation

@juanmaguitar

Copy link
Copy Markdown
Collaborator

Why

A Gutenberg checkout does not build or watch the way wordpress-develop does. Core's watcher is grunt -- _watch, reached through npm's passthrough separator so Grunt's watch wrapper does not redo the full build first; Gutenberg's is npm run dev, its own incremental watcher, which would treat that separator's _watch as an argument it does not know. The terminal's npm run <script> list is Core's too (grunt, watch, build:dev exist only there). #251 needs both read from the site's type.

What changes

planDevServerStart takes the type's build entry and returns its watcher; with no entry it returns Core's, so a caller that does not know the type gets what every site got before. The grunt -- _watch constants and the reasoning behind them move from the planner into the registry, so the fact has one copy (the follow-up from #469's review). The card reads projectType from site:status, derives the type's build entry once per render, and hands it to the watcher start and the terminal's script list.

Both targets build with npm run build, so the registry's buildScript field, which nothing needed to vary, is gone rather than threaded through five call sites as a constant.

Deliberately not in this PR: anything a contributor can see. Nothing sends a type at creation yet, so every site is Core and behaves as it did. The choice appears in the PR that also serves a Gutenberg site through Playground, so that a Gutenberg site is never offered before it can be built, watched and run.

How to test this

Platforms: any. No user-visible surface: an existing site starts the same watcher and accepts the same scripts.

From the repository root:

node --test tests/unit/dev-server-command.test.cjs
node --test tests/unit/project-type.test.cjs
npm run test:e2e

The planner test is the demonstration: Core's plan is byte-for-byte what it was (grunt, ['--', '_watch']), and Gutenberg's is dev with no arguments. The journeys drive the real card through the watcher start on a Core site.

What must not have happened: a Core site's watcher gaining or losing the -- separator (the planner test pins both halves), or the terminal refusing grunt on a Core site.

Risks and limitations

  • Review outcome: 1 [fix here] fixed, 4 [follow-up] (three of them land in the PR that exposes the choice: the terminal docs, the double build on an unbuilt Gutenberg site, the watcher-tree stop pass).
  • The renderer's projectBuild lookup is untestable by construction (index.jsx, see TESTING.md); the planner test covers the decision, the journeys cover the Core path through the card.
  • Gutenberg's npm run dev tree (tsc --watch, wp-build --watch, an esbuild service) outlived a plain SIGTERM to npm in the 15 Sep spike. The app stops watchers through kill-tree.js (process group on macOS, taskkill /T on Windows), which this PR does not touch; the PR that exposes the choice must confirm Stop ends that tree by hand on both platforms.

Related

Part of #251. Stacked on #469.


Design decisions and alternatives considered
  • The planner keeps a default rather than requiring the argument: it is the seam every caller reaches the watcher through, and a default of Core is the same rule the registry follows everywhere.
  • Dropping buildScript rather than keeping it for symmetry: a field with one value across every target is a constant, and the review standard asks for complexity proportional to what is needed now.
Review outcome (required — see AGENTS.md)

1 [fix here] · 4 [follow-up] — the [fix here] fixed in 34d4005, plus one style note taken.

  • 🟡 React [fix here] src/renderer/index.jsx: the terminal's help and npm run guard closed over the type's script list, and the xterm creation effect depends on printHelp's identity, so the first status reporting a non-Core type would have disposed and recreated the terminal (scrollback gone, a streaming command writing into a disposed instance). Invisible today, exactly what the picker PR would have tripped. Fixed: the handlers read the list through a ref, the indirection terminalInputHandlerRef already uses.
  • 🔵 Architecture [follow-up] dev-server-command.cjs / index.jsx: planDevServerStart's needsBuild has no production consumer; the build-first decision is made inline in startBuildWatch. Pre-existing; collapse when the picker lands.
  • 🔵 Performance [follow-up] index.jsx watcher start: on an unbuilt Gutenberg site the flow runs npm run build and then npm run dev builds again (about 20 s), and the tab reads "watching" while dev is still building. Not reachable until a Gutenberg site can exist; for the PR that exposes the choice.
  • 🔵 Cross-platform [follow-up] src/kill-tree.js: SIGTERM with no SIGKILL escalation; the Gutenberg watcher tree (tsc --watch, wp-build --watch, esbuild) needs a manual stop-and-pgrep pass on both platforms once reachable. Nothing here touches it.
  • 🔵 Docs [follow-up] docs/guide/terminal.md: lists Core's fixed script list, true until the picker; the picker PR updates it.
  • Style, taken: s?.projectType || DEFAULT_PROJECT_TYPE duplicated the registry's own defaulting.

Verified by the reviewer: the component holds a lookup, not a decision; every hook that reads projectBuild lists it; there is no second builder of the watch command; the registry is in the esbuild bundle; the planner test pins Core's plan byte for byte and Gutenberg's separately.

  • Review: completed — fresh Explore subagent context (local /self-review); reviewed head 0f8809c / base 85f8047; deterministic layer (lint, npm test, 29/29 journeys) run by the author; outcome above
  • Since review: 0f8809c → 34d4005 checked: only the fix and the style note; lint, bundle and npm test re-run green
Screenshots or recording

Nothing on screen changes.

🤖 Generated with Claude Code

@juanmaguitar juanmaguitar added this to the v1.2.0 milestone Sep 15, 2026
@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e9e5aaeb-4cae-4669-9ccd-c505694588e9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@juanmaguitar
juanmaguitar added this pull request to stack #477 September 15, 2026 09:12
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/251-build-per-type branch from 34d4005 to a7a4ff4 Compare September 16, 2026 10:42
juanmaguitar added a commit that referenced this pull request Sep 16, 2026
…251)

The create-site dialog gains a "Contribute to" choice, read off the registry, Core by default; the choice goes to wordpress:setup as the type the stack already accepts. The button and the dialog are "Create a site" now, since the old name is false, and the two specs and the screenshot script that click it follow, as do the two screenshots that show it.

Docs: creating a site, getting started, the setup wizard, the terminal (Gutenberg's script list and why bare `test` is not on it), running the site (a Gutenberg site is a plugin inside a stock WordPress, and what the file-modification lock means).

The opt-in real-setup lane runs once per target. The Gutenberg variant logs in and reads the Plugins screen (Gutenberg active, no Delete links), and checks that stopping the watch leaves no process of its tree alive, which a signal to npm alone does not manage. The name is filled after the choice and read back until right: in the first seconds of a fresh app the filled value came back with letters added or dropped, not reproduced by hand or in a warmed-up app.

Deferred, from #470's review: on a never-built Gutenberg site the start builds twice (npm run build, then npm run dev's own first build), and the watcher tab reads "watching" during that first build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juanmaguitar and others added 2 commits September 17, 2026 08:57
…wn target (#251)

The dev-server planner takes the type's `build` entry and returns its watcher: `grunt -- _watch` for Core as before, `npm run dev` for Gutenberg, whose own script is the incremental watcher and must not receive Core's passthrough separator. The `grunt -- _watch` constants and the reasoning behind them move from the planner into the registry, so the fact has one copy (the follow-up from #469's review). The terminal's `npm run <script>` list is the type's too.

The card reads the type from `site:status` and defaults to Core, so every existing site starts the same watcher and accepts the same scripts it did. Both types build with `npm run build`, so the registry's `buildScript` field, which nothing needed to vary, is gone.

No visible change until the create-site dialog offers the choice, in the pull request that also serves a Gutenberg site.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From the pre-PR review. The terminal's help and `npm run` guard closed over the type's script list, and a new array identity there would have recreated `printHelp`, whose identity the xterm creation effect depends on: the first status reporting a type would have disposed the terminal, scrollback included. The handlers read the list through a ref now, the same indirection the input handler already uses, so their identities never change with the type. The card also stops re-defaulting the reported type; the registry answers Core for a missing one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/251-build-per-type branch from a7a4ff4 to d3bdaff Compare September 17, 2026 06:57
juanmaguitar added a commit that referenced this pull request Sep 17, 2026
…251)

The create-site dialog gains a "Contribute to" choice, read off the registry, Core by default; the choice goes to wordpress:setup as the type the stack already accepts. The button and the dialog are "Create a site" now, since the old name is false, and the two specs and the screenshot script that click it follow, as do the two screenshots that show it.

Docs: creating a site, getting started, the setup wizard, the terminal (Gutenberg's script list and why bare `test` is not on it), running the site (a Gutenberg site is a plugin inside a stock WordPress, and what the file-modification lock means).

The opt-in real-setup lane runs once per target. The Gutenberg variant logs in and reads the Plugins screen (Gutenberg active, no Delete links), and checks that stopping the watch leaves no process of its tree alive, which a signal to npm alone does not manage. The name is filled after the choice and read back until right: in the first seconds of a fresh app the filled value came back with letters added or dropped, not reproduced by hand or in a warmed-up app.

Deferred, from #470's review: on a never-built Gutenberg site the start builds twice (npm run build, then npm run dev's own first build), and the watcher tab reads "watching" during that first build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juanmaguitar
juanmaguitar merged commit ff9f1d8 into trunk Sep 17, 2026
8 checks passed
@juanmaguitar
juanmaguitar deleted the juanmaguitar/251-build-per-type branch September 17, 2026 16:34
juanmaguitar added a commit that referenced this pull request Sep 17, 2026
…k WordPress (#251) (#471)

## Why

A Core site's `build/` is a whole WordPress, and the Playground runner
mounts it as the docroot before install and skips the download. A
Gutenberg checkout is not a WordPress at all; it is a plugin, and there
is nothing to mount as a docroot. #251 needs the served site to be a
stock WordPress with the checkout mounted and activated as its Gutenberg
plugin.

## What changes

The stock WordPress installation now belongs to the site's lifecycle:
stopping and restarting preserves posts, settings and uploads; deleting
the registered Gutenberg site stops its processes and removes the
persistent installation before removing the checkout. The runner and
cleanup canonicalize the same directory, including Windows casing and
macOS path aliases. Failed cleanup keeps the checkout and registry entry
retryable.

`src/playground-plan.cjs` (ported from the closed #261) turns a serve
strategy into the `runCLI` options: `docroot` is what Core always sent
(mount before install as `/wordpress`,
`install-from-existing-files-if-needed`), `plugin-mount` uses
Playground's persistent `start` command to retain its WordPress
installation across restarts, mounts the checkout at
`wp-content/plugins/gutenberg` and adds an `activatePlugin` step. It
also names the constants a strategy needs on top of the shared debug and
SMTP set: the plugin mount is the contributor's working tree, `.git` and
uncommitted work included, so `plugin-mount` sets `DISALLOW_FILE_MODS`
and `DISALLOW_FILE_EDIT`; without them **Plugins > Delete** in the
served site removes the checkout. The cost is deliberate: the served
site cannot install a second plugin or theme, and has no file editor.
Losing an afternoon of work is worse than restarting a preview.

The runner takes one JSON argument instead of a bare build directory.
`playground:start` reads the site's type, writes the config, and runs
the runner from the checkout for a plugin mount, from `build/` as before
for a docroot. The map `playground:stop` and the quit sweep walk is
keyed as it was.

Deliberately not in this PR: anything a contributor can see. Nothing can
create a Gutenberg site yet; the choice arrives in the next PR, now that
a Gutenberg site can be cloned, built, watched and served.

## How to test this

Persistence regression, **macOS verified; Windows supported by the same
opt-in lane**, current head. From the repository root, run
`TOOLKIT_REAL_SETUP=1 npx --no-install playwright test --config
tests/e2e/real-setup.config.js -g 'keeps posts'` (PowerShell: set
`$env:TOOLKIT_REAL_SETUP = '1'` first, then run the command without the
POSIX assignment). This boots real WordPress, saves a post and upload,
restarts and checks both, deletes the installation and checks that the
same location starts fresh. It fails with the previous `server` command
(empty title, missing upload) and passes with the correction. The IPC
suite additionally covers registered/Core/unregistered deletion and a
cleanup failure. The fixture and its persistent installation are removed
after the test.

Platforms: any for the Core path; **macOS** for the Gutenberg pass below
(run by the author, 15 Sep, on `3c7d13c`; the fixes since do not touch
the plugin-mount path).

Core, no visible change, from the repository root:

```
node --test tests/unit/playground-plan.test.cjs
node --test tests/unit/runner-wiring.test.cjs
node --test tests/unit/ipc-wiring.test.cjs
```

The runner test pins the Core options byte for byte against what the
runner sent before (`mount-before-install` of the build directory, the
install mode, no plugin mount, no steps, no file-mods constants), and
the plugin mount separately.

Gutenberg, by hand, since the app cannot make such a site yet: with a
built `WordPress/gutenberg` checkout on disk, run `src/server-runner.js`
on Electron's Node with the argument
`{"strategy":"plugin-mount","pluginDir":"<checkout>","pluginSlug":"gutenberg"}`
and the checkout as the working directory, then open the `SERVER_URL:`
it prints.

1. The front page loads. Expected: its HTML references
`wp-content/plugins/gutenberg/` assets.
2. Log in (`admin` / `password`) and open **Plugins**. Expected:
Gutenberg is listed with a **Deactivate** link; no plugin has a
**Delete** link; there is no **Add New Plugin**.
3. Open **Posts > Add New**. Expected: the editor loads its scripts from
`wp-content/plugins/gutenberg/build/`.

Observed on the author's run: up in 5 to 14 s, all three as expected.

**What must not have happened:** a Core site's Playground options
changing in any way (the runner test is the guard); the served Gutenberg
site offering a Delete link on its own plugin; anything written into the
checkout by the served site.

## Risks and limitations

The restart/deletion regression passed locally on macOS; its Windows
network run has not been executed in this correction pass. Cleanup
follows the installed Playground CLI's
`~/.wordpress-playground/sites/<sha256(canonical cwd)>` convention,
pinned by the real CLI test. A checkout removed externally cannot be
resolved safely, so deleting its stale registry entry does not guess
which persistent directory to remove. Previously discarded temporary
content cannot be recovered.

- Review outcome: 1 [fix here] fixed, 4 [follow-up] (three taken, the
start race deferred).
- The Gutenberg pass is on macOS only. Windows runs the same runner on
the same JSON, with backslashes escaped by `JSON.stringify` and no shell
in the spawn; the next PR, which exposes the choice, carries the Windows
pass through its Buildkite artifact.
- Adminer's SQLite path (`/wordpress/wp-content/database/.ht.sqlite`) is
Playground's stock install's under `plugin-mount`, the same as under
`docroot`; verified by the front page and admin loading, not by opening
Adminer.
- `DISALLOW_FILE_MODS` also blocks plugin and theme installs and core
updates inside the served site. Deliberate, and documented in the next
PR alongside the choice.

## Related

Part of #251. Stacked on #470.

---

<details>
<summary>Design decisions and alternatives considered</summary>

- One JSON argument rather than a second positional one: the runner's
contract becomes "what to serve", and a third strategy is a new value,
not a new argument.
- The strategy's constants are spread last into the blueprint's
constants so nothing shared can override the guard.
- Mounting the checkout read-write with WordPress told not to modify
files, rather than a read-only mount: Playground's NODEFS mount has no
read-only mode, and the two constants are WordPress's own switch for the
same thing.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

1 [fix here] · 4 [follow-up] — the [fix here] and three of the
follow-ups fixed in `fee96e5`.

- 🔴 Tests / cross-platform [fix here]
`tests/unit/runner-wiring.test.cjs`: the new mount assertions compared
`hostPath` to POSIX literals while the runner resolves them, which on
Windows gives a drive-rooted path; four red CI jobs. Fixed: expected
values resolved the same way.
- 🔵 Security [follow-up, taken] `src/playground-plan.cjs`: `pluginSlug`
went into the VFS path unvalidated; only the registry supplies it today,
but the module is the seam. Fixed: a plain-name guard, with a test.
- 🔵 Tests [follow-up, taken]: "byte for byte" was not literal, the
docroot plan sent two empty arrays the old runner never did (inert in
the installed CLI, verified by the reviewer, but unpinned). Fixed: the
docroot plan sends exactly the old options and the test asserts the keys
are absent.
- 🔵 Performance [follow-up, taken as a comment]: the first Gutenberg
serve needs the network for the WordPress release; Playground caches it
afterwards. Recorded beside the strategy.
- 🔵 Failure paths [follow-up, deferred] `src/main.js`
`playground:start`: two starts for one site can both pass the
running-server guard, since awaits precede it (pre-existing, the SMTP
await was already there; this PR adds a store read). Deferred as its own
fix: the cheap change is a placeholder in the map at the guard.
- Style, taken: `isPluginMount` once instead of three strategy
comparisons; the runner harness comment no longer claims the Playground
packages are not installed.

Verified clean by the reviewer: the serve config is built from app-owned
values and passed as argv with no shell (Windows backslashes survive
`JSON.stringify` / `JSON.parse`, `path.resolve` is a no-op on an
absolute path); `playground-plan.cjs` has no requires or side effects so
the loopback patch still precedes the CLI load; the `DISALLOW_FILE_*`
trade-off is recorded and no remaining WordPress write path reaches an
active plugin's directory with both set; running the runner from the
checkout writes nothing into it; a plan failure reaches the renderer
through the existing log and exit path; the Gutenberg wiring test fails
if the handler stops reading the type.

- **Review:** completed — fresh Explore subagent context (local
`/self-review`); reviewed head `3c7d13c` / base `34d4005`; deterministic
layer (lint, `npm test`) run by the author; Playground option handling
verified by the reviewer against the installed `@wp-playground/cli`
bundle; outcome above
- **Since review:** `3c7d13c → fee96e5` checked: only the fixes above;
lint and `npm test` re-run green (1480 pass, 2 pre-existing skips)

</details>

<details>
<summary>Screenshots or recording</summary>

Nothing on screen changes in the app. The served Gutenberg site's
Plugins page, from the manual pass above: Gutenberg active, no Delete
links.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<details>
<summary>Stack correction review — 2026-09-17</summary>

0 remaining [fix here] · 0 new [follow-up] in this correction pass.
Persistence and deletion lifecycle fixed; the original data-loss finding
and the additional deletion/recreation finding are resolved.

- Current PR head: `244ca76c360e183a8da9f2343d7cf05fc05f5924`;
configured base head: `d3bdaff0b805b55ef8c9a8f0c4093ef68064ab24`.
Previous PR head: `1283b18bf0946eca9b5d1810b299eca1177baa69`.
- Reviewer: fresh Codex context `/root/review_fixes`. Completed
five-dimension review of the correction delta from previously reviewed
integrated head `bc8c4fda3fb78d51cd6f3abd5fc1b0f1e89c0095` to
`c8c86677491012f9c9d4e99fe43b4ceff1c9647b`, then re-reviewed through
final stack head `34550eec7a9031d2d5f03116801a60e313fa0559`. The
additional runtime-deletion finding was fixed and verified; the original
three findings remain resolved. Earlier review records above are
historical.
- Validation on the combined stack: lint; Node unit/integration/IPC
suite (1,547 passed, two runtime skips); Electron suite (1,549 passed);
33 macOS journeys; documentation build. The real Playground
restart/deletion regression passed on macOS. Windows CI results must be
read on these new heads; this pass does not claim a fresh Windows
real-setup run.
- CodeRabbit's earlier check is not review coverage for these new heads.
This local correction review is the review of record for the delta; no
new bot review was requested.

</details>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
juanmaguitar added a commit that referenced this pull request Sep 17, 2026
…251) (#476)

## Why

A contributor who wants to work on the block editor has no way in: every
site the app creates is a `wordpress-develop` checkout. The three PRs
under this one taught the app to clone, build, watch and serve a
Gutenberg checkout; none of them let anyone ask for one. This is the PR
that does, now that the whole path works. #251 has the case.

## What changes

The create-site dialog gains a **Contribute to** choice, **WordPress
Core** or **Gutenberg**, each with a line saying what the app does with
such a site today, read off the registry so the copy lives in one place.
Core is selected by default. The choice goes to `wordpress:setup` as the
`projectType` the stack already accepts, and the button and dialog are
renamed **Create a site**, since "Create WordPress Core site" is now
false. The rename reaches the two specs and the screenshot script that
click it, and the two screenshots that show it are regenerated.

A Gutenberg site is told apart on screen and shown only what applies.
The sidebar row and the site header carry a **Gutenberg** tag; the
checklist's words are the target's (the repository it clones, the
packages it compiles, the WordPress it launches with the checkout as its
plugin), from a `setup` block in the registry, and the type reaches the
row from the moment the dialog closes because the placeholder carries
the choice. The Trac-shaped cards show only where the work item is a
Trac ticket: on a Gutenberg site the ticket card gives way to a line
saying that working on a GitHub issue from here, with its branch and its
pull request, comes in a later version; the apply card is **Check out a
pull request** with no patch-file path; **Attach to Trac** is not
offered while **Hand it to a mentor** still is; the pull-request
destination says the flow is not available yet before it invites a
sign-in; a `wpct://ticket` link that arrives while a Gutenberg site is
in front is turned away by name, kept for a Core site, and the note
hides on that site only. Every one of those strings lives in the
registry's `cards` block, and a test pins that no card of a non-Trac
target speaks of Trac.

Docs: creating a site (the new field, what each choice clones, the tag
and what a Gutenberg page leaves out), getting started, the setup
wizard, the terminal (the Gutenberg script list, and why `test` is not
on it), running the site (a Gutenberg site is a plugin inside a stock
WordPress; what the file-modification lock means), Trac tickets and
applying patches (which cards are Core-only).

The opt-in real-setup lane now runs one job per target and platform (a
Core setup alone takes half an hour on the Windows runner, and two in
one job hit its limit): the Gutenberg variant also logs in and reads the
Plugins screen (the checkout is the active Gutenberg, and cannot be
deleted), and checks that stopping the watch leaves no process of its
tree alive (POSIX; Windows is probed by the workflow only for the flow).
Its name field is filled after the choice and read back until right: in
the first seconds of a freshly started app the value Playwright filled
came back with letters added or dropped (`real-setupes`, `real-setu`),
which sent the site to a folder the test was not watching. Not
reproduced by hand, nor in a warmed-up app across a dozen tries; noted
here rather than hidden.

Deliberately not in this PR: a GitHub issue as the work item (the ticket
card still asks for a Trac ticket on a Gutenberg site, and its linked-PR
list searches `wordpress-develop`), opening a pull request against
`WordPress/gutenberg`, and `wpct://issue/N`. They are the next three
PRs; until they land a Gutenberg site is for building, running and
trying pull requests by checkout (which already fetches from the site's
own origin), not for filing work. So that a Gutenberg site cannot open a
pull request against `wordpress-develop` in the meantime,
`github:open-pr` refuses any site that is not Core, with a sentence that
points at the patch file; the choice's copy and the docs say only what
is wired.

## How to test this

Platforms: **macOS and Windows**; the Gutenberg path spawns npm and
Playground through the platform shims. Build: the Buildkite artifact for
the current head.

The **Real WordPress setup** workflow is the pass: it now creates one
site per target and, for Gutenberg, logs in and reads the Plugins screen
(Gutenberg active, no **Delete** link anywhere) and checks that Stop
leaves no watcher process behind (POSIX). Run locally by the author on
macOS, 15 Sep, current head: the Gutenberg variant passed in 4.0 minutes
end to end. Runs on this branch:
https://github.com/WordPress/contributor-toolkit/actions/runs/34955889102
on `ecab2bd` with one job per target and platform: **Core green on macOS
(2.7 min) and Windows; Gutenberg green on Windows end to end; Gutenberg
on macOS red at the last step only**, the survivor check, which is
#479's fix (see Risks). The two earlier runs (34948704875, 34949992162)
ran both targets in one job and were cancelled on Windows at the
60-minute limit, which is why the jobs are split now.

By hand, either platform, current head, in a folder you do not mind
deleting:

1. Click **Create a site**. Expected: the dialog has three fields, and
**Contribute to** is on **WordPress Core**, with a line under each
choice.
2. Choose **Gutenberg**, name the site `gb`, pick a folder, click
**Create site**. Expected: the folder created is `gb`, and the terminal
shows the clone of `WordPress/gutenberg`.
3. Wait for the checklist to run install and build on its own. Expected:
about a minute for the install and half a minute for the build on a fast
machine; **This site is ready to work on**.
4. Click **Start dev server and finish the wizard**. Expected: the
**Build watcher** tab shows `npm run dev` building the packages once and
then `Watching for changes…`; the server URL appears; the site opens.
5. Log in (`admin` / `password`), open **Plugins**. Expected: Gutenberg
is active with a **Deactivate** link; no plugin has **Delete**; there is
no **Add New Plugin**.
6. Open **Posts > Add New**. Expected: the editor loads.
7. Click **Stop build watch**, then **Stop dev server**. Expected on
macOS: `pgrep -f gutenberg-trunk` in a terminal (anywhere) prints
nothing within a few seconds.
8. In the terminal panel, type `npm run grunt`. Expected: refused, with
the Gutenberg list (`build`, `dev`, `test:unit`, `lint`, `lint:js`).
8a. Look at the sidebar and the header. Expected: a **Gutenberg** tag on
the row and next to the status (a Core site wears **Core** in the same
places, since `00181bf` (was `df4f989` before the rebase onto trunk at
`576d218`): JuanMa's ask on 2026-09-16, a mixed list read as one tagged
and the rest bare); the checklist steps read "Download Gutenberg",
"Compile the Gutenberg packages…", "Launch a WordPress with this
checkout as its Gutenberg plugin…"; no **Trac ticket** card, a **GitHub
issue** card saying it comes later; the apply card reads **Check out a
pull request** with no `.diff` link.
8b. With the Gutenberg site in front, open `wpct://ticket/62281` from a
browser (or `open -a "<build>/dist/mac-arm64/WordPress Contributor
Toolkit.app" "wpct://ticket/62281"` with every other copy quit).
Expected: "Ticket #62281 cannot be linked to <site>", no **Link ticket**
button, `git branch` in the checkout shows no `ticket/62281`; click
**Hide**, switch to a Core site: the question is asked there.
9. On the Gutenberg site, open **Review & submit changes** and try
**Open a pull request** (sign in if asked). Expected: refused with
"Opening a pull request from this site is not supported yet — save the
patch file instead."; nothing reaches GitHub.
10. Create a second site with the default choice. Expected: exactly what
a site was before this stack: `grunt -- _watch` in the watcher tab,
`grunt` accepted in the terminal, a pull request opens as before.

**What must not have happened:** a Core site changing in any way (step
10); a Gutenberg site reaching GitHub's fork or pull-request endpoints
(step 9); the served Gutenberg site offering **Delete** on its plugin
(step 5); a watcher process outliving Stop (step 7); the checkout
gaining files from the served site (`git status` in it is clean apart
from `build*`, which Gutenberg ignores).

## Risks and limitations

- Review outcome across six passes: 15 [fix here] fixed, 12 [follow-up]
(8 taken, 4 deferred: `pgrep -f` misses argv-less helpers, the no-Delete
assertion's strength, and two pre-existing Core sentences in the Review
& submit modal's staleness banner and dirty-tree detail).
- On a Gutenberg site that has never been built, the dev-server start
runs `npm run build` and then `npm run dev`, which builds everything
again before watching: about twenty seconds twice on a fast machine. And
the watcher tab reads "watching" while `dev` is still in its first
build. Both are the #470 review's follow-ups; deferred here because
collapsing the two needs the watcher's own "Watching for changes" line
as the ready signal, which is its own change.
- `sites:add` (adopting a folder) still records every adopted checkout
as Core. Adopting a Gutenberg checkout gives a site that builds with
grunt; detection (`package.json` `name`) is a small follow-up.
- **Stop does not end Gutenberg's watcher tree on a slow machine.** On
the macOS runner, 3 of 3 runs left `tsc --build` (the native TypeScript
binary Gutenberg's `npm run dev` runs) alive after Stop: it ignores the
SIGTERM the app sends the process group while it is mid-build, and the
app's escalation to SIGKILL after 3 s reaches only the direct child
(`npm:kill` in `src/main.js`). Locally, where the first build had
finished, it died 3 of 3 times. The fix belongs to `kill-tree.js` and
`npm:kill` on trunk, so it is its own pull request (#479); this PR's
real-setup lane is what found it, and stays red on macOS until that
lands.
- The process-tree check runs on POSIX only, and by argv (`pgrep -f
<site path>`), so a helper whose arguments carry no path, such as
esbuild's service, is not seen by it; on Windows the workflow proves the
flow, not the kill (its `taskkill /T` path has injected unit coverage in
`kill-tree.test.cjs`).
- The Plugins-screen assertion that no **Delete** link exists only bites
if the stock install ships an inactive plugin; a stronger probe
(`plugin-install.php` refusing under `DISALLOW_FILE_MODS`) is a
follow-up.
- A first Gutenberg serve needs the network for the WordPress release
Playground then caches.
- The sidebar heading still reads "WordPress Core"; it is the app's
title, so a follow-up rather than this PR.
- The name-field mutation seen by the real-setup lane in a fresh app
(above) is unexplained. If a contributor ever reports a site named with
letters they did not type, this is where to look.

- The sidebar row's accessible name is now `<label> Core` / `<label>
Gutenberg`; the engine journeys' helper matches the label plus that
word. Any other test that finds a row by exact name has to do the same.

## Related

Part of #251. Stacked on #471.

---

<details>
<summary>Design decisions and alternatives considered</summary>

- A radio, not a dropdown: two options, both worth reading, and the help
line under it is where a first-time contributor learns the choice is
permanent.
- "Create a site" rather than "Create a contributor site": the sidebar
button is short and the dialog says the rest.
- The choice is exposed here, after serve, rather than in #469 with the
field: a Gutenberg site the app could create but not run is a state the
app must not produce.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

Six passes, each by a fresh Explore subagent context (local
`/self-review`), each on the head that moved since the previous one.
Totals: **15 [fix here] fixed, 12 [follow-up] (8 taken, 4 deferred and
recorded under Risks)**.

- **Pass 1, `3c0c4ba`**: 4 fix / 4 follow-up. The choice's copy promised
"where its pull requests go" while `github:open-pr` targeted
wordpress-develop for every site (fixed: the handler refuses non-Core
sites, copy narrowed); a docs claim about empty names (fixed); no
PR-time coverage of the radio (fixed: `legacy-site.spec.js`); stale
comment and unused `description` field (fixed: per-option descriptions).
Follow-ups taken: mutation comment reworded, three docs pages and the
clone string; deferred: `pgrep -f` misses argv-less helpers, the
no-Delete assertion's strength.
- **Pass 2, `28978e7`**: reconciled; (this pass was the CodeRabbit-style
re-check of the first's fixes, no new findings beyond the ones carried).
- **Pass 3, `ecab2bd`** (screen adaptations): 3 fix / 3 follow-up. Four
card strings decided in `index.jsx` (fixed: registry `cards` block); the
`wpct://ticket` prompt not gated by provider, which could put a
Gutenberg site on a Trac ticket branch with no card naming it (fixed:
`deepLinkNotice` takes the provider, `unsupported` state); PR preview
URL hardcoded to wordpress-develop and the modal pointing at a card the
page lacks (fixed). Follow-ups taken: mentor destination back outside
the gate, a journey with a seeded Gutenberg site, two docs pages.
- **Pass 4, `94a12cb`**: 2 fix / 3 follow-up. "Not now" on the refusal
note cleared the ticket app-wide while the text promised to keep it
(fixed: hidden per site); the submit sidebar still said "fork
wordpress-develop" and counted "the other two" (fixed: upstream-aware,
neutral counts). Follow-ups taken: journey asserts no ticket branch,
unused `workItemNoun` removed, the last inline string moved to `cards`.
- **Pass 5, `e4057cd`**: 2 fix / 3 follow-up. The hide reset keyed on
the ticket number, so the same ticket twice did nothing (fixed: keyed on
the arrival); the signed-out sign-in pitch survived the refusal (fixed:
refusal first). Follow-ups taken: `prAfter` and the sign-in sentence
into the registry; deferred: the reset re-firing on the null-and-back
prop (taken in pass 6).
- **Pass 6, `2f2ded7`**: 3 fix / 2 follow-up. No data-side guard that a
non-Trac target's cards say nothing of Trac (fixed: registry test); a
dead Gutenberg `signInCannot` (fixed: removed); the destination's `cost`
line still promised the fork (fixed: `prCost` per type). Follow-ups: the
reset keyed on the arrival stamp (taken); pre-existing Core wording in
the staleness banner and the dirty-modal detail (deferred, not
introduced here).

- **Review:** completed — heads `3c0c4ba`, `28978e7`, `ecab2bd`,
`94a12cb`, `e4057cd`, `2f2ded7` / base `fee96e5`; deterministic layer
(lint, `npm test`, journeys) and the opt-in real setup run by the author
on every head; outcome above
- **Since review:** `2f2ded7 → 3023bd3` reviewed by a seventh
fresh-context pass: all four of pass 6's findings resolved, no new
findings; lint, bundle, `npm test` (1484) and 30/30 journeys green on
this head

</details>

<details>
<summary>Screenshots or recording</summary>

`docs/public/screenshots/empty-state.png` and `create-site-modal.png`
regenerated from the fixture tier (`npm run shots -- --only=…`). Before:
"Create WordPress Core site" and two fields. After: "Create a site" and
the **Contribute to** radio between the name and the location.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<details>
<summary>Stack correction review — 2026-09-17</summary>

0 remaining [fix here] · 0 new [follow-up] in this correction pass. No
PR-local behavior changed; git range-diff reports every original commit
unchanged (=) against its rebased base.

- Current PR head: `08315156268f34ab94f6f0c81192f02642d48ce0`;
configured base head: `244ca76c360e183a8da9f2343d7cf05fc05f5924`.
Previous PR head: `ab2cfa708e325d58f8895113e1737bf20b76b3be`; only the
inherited stack corrections and commit identities changed.
- Reviewer: fresh Codex context `/root/review_fixes`. Completed
five-dimension review of the correction delta from previously reviewed
integrated head `bc8c4fda3fb78d51cd6f3abd5fc1b0f1e89c0095` to
`c8c86677491012f9c9d4e99fe43b4ceff1c9647b`, then re-reviewed through
final stack head `34550eec7a9031d2d5f03116801a60e313fa0559`. The
additional runtime-deletion finding was fixed and verified; the original
three findings remain resolved. Earlier review records above are
historical.
- Validation on the combined stack: lint; Node unit/integration/IPC
suite (1,547 passed, two runtime skips); Electron suite (1,549 passed);
33 macOS journeys; documentation build. The real Playground
restart/deletion regression passed on macOS. Windows CI results must be
read on these new heads; this pass does not claim a fresh Windows
real-setup run.
- CodeRabbit's earlier check is not review coverage for these new heads.
This local correction review is the review of record for the delta; no
new bot review was requested.

</details>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant