Local verification
| Command | What success proves | Excludes |
|---|---|---|
vp run check:affected | Selected gates passed, including any tests they contain | Unselected scopes, verification-only suites and builds, and CI-only checks |
vp run check | Every gate the quality task in vite.config.ts runs passed | Distributable builds, browser and live-service tests, images, mutation analysis, and release security |
vp run verify | The local verification suites and builds passed | Server integration and database tests, full-application E2E, live-service tests, images, mutation analysis, and release security |
While you work, run check:affected.
Before you push, run check.
It validates the contents of all pending changeset files through gate:changesets.
CI also checks release-note presence and changes against the tested merge base.
Before you request review, run verify.
vp run with no task name lists every task, and vite.config.ts is where each one is defined.
On first use, the script tests in check download two pinned tools, Vale and oasdiff, into a local cache.
Dependency installation
After changing dependency declarations, the lockfile, package-manager settings, or declared patches, reconcile the installed dependencies before local gates. This includes changes from a branch switch, rebase, or merge.
vp install --frozen-lockfile
The native package manager reconciles the installed graph and checks declared patches. A frozen install preserves the lockfile. See Local development for the pinned toolchain.
Affected selection
The command compares committed, staged, unstaged, untracked, and deleted paths with the merge base of
origin/main. For another target, run vp run check:affected --base <revision>.
| Changed path | Task |
|---|---|
webapp/** | affected:webapp |
server/** outside the agent and practice script resources | affected:server |
server/**/resources/{agent,practices/scripts}/**, docker/agents/** | affected:agents |
docs/** | affected:docs |
docs/images/readme/** | affected:docs and affected:webapp |
.github/**, .claude/**, .agents/**, .changeset/**, scripts/**, webapp/tools/oxlint/**, root package and tsconfig files, generated artifacts, AGENTS.md, CLAUDE.md, or any other path | quality |
The tasks are defined in vite.config.ts. Selection uses git diff --no-renames, so both sides of a
rename are evaluated. Git documents the underlying change statuses in
git diff.
Pre-push hook
vp install configures the Vite+ hook dispatcher, which runs the project hooks in .vite-hooks/.
It preserves a custom core.hooksPath and the local preference set by vp hooks disable.
vp hooks enable re-enables the dispatcher. vp hooks status shows which hooks are active.
Every worktree of a clone shares core.hooksPath.
The install sets it to the dispatcher's path relative to the worktree root.
Git resolves a relative hooks path against the worktree that runs the hook
(core.hooksPath).
So each worktree runs its own dispatcher, its own hooks and its own vp.
The install removes an absolute dispatcher path, which older Vite+ releases wrote, before it configures the relative one.
Each worktree needs its own vp install, which creates that worktree's dispatcher in .vite-hooks/.
A worktree without one has no dispatcher, so Git runs no hook in it.
vp hooks status reports the dispatcher as missing.
It never falls back to another checkout's vp or to one on PATH, because that vp may not read this worktree's configuration.
The pre-push hook runs vp run check.
git push --no-verify skips it. With custom hooks, run
vp run check yourself before pushing.
Complete local verification
vp run verify runs quality, then these suites and builds:
- The webapp unit tests.
- The Storybook browser tests.
test:server:selection, followed bytest:server:verification(unit and architecture with coverage).- The webapp build, with a
routeTree.gen.tscomparison that restores the committed file before the command exits. - The Storybook build.
- The docs build.
Server integration and database tests, full-application E2E, live-service tests, image builds,
mutation analysis, and release security are outside verify.
See CI/CD.
Run affected server integration tests separately with vp run test:server:integration.
Application and Storybook bundles
The application keeps Vite's 500 kB chunk warning limit. The floating Hephaestus panel is loaded behind its feature boundary. React's shared renderer has a separate cacheable chunk.
Storybook is a development tool, not the application bundle. Its build checks every JavaScript chunk:
ordinary chunks must stay at or below 500,000 bytes. Two monolithic upstream modules have separate
raw and gzip budgets in webapp/.storybook/chunk-budgets.ts:
| Upstream module | Raw bytes | Gzip bytes |
|---|---|---|
| Storybook preview runtime | 1,186,100 | 339,000 |
| axe-core | 600,000 | 165,000 |
The story index lists every story with its preload dependencies, so it grows with each story. It is application code, so it has its own ordinary chunk and does not count against the preview runtime budget.
The build prints the actual sizes against these budgets. It fails on an unknown oversized chunk or
when either limit is exceeded, rather than repeating Vite's undifferentiated size warning. Module
identity, not a filename such as iframe or axe, selects the exception. An upstream upgrade that
outgrows a budget needs fresh attribution before a budget change.
Removing accessibility checks or collecting application code into a tool chunk is not a size fix.
Vite+ task cache
On Windows the runner's automatic input tracking misses a file you add, so run
vp run --no-cache check there.
quality is a group: its gates run in parallel, and a cacheable gate whose inputs are unchanged replays.
vite.config.ts states which gates are uncached and which name their inputs. Run
vp run --last-details to inspect the latest run and its miss reasons, or vp cache clean to
discard the local cache. In CI the cache restores per CI/CD.
Java checks and caching
gate:server invokes Gradle once for build-script formatting, Java formatting and PMD. Gradle owns
their inputs and cached outputs.
Vite does not cache a second verdict over the Java build. The scoped
commands format:java:check and lint:java use the same native tasks. Test execution remains uncached.
Gradle reports individual deprecations through org.gradle.warning.mode=all. The pinned
GraphQL code-generation plugin
still exposes nullable Boolean isX() properties that Gradle 10 will stop recognizing. This needs
upstream getX() accessors preserving nullable semantics, not primitive defaults or disabled
warnings.
Cached generation can omit these diagnostics.
A quiet warm run does not prove that the dependency is repaired.
vp run --no-cache check disables Vite's cache, not Gradle's. To inspect a Java check without task-output
or configuration-cache reuse, run:
./server/gradlew -p server :spotlessCheck :application:spotlessCheck :application:pmdMain \
--rerun-tasks --no-build-cache --no-configuration-cache --profile
The generated HTML report is server/build/reports/profile/profile-*.html. Keep clean, warm and cache-restored
measurements separate.
Gradle's performance guide
explains why a baseline is necessary before a build change.
PMD must analyze every selected source successfully: a report containing processing or configuration
errors fails the task, even when it contains no rule violations. vp run gate:pmd-canary proves this
with clean, rule-violating and unparseable sources in a temporary directory. It checks the production
task's source coverage without editing or compiling the application's source files. CI runs it when
its implementation or PMD wiring changes.
Separate gates prove the facts that the graph needs.
gate:runner-contract proves how vp run executes commands.
gate:lint-contract proves that every house rule, type-aware diagnostic, and webapp design-system check still fires through vp lint. Its positive fixtures use the real Tailwind
theme, its imports, and the typography plugin.
A misspelled variant proves that the check loaded Tailwind instead of silently using its less precise class grammar.
Design-system lint
@shadcn/lint runs inside the existing Oxlint pipeline, not a
separate ESLint pass. webapp/.oxlintrc.json owns the enabled policies and their rationale.
webapp/components.json owns component and theme discovery.
No second alias or token list is necessary.
Inline styles fail unless they forward props, carry CSS custom properties, or document a runtime boundary at the offending line. Padding and gaps use the theme spacing scale. This is not a blanket ban on layout formulas or arbitrary selectors.
Selected indirect vocabulary maps are also checked at their definitions where indirect consumers exceed the plugin's one-hop analysis.
Run vp run lint:webapp to check the webapp.
Run vp run gate:lint-contract to verify enforcement.
The plugin's analysis limits matter: a clean lint result does not prove contrast, correct runtime classes or interaction behavior. Review both light and dark themes. Run the affected component tests. A color suggestion is based on proximity in light mode, not on its semantic purpose. Select the existing token for the meaning. A genuine false positive gets a line-level suppression with its reason, never a palette-wide exception.
Repository tooling
Use these conventions when changing tasks or lint configuration.
Task vocabulary
Task names are lowercase colon-separated words. A word can also contain digits or hyphens. The first word identifies the task type. Only these prefixes are allowed:
| Prefix | Meaning |
|---|---|
affected | Internal groups selected by check:affected for one changed tree |
build | Produce a distributable artifact |
check | Run an uncached read-only check or a group of checks. check is the local quality entry point. |
ci | Internal groups shaped for CI jobs and runner platforms |
db | Inspect or update the development database schema and its generated documentation |
dev | Start, stop, reset, or configure local development infrastructure |
docs | Build, serve, or lint the documentation site |
fix | Apply formatting and safe lint fixes |
format | Apply formatting. A final check segment makes the operation read-only. |
gate | Produce one verdict that CI can annotate. Every gate belongs to quality unless explicitly CI-only in the task contract test. |
generate | Regenerate a committed artifact from its authoritative source |
lint | Run a linter. A final fix segment applies safe fixes. report writes a report. |
pr | Follow a pull request on GitHub |
quality | Internal graph with every local quality gate. Use check at the command line. |
release | Prepare or publish a release version |
report | Turn existing results into a human- or machine-readable report |
schema | Refresh a checked-in external integration schema or contract fixture |
sync | Reconcile checked-in values with an authoritative local source |
test | Execute a test suite |
typecheck | Run a language type checker without emitting artifacts |
verification | Run credential-free builds and test suites beyond quality. The bare name is the internal graph. |
verify | Run the complete credential-free local verification entry point |
Segments after the prefix identify the subject and specialization, such as
test:server:integration.
:webapp, :server, and :agents are the tree scopes.
:java scopes only format and lint.
The Java leg of check is gate:server.
Lint and format
Oxlint lints.
Oxfmt formats, sorts imports, and sorts Tailwind classes.
One rule set has multiple layers.
The root .oxlintrc.json is the base for every TypeScript file and the config for the Node trees.
These trees are the agent runner, the practice script toolkit, scripts/**, load-tests/**, and the task graph.
oxlint.react.jsonc is the React layer.
oxlint.app.jsonc adds the application compiler, Vitest, and Storybook policy for webapp/.oxlintrc.json and extension/.oxlintrc.json.
Docs extend the base and React layer.
Each tree holds only its additions or different decisions, with a reason for each.
Write a rule once.
gate:docs-lint type-checks the docs tree and runs markdownlint-cli2 (docs/.markdownlint-cli2.jsonc).
docs:lint is its alias.
-
Run oxlint through
vpfrom the repo root, as CI does:vp -C webapp lint .vp -C extension lint .vp -C docs lint .vp run gate:agents-lint
The
optionsblock (typeAware,typeCheck,reportUnusedDisableDirectives) travels throughextends. Thus, a bareoxlintstarted inside a tree sees the same rules. It resolvestsconfig.jsonfrom where it started. -
extendscarriescategories,rules,plugins,overrides, andoptions. Each tree owns itsenv,settings, andignorePatterns. Vite+ takes the config as an object and accepts only objects inextends. Thus,webapp/tools/oxlint/load-config.tsinlines the files for both Vite configs. It drops theirjsPlugins, because that path rejects relative plugin paths. The file on disk keeps the paths for the CLI and editors. -
Type-aware rules need a file named exactly
tsconfig.json. The root stub supplies one for the Node trees configured bytsconfig.node.json.load-tests/tsconfig.jsontypes the k6 scripts the same way. -
The house rules are one oxlint plugin under
webapp/tools/oxlint/. All tree configs load it. Each config chooses which rules to turn on. Thus, adding a rule to the plugin enables it nowhere. § oxlint traps has the rest. -
vp rundoes not give a command a POSIX shell on every platform. Thus, a command never contains$.scripts/ci-contract.test.tsenforces this. When the config loads,vite.config.tsdetermines what a command reads from the environment.scripts/check-runner-contract.tsproves the runner facts that the graph needs.An oxfmt pattern with no
/matches a basename at any depth. A pattern with a/is anchored at the working directory. Thus, a root-only pass pairs its patterns with!*/**.
oxlint traps
Each restriction explains itself in its diagnostic at the call site. The following constraints do not appear in diagnostics. The commands assume the webapp. The extension uses the same layers.
-
Suppress with
// oxlint-disable-next-line <rule> -- <why>, above the line named by the diagnostic. Spell the rule as the diagnostic prints it:plugin(rule)becomesplugin/rule. A directive that suppresses nothing fails the build.options.reportUnusedDisableDirectivesiserror. Thus, the build detects misspelled rules, nonexistent rules, and suppressions whose reason no longer applies. The next case can still hide the original diagnostic. -
A hooks suppression silences the whole component. oxlint routes every React-hooks diagnostic through one directive name.
// oxlint-disable-next-line react-hooks/rules-of-hookscan appear anywhere in a component body. It also suppressesreact(set-state-in-effect)andreact(no-deriving-state-in-effects)for that whole component. This happens regardless of the line below the directive. Neighboring components still report diagnostics, so the file does not appear disabled.The build still fails because it reports the directive as unused. It reports the directive, not the effect, so the diagnostic you intended to silence disappears. The two effect rules report on the
setStateline inside the effect, not onuseEffect. Thus, a directive above the effect never covers them. Fix the effect. -
The config validator catches three shapes and misses a fourth. oxlint refuses to start in these cases:
jsPluginsnames a module that it cannot load.pluginsnames an unknown plugin.- A rule names a rule that its plugin does not define, including a namespace with no provider.
It does not detect a rule whose plugin is a built-in omitted from
plugins. It drops that rule with no diagnostic and a zero exit. Check this with one command. A config of{"plugins":["typescript"],"rules":{"unicorn/prefer-node-protocol":"error"}}passes a file that imports"path". -
react/forbid-dom-propssees a DOM element and not a component.data-testidon a<div>fails the build. The same attribute handed to<Button>or<Textarea>and spread onto the DOM from inside does not fail. Thus, the ban does not cover the component layer. Do not treat that gap as permission. -
A nullable string, number or boolean in a condition names its falsy case.
typescript/strict-boolean-expressionsreports the truthiness check, but not its replacement. Use the applicable check:- For a string,
hasText(s)(@/lib/text). - For a count,
n !== undefined && n > 0. - For a boolean,
flag === true. - For a
ReactNodeslot,rendersContent(node)(@/lib/react-node). Here,0renders andfalsedoes not.
A
ReactNodeslot typedReactElement | undefinedneeds none of these checks. - For a string,
-
The one exception
jsx-a11y/no-autofocusearns is the first field of an overlay the user just opened. Suppress that case inline with the reason. Everything else is the bug the rule describes. -
Before you trust or change an off rule's reason, re-derive its reports. From the repository root, run
vp -C webapp lint -A all -D <rule> --report-unused-disable-directives-severity=off ..-A allexcludes the rest of the rule set from the result. The severity flag excludes suppressions that those excluded rules left unused. A nestedoverridesentry still applies. Thus, a rule that an override disables for some files stays off there. -
An
offentry pins a decision and carries its reason. The root.oxlintrc.jsonheader owns the base policy. If a category enables the rule, explain why it does not apply here. Otherwise, explain why a rule that someone could select is wrong here. Thus, the decision survives a category change in an oxlint release. If this tree disables a rule enabled by the base, name the different constraint, not a preference. Examples are the browser target and the React Compiler.