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:push | Selected static checks, prose checks and focused unit tests passed | Complete Node suites, Java unit and integration tests, browser suites, builds and live-service tests |
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, let the pre-push hook run check:push: it runs the checks that pull request CI runs for the changed trees.
Run check when you change a shared input, such as the lockfile, or before you push another local ref.
check validates the contents of all pending changeset files through gate:changesets.
The pre-push hook also applies CI's release-note rules, on every push, against the merge base with origin/main as last fetched.
A branch must not edit MIGRATION.md, or delete or rename a pending changeset or migration fragment.
A change to shipped code needs an added changeset.
CI applies the same rules against the tested merge base, and exempts drafts, bots and verified reverts.
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 cache that every worktree of the clone shares.
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.
Rebase generated artifacts
For a pull request in this repository that targets main, use:
vp run pr:rebase-generated <pr-number>
Use Git 2.38 or later.
Get the branch owner's consent before you rewrite their branch.
Ask the queue owner to dequeue the pull request first.
Check out its branch at the remote head, with a clean worktree and index.
Install its dependencies first with the frozen command above.
Unset HEPHAESTUS_APPLICATION_JAR.
The command refuses an existing Git operation, merge commits, a queued pull request, or a different local head. If an older branch does not have this command, run its script from a current checkout:
node /path/to/current-checkout/scripts/rebase-generated.ts <pr-number>
Run that command from the pull request's worktree. The build and generators use the rebased worktree's code and dependencies.
The command fetches main and records its commit.
It rebases onto that commit and restores the original branch if a handwritten file conflicts.
It resolves conflicts only in server/openapi.yaml, both generated API client directories, and the configuration reference.
It also moves this branch's migrations after the newest migration on main and in the merge queue, as vp run db:reorder does.
The rename is staged beside the regenerated files.
Then it reconciles dependencies with a frozen install and builds one fresh server JAR. That JAR supplies the spec and configuration metadata, and both clients are regenerated from the spec. The command does not push or change the merge queue.
Review the regenerated diff and compare the commits with git range-diff.
The command reports any generated-only commit it drops during conflict resolution.
Commit the regenerated files, then run format, check, and the tests for the affected paths.
Use the original head that the command prints as the explicit push lease.
If generation fails, the branch stays rebased and partial output stays available for diagnosis.
Fix the failure and unset HEPHAESTUS_APPLICATION_JAR.
Run vp run generate:api and vp run generate:configuration-reference before you commit or push.
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 |
.changeset/*.md except README.md | affected:changesets: release-note validation, Markdown lint and prose |
.github/**, .claude/**, .agents/**, other .changeset/** files, 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
Git runs the hooks in the tracked .vite-hooks/ directory.
Every worktree of a clone shares core.hooksPath, and Git resolves a relative path against the worktree that runs the hook
(core.hooksPath).
vp install sets the path to .vite-hooks, which exists in every checkout.
Thus, a new worktree runs its own hooks from its first checkout, before any install.
The install replaces a path to this repository's hooks in one worktree of the clone or in a removed checkout.
It also replaces a path to the dispatcher that Vite+ generates in each worktree.
An absolute path runs one worktree's hooks and vp in every other worktree.
Git runs no hook, without a warning, in a worktree where the dispatcher is missing.
The install also removes each worktree's own core.hooksPath override that names these hooks, in every worktree of the clone.
Thus, no worktree depends on which checkout last ran an install.
The install keeps a contributor's own hooks directory, also one named .vite-hooks in another project.
Do not set core.hooksPath by hand.
Each hook starts through .vite-hooks/run, which runs the vp that the install of that worktree put in its node_modules.
In a worktree without an install, the hook refuses the commit or push and says to install, instead of skipping.
The install and vp run check:affected warn when the hooks path that Git uses in a worktree does not exist.
To skip the hooks once, set VP_GIT_HOOKS=0.
git config vp.hooks.disabled true turns them off in a clone, and the install keeps that setting.
Do not run vp hooks enable: it points every worktree back at the generated dispatcher.
A worktree on a branch from before the tracked launcher runs its old hook directly, so it needs vp on PATH until you rebase it.
The pre-push hook runs vp run check:push with one task worker.
It compares the same paths as check:affected and runs selected format, lint, TypeScript and prose checks.
It does not write source files.
git push --no-verify skips the hook.
A change under db/changelog/next/ first checks that the branch's migrations sort after origin/main as last fetched and after the merge queue's.
If the hook cannot read the queue, it says so and checks against origin/main only.
For Node, it runs changed test files, existing source companion tests and the CI contract suites for workflow changes.
This selection does not cover every fixture or resource input.
The complete Node suites remain in check and CI.
A server change runs one Gradle invocation: Spotless, PMD, the test compilation and the architecture tier. The Java unit and integration tiers remain in CI. Instruction changes also run the instruction gate. The workflow parser and CI linters remain outside the hook.
For frontend source files, the hook runs the changed test files and the colocated test of each changed source, such as Dialog.test.tsx beside Dialog.tsx.
It does not follow the import graph: through the router, one component reaches most route tests, which take minutes on one worker.
CI runs the complete unit project.
A source without a colocated test runs no test here.
The hook then prints that CI runs the tests that reach the change through imports.
Shared inputs, test helpers, network mocks, deleted files and non-code resources select the complete unit project. The extension selects its unit project only. Each test runner uses one worker. If a frontend unit run prints nothing for five minutes, the hook stops it and fails. On Linux and macOS, each Node process of the run first writes a diagnostic report, and the hook prints their directory.
A webapp change also builds the webapp, which enforces the bundle budget. The build takes seconds, and the browser suites in CI build the same bundle.
A change under scripts/ selects the tooling checks and each tree whose CI path filter in cicd.yml names the file.
Pull request CI runs the same trees for that change.
An unknown path or missing merge base selects every scoped check and both frontend unit projects.
The hook does not fetch, install tools, start a browser or build an artifact other than the webapp bundle.
If a pinned tool is absent from the shared cache, run vp run check once in any worktree of the clone.
The hook reports its selected scope and the suites that still require the full gate or CI.
It checks the current worktree, its HEAD and pending changes.
Before you push another local ref, run the full gate on that exact commit.
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. It also builds the commit that the change merges onto and holds the bundle budget against it (webapp/AGENTS.md§ Routing). - 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 | 672,000 | 190,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.
Retired paths
Run node --test scripts/check-no-legacy.test.ts to test the retired-path check.
Run node scripts/check-no-legacy.ts to check source deprecations, configuration aliases, path redirects and docs redirect stubs.
Provider schemas and provider payload mixins follow their upstream contracts.
Authentication, current landing pages and the canonical workspace-host redirect remain current flows.
Review also checks behavior that source patterns cannot classify.
scripts/no-legacy-baseline.json records the remaining removals during the release candidates.
Each entry names its owner and records its source file's fingerprint.
A file with an entry stays frozen until its retired paths and entries are removed together.
The gate rejects new entries, larger counts, changed fingerprints and stale entries.
The first baseline can record only files unchanged from the Git base.
Stable 1.0 and later releases require an empty baseline.
Debt guards
vp run gate:adr-confirmation checks every Accepted ADR.
Its Confirmation section must name an existing test class, gate, test task or script in code spans.
Every named executable reference must resolve.
A test method suffix such as BoundaryTest#checksBoundary resolves its class.
The gate checks existence, not test coverage or method behavior.
Existing gaps live in scripts/baselines/adr-confirmation.json.
Each entry names its owner, reason and removal condition.
New debt fails even when the total count stays the same.
The gate also compares the baseline with the base branch and refuses new entries.
When a check exists, delete its stale baseline entry.
The first PR for a baseline establishes its ceiling.
The gate runs offline and includes its own tests. The affected selections run it for docs, server, webapp, extension and agent changes. Their CI quality legs also run it. Only the reference forms above count as checks. A workflow name alone does not count.
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 |
job | Run a long command detached from the caller, and wait for it in bounded steps |
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.