Skills for creating, testing, reviewing, migrating and guarding Spring Boot 4 projects with Java 25: project scaffolding with architecture patterns, Boot 4 test templates, code review, Boot 3 to 4 migration, and a team harness of build gates and agent hooks.
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
oauth2Login appsGuidance for a project skill that drives a Spring Boot app in a real browser and proves that a change works. pstack's create-verification-skill generates such skills. Give it this file. Browser skills depend on the app's routes, roles and features, so the harness ships none.
The guidance comes from two oauth2Login (BFF) apps with a separate frontend dev server. The sign-in, doctor and proof basics were tested on the first, with the trial below. Everything else was observed on the second, over about 110 journey runs. Rules marked proposed answer a failure seen there but have not been tried yet.
Each section names the part of the generated skill it shapes: Launch, Doctor, Drive, Evidence, Cleanup or Helpers.
~/.<app>-verify/profiles/<persona>. The profiles hold live cookies. Never commit them, and add their path to .gitignore.JSESSIONID has no expiry, and in our runs Chrome did not save it in the profile, so a run started without an app session. Session restore or a customized session cookie can keep it, so check the session rather than assume it's gone. The tool opens /oauth2/authorization/{registrationId}, and the identity provider signs the profile in silently from its own saved cookie. A backend restart, which drops in-memory sessions, needs nothing from the user.channel: 'chrome' with launchPersistentContext), so no browser download is needed. Install @playwright/test outside the repository, not playwright-core: its expect retries, and it works in a plain script, so journeys need no test runner.Run one read-only check before any drive. It finishes in seconds; on the second app it took 70 to 100 s, which agents then paid before every run. Move first-time work, such as installing tools and checking the browser, into a separate setup command; that took the doctor to 12 s.
Request /@fs/<absolute checkout path>/frontend/src/main.tsx from the dev server. Pass only on 200 with a JavaScript content type. This shows the server can read that file, not that the app it serves was started from it.
server.fs.strict (on by default) answers 403 for files outside server.fs.allow. By default that is the workspace root, or the project root when there is no workspace, so a dev server started from another checkout refuses the path.text/html, the SPA fallback, so a wrong path passes on status alone.vite.config.* sets fs.strict: false, or an fs.allow wide enough to cover the other checkout. The doctor reads the config and warns on either.Compare the commit in /actuator/info (git.commit.id) with git rev-parse HEAD. The info id may be the short one, so pass when HEAD starts with it. When they differ (proposed), run git diff --name-only <running>..HEAD -- backend/ and fail the run if it lists files or git does not know the commit. An empty list passes: a frontend commit moves HEAD without changing the backend. When the info has no commit, report the backend as unverified. Warn when the backend folder has uncommitted changes, because two worktrees on the same commit also match, and a commit doesn't say what was compiled.
Proposed: repeat this check at the start of every journey run and put both commits on the summary line. The doctor is a separate command, and on the second app its stale-backend warning appeared 5 times.
git.properties is on the classpath. Add io.github.git-commit-id:git-commit-id-maven-plugin; spring-boot-starter-parent already configures it, so the declaration needs no version or goals.health over HTTP. Add info with management.endpoints.web.exposure.include=health,info in the local profile only. Call it with the persona's session if security does not permit it.revision goal, so the info has no commit. A shared run configuration in .run/*.run.xml that runs that goal before launch fixes it for the team. An Application configuration needs no paid IDE edition.--dry-run that checks everything else. A journey declares that it needs the action, and the normal run command refuses that journey.A journey proves only what its checks name, and its author writes them. On the second app, journeys caught fixes that did nothing, and a person still found most visible bugs: a duration on a call with no recording, a lost border, a missing row highlight, fewer visible rows. Most were things that disappeared, which generic layout checks such as sideways scrolling, cut-off text or contrast would not have caught. So pair journeys with the comparison and screenshot review below, and turn each bug they find into a failing check.
Move bad-input cases to unit tests, and keep one real browser path per filter. Unit tests do not replace journeys, which prove what only a browser can: wiring, sessions, live integrations and layout.
Proposed. Things that disappear pass every check that names only what should be there, so compare the two runs:
--before run and the final run, and pair the shots by label.Visual quality, such as a poor calendar, stays a human judgment. The report never claims that this comparison covers it.
The first review found five issues while every check passed. On later shots it found fewer, and its catch rate is unmeasured.
Proposed. Use real bugs, not invented ones. In a worktree, undo real past fixes one at a time, run the journeys and the review, and count the catches. Do this once as a baseline and again after a change to the tool or skill, with the same set of fixes. Keep a ledger of which bug each journey caught, so a journey that never catches anything can be questioned.
Give journeys an api(path, body?) helper that calls the app's API with the browser's session and CSRF token, as the SPA does. Compare the page with its answer, not with the page's last network response: the SPA's query cache can answer a repeated query without a request, so that response can be older than the question being checked.
With Spring Security's cookie CSRF setup (csrf.spa()), read the token from the XSRF-TOKEN cookie and send it as X-XSRF-TOKEN on unsafe methods. Read the cookie on every call, never once per run: Spring clears it on sign-in and sign-out and creates it only when a request needs it. When it is missing, make one GET first.
Write each wait as a retrying await expect(locator)..., so the check ends on a state the page shows. Never use Playwright's waitUntil: 'networkidle'; its docs mark it discouraged. When a wait for quiet cannot be avoided, count only requests to the app's own origin.
On the second app, an embedded third-party widget opened two loads that never finished once its persona was signed in. Every wait for quiet timed out after 20 s, and one journey went from 13 s to 60 to 120 s. Hand-written waits also caused most of the journey bugs in one feature, which took about a third of its time.
expect, or report the flow NOT VERIFIED.--before) as a file with the commit, and warns when the checkout has uncommitted app changes. The report has a Failed first column that points to the file. Agents skip this step unless the report asks for its evidence.--app on a base worktree, the file can otherwise name the wrong commit.git worktree add ../<app>-before <base>), and run the new journeys from the current checkout against the app started from it. Point the doctor's checkout check at the worktree for that run.
oauth2Login builds its redirect URI from the base URL, port included, so the old backend must run on the registered port. Stop the current backend, start the old one, run --before, then start the current one again. Two backends side by side need a second redirect URI registered with the identity provider.A check that cannot run because data is missing reports SKIP, never PASS or FAIL. This overrides pstack, which reports a failure for a missing seed precondition. Keep its other rules: never weaken the journey, and note the missing data in the feature's Gotchas.
A run with skips and no failures exits with code 2, and the report lists each skip as NOT VERIFIED. Without this, agents report the gap as a pass, or change the data until it passes. A FAIL that only means "no data" teaches them to ignore failures.
A check whose subject is missing calls skip() too. An early return, or an if (visible) around the assertions, reports PASS with nothing tested. Proposed: the tool counts expect and ensure calls per check and fails any check that makes none at runtime. A lint would miss an if with no else.
The report's other states: NOT APPLICABLE for a change no screen can show, with one line saying why, and NOT VERIFIED when verification is turned off.
A trial planted a wiring bug that every unit test missed, and ran 3 Sonnet sessions per setup:
| Setup | Caught the bug |
|---|---|
Skill and an AGENTS.md line, task did not ask for proof | 0/3 (the skill was never used) |
| No skill, task asked for screenshot proof | 0/3 (no way to see the page) |
| Skill, task asked for screenshot proof | 3/3 |
AGENTS.md line alone does not make agents verify. Two sessions called the check "still outstanding" in their final report, then stopped.--app <url>), not environment-variable prefixes (VAR=x node ...). A prefixed command does not match common allow rules such as Bash(node:*), and that blocked 2 sessions in the trial.grep or cut even with a summary line, and a pipe hides the exit code.expect, and go lower only with data. Playwright actions wait 30 s by default, so one missing element cost twice the assertion limit.journeys/scratch/) or in the agent's scratchpad. Probes left in journeys/ run with the real journeys.SKILL.md files call. pstack writes only the Claude Code copy, and a second full copy drifts: the agent on the second app copied the tool across by hand and ran diff -r to check.! node <tool> login <persona>)./orders/recent into a Windows path. Take routes without the leading slash, or document MSYS_NO_PATHCONV=1.Save evidence outside the repository. Cleanup never deletes it.
Screenshots, facts files and traces can hold personal data, and Playwright traces record the DOM and network traffic. On an app with personal data, trim console lines and keep the files out of tickets and PRs.
Proposed: a prune command the user runs, separate from cleanup, with a retention limit that covers the whole evidence folder, screenshots included. Traces alone are not enough, because they are kept only for failed runs. With test data this is routine cleanup, so it comes after the fixes above.