init
ward init creates ward.toml using an interactive wizard or explicit options. It sets up Maven,
Gradle, TypeScript, and JavaScript projects; see Node projects for what changes
in a Node project.
ward init [-C <dir>] [--project <dir>]... [--preset <standard|strict>] [--integration <none|agent>[,...]] [--quality-hooks] [--remove-quality-hooks] [--ci github] [--agent-rules] [--gate <id>[,...]] [--tool openspec] [--openspec-validation <openspec|openspec-archived>[,...]] [--install-tool <tool|gate>[,...]] [--architecture <style>] [--overwrite] [--dry-run] [--accept-compatibility-updates] [--yes]Options
Section titled “Options”| Option | What it does |
|---|---|
-C, --directory <dir> | Start in <dir> instead of the current directory. See Choosing projects. |
--project <dir> | Configure this Maven, Gradle, TypeScript, or JavaScript project, relative to -C. Repeat for several. |
--preset <standard|strict> | Pre-select the preset’s gates and rules. See Presets and gates. |
--gate <id>[,...] | Enable these gates. With --preset, adds to the preset’s gates. |
--integration <none|agent>[,...] | Install agent integrations. none cannot be combined with an agent. |
--architecture <style> | layered, hexagonal, or clean for the archunit and dependency-cruiser gates. |
--tool openspec | Add OpenSpec as a project tool. |
--openspec-validation <id>[,...] | Add the openspec (active) or openspec-archived validation gate. |
--install-tool <tool|gate>[,...] | Consent to install a missing tool, such as trivy or openspec. |
--quality-hooks | Add Ward checks to the Git pre-commit and pre-push hooks. |
--remove-quality-hooks | Remove only Ward’s sections from those hooks. |
--ci github | Write a GitHub Actions workflow that runs the ci profile. |
--agent-rules | Write the selected gates’ limits into a Ward-owned section of AGENTS.md. |
--overwrite | Replace an existing ward.toml without asking. |
--dry-run | Print the change plan and exit without writing anything. |
--accept-compatibility-updates | Approve validated Maven plugin version updates without asking. |
--yes | Use defaults for unanswered questions, skip the review, approve compatibility updates. See How questions are answered. |
--verbose | Print initialization diagnostics to stderr. See Troubleshooting. |
agent accepts the IDs documented for integrate, including codex,
cursor, copilot, gemini, windsurf, cline, factory-droid, and amp.
Presets and gates
Section titled “Presets and gates”standardandstrictpre-select their offline gates.custom(wizard only) starts with none.- For TypeScript and JavaScript, see Node projects for what each preset selects.
- The gates you confirm are exactly the gates configured. Unticking a pre-selected gate removes its
ward.tomlsection, build declarations, and rule file. The preset’s rules still apply to the other gates. - With
--preset,--gateonly adds gates. It never removes preset gates. - Init does not run gates.
Some gates need extra input:
archunitasks for Layered, Hexagonal, or Clean architecture. If test sources already contain ArchUnit classes ending inTestorTests, init runs those instead of generating a duplicate. Otherwise it creates a minimalArchitectureTest.java; see Generated files.dependency-cruiserin a TypeScript or JavaScript project asks for the same three styles. Init writes the config that enforces the style, unless the project has its own; see Generated files.zapwrites a<TARGET_URL>placeholder. Replace it with an authorized target beforecheckwill run the gate.api-compatneeds a released baseline. On Maven it is offered only whenpom.xmldeclares<distributionManagement>; otherwise--gate api-compatfails with that explanation.openapi-lint,openapi-diff, andopenapi-compatare offered only when Git tracks anopenapi.yml,openapi.yaml, oropenapi.jsoninsrc/main/resourcesor the project root. Init writes that path as theirspec; see OpenAPI contract gates.dependency-checkwithtrivy: init warns that trivy already scans dependencies without an API key, so you can usually drop one of them.
Profiles
Section titled “Profiles”Init assigns each gate to cumulative profiles:
| Gates | Profiles |
|---|---|
Fast: format, checkstyle, pmd, gitleaks, prettier, eslint, biome | default, pre-commit, pre-push, ci |
Slower local gates, including compile, tsc, the test runners, and custom commands | pre-push, ci |
| Environment-dependent gates | ci |
default is the same fast tier as pre-commit, and ci contains every selected gate.
What init changes in your build
Section titled “What init changes in your build”For format, enforcer, license, api-compat, and pmd, init adds missing plugins using
Ward’s embedded Java compatibility catalog.
- Existing compatible plugin versions are kept.
- For an incompatible managed plugin, init shows the current version, the compatible version, the
detected constraints, and the file, then asks for consent. Approve with
--yesor--accept-compatibility-updates. - A declined update leaves the build file unchanged, and init reports that the gate was not reconfigured.
Java compatibility
Section titled “Java compatibility”Ward supports Java 8 to 25. Init chooses every plugin and engine version for the JDK that runs your build, not for the JVM that runs Ward:
- Maven: the
Java versionthatmvn --version(or./mvnw --version) reports. - Gradle: the
Launcher JVMthatgradle --version(or./gradlew --version) reports. Init also reads the Gradle version, because some plugins need a newer Gradle.
The release the build targets (maven.compiler.release, sourceCompatibility,
JavaLanguageVersion.of(n), or options.release) defaults to that JDK. A release above the
runtime JDK cannot compile, so init stops and asks you to run the build on a newer JDK.
Each version was run on a real JDK in Ward’s compatibility matrix:
| Tool | JDK 8 | JDK 11 | JDK 17 | JDK 21 | JDK 25 |
|---|---|---|---|---|---|
| Checkstyle engine | 9.3 | 10.20.2 | 10.20.2 | 10.20.2 | 10.20.2 |
| SpotBugs (Maven plugin / Gradle engine) | 4.8.6.6 / 4.8.6 | 4.9.7.0 / 4.9.7 | 4.9.7.0 / 4.9.7 | 4.9.7.0 / 4.9.7 | 4.9.7.0 / 4.9.7 |
| PIT | 1.17.4 | 1.30.1 | 1.30.1 | 1.30.1 | 1.30.1 |
| Spotless (Maven / Gradle) | 2.30.0 / 6.13.0 | 2.46.1 / 7.2.1 | 3.10.3 / 8.10.4 | 3.10.3 / 8.10.4 | 3.10.3 / 8.10.4 |
| google-java-format | 1.7 | 1.24.0 | 1.28.0 | 1.36.1 | 1.36.1 |
PMD (plugin 3.28.0, engine 7.17.0), JaCoCo 0.8.13, ArchUnit 1.5.1, and Enforcer 3.5.0 use the same version on every JDK.
Every strict gate runs on each LTS JDK from 8 to 25. When no version of a gate’s tool runs on your
JDK, init leaves that gate out and configures the others. The plan, --dry-run, and the init
output name it:
format omitted: spotless-maven-plugin needs JDK 8+; this project builds on JDK 7- A gate you pass with
--gatethat cannot run on your JDK stops init before it changes any file. - In the interactive gate list, such a gate is shown as unavailable and cannot be selected.
- The Gradle
dependency-checkandsonarplugins need JDK 11, so on Gradle they are omitted on JDK 8. - Above Java 25, init configures the project exactly as for JDK 25 and warns that Ward has not been tested on that JDK and does not support it.
- Below Java 8, init warns that Ward supports Java 8 to 25 and may not work with that version.
- Neither warning stops init.
- Plugins go only into the project-level
<build><plugins>, and dependencies only into the project-level<dependencies>. Missing sections are created. - Profiles,
dependencyManagement, and plugin dependencies are never edited. A plugin declared only inside a profile is reported as not reconfigured. - The one addition to
<build><pluginManagement>is the strictenforcerpins described in First failures understrict. Existing entries there never change. - Compatibility updates change only the managed plugin’s own project-level
<version>. - New declarations match the file’s indentation. The rest of
pom.xmlstays byte for byte as it was. mutationaddspitest-maven1.30.1 withpitest-junit5-plugin1.2.3, which read Java 25 class files.- The
maven-pmd-plugindeclaration enables<analysisCache>true</analysisCache>. On Ward’s own sources this cut a warm PMD run from about 8 s to 2 s.
Before writing, Ward checks the edited pom.xml in-process, without running Maven or fetching
anything. The file must be well-formed XML, and each added declaration may contain only elements
Maven allows there. If a check fails, pom.xml is not written, the error names the tool, and init
rolls back its other changes.
Init adds pinned plugin declarations to build.gradle or build.gradle.kts in the detected DSL,
and prefers the Gradle Wrapper when one exists.
- Declarations go into the existing top-level
plugins { }block. Without one, init creates it at the top, after anybuildscript { }orpluginManagement { }block, because Gradle acceptsplugins { }only there. - Each block is added only once, so running init again does not duplicate it. The Kotlin DSL gets the same blocks in Kotlin syntax.
Init then appends blocks that make the plugins use what it generated:
- With a preset,
checkstyle { configFile = file('config/checkstyle.xml') }andpmd { ruleSetFiles = files('config/pmd-ruleset.xml'); ruleSets = [] }, both limited to themainsource set, as the Maven plugins are. Each is followed by atasks.namedblock that limits the task to the files in thewardPathsproperty when a partial check sets it. spotless { java { googleJavaFormat() } }forformat, unless aspotless { }block exists.jacocoTestCoverageVerificationwith the preset’s minimums, orLINE0.80without a preset. Thecoveragegate runs this task, which fails below a minimum.pitest { pitestVersion = '1.30.1'; junit5PluginVersion = '1.2.3' }formutation, plus the preset’smutationThreshold. PIT is pinned because the plugin’s default PIT cannot read Java 25 class files. PIT derivestargetClassesfrom the projectgroup, so setgroupin the build script.
Neither build tool gets PIT history settings: since PIT 1.30, incremental analysis needs the commercial Arcmutate history plugin.
Generated files
Section titled “Generated files”-
ward.tomlincludespartialtables forcheckstyle,pmd,format,semgrep,gitleaks,eslint,prettier, andbiome. The check after each agent edit, and the pre-commit hook, skip gates the change cannot affect and narrow these tools to the edited or staged files. Other gates,ward checkwithout--path, the pre-push hook, and CI still check the whole project.gitleaksscans those files directly, so it sees changes that are not yet committed; its full run scans the Git history. -
ArchitectureTest.javagoes in the package shared by all main sources, for examplecom.acme.shopforcom.acme.shop.domainandcom.acme.shop.application. Its rules analyze that whole package. The layer rule passes while layer packages are empty, so a new project does not fail before they exist. -
The
gitleaksgate writes a redacted SARIF report, soward checkcan attach findings (file, line, and rule) to a failed gate:[quality-gates.gitleaks]command = ["gitleaks", "detect", "--no-banner", "--redact","--report-format", "sarif", "--report-path", "target/gitleaks.sarif"]Gradle projects use
build/gitleaks.sarif. Ward creates the report directory before the gate runs, so it works on a fresh clone. Configurations from earlier versions keep working without findings; re-runward initor add the four report arguments to enable them. -
The
licenseplugin matches license names exactly and case-sensitively, so init addslicenseMergesthat map common spellings to one name. The policy depends on the preset:standarddenies GPL and AGPL, mapping spellings such asGPL-3.0andGNU Affero General Public License v3.0to the excluded names. Licenses it does not recognize pass, so unusual GPL spellings are not caught.strictallows only Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, ISC, EPL-1.0, EPL-2.0, and CC0-1.0. Any other license, or a dependency without one, fails the build. A dual-licensed dependency passes when one of its licenses is allowed. LGPL, MPL, and CDDL are not listed because they need a legal decision; add an approved license toincludedLicensesand its spellings tolicenseMerges.
-
On Gradle, the
licensegate runscheckLicensefrom thecom.github.jk1.dependency-license-reportplugin, and init writes alicenseReport {}block whose rules are case-insensitive regular expressions. The plugin reads licenses from POMs and from files inside the jar, and a dependency passes when any of them matches a rule.standardallows every license except GPL and AGPL. LGPL, the Classpath Exception, and dependencies without a license pass. Other GPL exceptions, such as MySQL’s Universal FOSS Exception, only cover open-source use, so they fail.strictallows only Apache-2.0, MIT, BSD, ISC, EPL, and CC0 or public domain. Any other license, or a dependency without one, fails.- Earlier versions configured
com.github.hierynomus.license, whoselicenseMainchecks source headers, not dependencies. Re-runward initto switch; remove the old plugin yourself if you do not use it for headers.
Existing files
Section titled “Existing files”- An existing
ward.tomlis kept unless you confirm the overwrite or pass--overwrite. - Preset rule files are Ward-owned. Unmarked existing rule files are kept and reported.
First failures under strict
Section titled “First failures under strict”The generated rule files load as they are, so a gate that fails right after init reports a real
finding. With strict, expect these on most existing projects:
checkstyle: findings such asMagicNumber,MissingJavadocMethod, orMethodLengthin existing code.enforcer: strict keepsrequirePluginVersionson purpose, so every plugin needs a pinned version.- Init pins the plugins Maven adds through its default lifecycle (clean, resources, compiler,
surefire, jar, install, deploy, and site) in
<pluginManagement>. It uses the versions Maven 3.9.16 binds by default and skips plugins the POM already declares. The pins appear in--dry-run. - The rule can still fail on other plugins, such as
maven-war-plugin, or when a parent POM supplies a version. Pin those in<pluginManagement>with the versions the message reports. - Use
standardif you do not want this rule.
- Init pins the plugins Maven adds through its default lifecycle (clean, resources, compiler,
surefire, jar, install, deploy, and site) in
license: “There are some forbidden licenses used” (Maven) or “Some libraries do not declare at least one allowed license” (Gradle) for any dependency whose only licenses are not on the allowlist, for example an LGPL-only JDBC driver. On Maven, add the license toincludedLicensesonce your legal policy approves it, or add its spelling tolicenseMergeswhen it is an approved license written another way. On Gradle, add a rule tolicenseReport.
Node projects
Section titled “Node projects”Init sets up TypeScript and JavaScript projects with npm, pnpm, Yarn, or Bun. Every Node tool runs
as a local dev dependency through the package manager’s runner: npx, pnpm exec, yarn, or
bunx.
- A project is TypeScript when
tsconfig.jsonsits next topackage.json, or a workspace member has one. - A TypeScript project’s framework comes from the packages it declares in
dependenciesordevDependencies, never from file names:vueornuxtis Vue,svelteor@sveltejs/kitis Svelte,astrois Astro, and@angular/coreis Angular. If several match, the first in that order (Astro, Svelte, Vue, Angular) decides. - For Vue, Svelte and Astro, the type-check gate is the framework’s checker in place of
tsc:vue-tsc(vue-tsc --build),svelte-check, andastro-check(astro check).tscis not offered for those projects. - For Angular, the type-check gate is
ngc -p tsconfig.app.json --noEmit, which checks component code and templates. The test gate isng-test:ng test --watch=false --coverage, run with the project’s own test builder. Ward writes no test config for Angular and edits noangular.json,karma.conf.jsortsconfig*.json. Angular’s defaultvitestdependency does not select thevitestgate; a declaredjestkeepsjest. - Angular’s coverage minimums are enforced only when
angular.jsonsetscoverageThresholds. When it sets none, the plan says so and namesangular.jsonandkarma.conf.jsas the files to set them in. - Angular’s ESLint config uses
angular-eslint, which lints.htmltemplates. Init pinsangular-eslintto the release for the project’s Angular major (21.4.0 or 22.6.0) and checks the Node range of that release. strykeris not offered for Angular, and the strict preset omits it, because no Stryker runner works with Angular’s test builder. The plan says so.biomeis refused for Angular, because Biome does not check templates. Init stops with an error that namesbiomeand suggestseslint, and changes no file.- A framework project whose TypeScript configuration has no inputs fails the type-check gate and
names the file.
vue-tsc --buildreportsTS18002for a roottsconfig.jsonwith"files": []and no references, andTS18003for a referenced config with"include": []. A root with references to configs that have sources passes. - Stryker mutates only the
srcfolder in Vue, Svelte and Astro projects, so component files and tests are not mutated. Other projects keep Stryker’s default. - The generated ESLint config ignores each framework’s output folder:
.astro/for Astro and.svelte-kit/for Svelte. - Biome and framework lint rules are out of scope: a Biome project with a framework keeps Biome’s
current behavior, and
eslintlints.vue,.svelte, or.astrofiles only when the project’s own ESLint config handles them. - Any other
package.jsonis JavaScript, and counts as a project only when it holds.js,.jsx,.mjs, or.cjssources of its own. Init does not count files innode_modules, build output (dist,build,out,target,coverage), hidden directories, directories with another build file (such aspom.xml), packages that are not workspace members, or root tool configs such asvite.config.js. A package without sources, such as a monorepo root, is skipped; see Choosing projects.
| Gate | Command (npm) | Profiles |
|---|---|---|
tsc | npx tsc --noEmit --pretty false | pre-push, ci |
vitest | npx vitest run --coverage | pre-push, ci |
jest | npx jest --coverage | pre-push, ci |
bun-test | bun test --coverage | pre-push, ci |
stryker | npx stryker run | ci |
prettier | npx prettier --check . | default, pre-commit, pre-push, ci |
eslint | npx eslint . --format json | default, pre-commit, pre-push, ci |
biome | npx biome ci --colors=off --reporter=checkstyle . (--error-on-warnings under strict) | default, pre-commit, pre-push, ci |
dependency-cruiser | npx depcruise . --config <file> --output-type err | pre-push, ci |
semgrep | semgrep scan --config p/typescript --error (p/javascript for JavaScript) | ci |
- JavaScript gets the same gates except
tsc. - A project that uses Biome gets the
biomegate instead ofprettier; see Biome. - The test runner is the one the project declares:
vitest, thenjest. A Bun project without either getsbun-test, and any other project getsvitestwith@vitest/coverage-v8. semgrepstays opt-in, andgitleaksandtrivyare offered as for Java.
standard | strict | |
|---|---|---|
| Gates | tsc (TypeScript only), the test runner, prettier and eslint (or biome in a Biome project) | the same, plus stryker and dependency-cruiser |
| Coverage | at least 70% of lines | at least 85% of lines and 75% of branches (Bun: lines only, because it does not measure branches) |
| ESLint (generated config) | recommended rules | TypeScript: typescript-eslint strict-type-checked; JavaScript: recommended rules |
| Mutation score | none | at least 60%, in ci |
A custom selection uses the standard coverage minimum, and stryker without strict reports its
score without a minimum. dependency-cruiser is offered by both presets, and strict selects it.
Declared ESLint and Biome versions
Section titled “Declared ESLint and Biome versions”When package.json already declares eslint or @biomejs/biome, init keeps that version, as long as
it is inside Ward’s supported range. Ward tests the oldest and newest version of each range on Node 22
and 24, with the TypeScript fixture.
| Tool | Ward pins | Supported range | Tested versions |
|---|---|---|---|
eslint | 10.12.0 | >=9.22.0 <11.0.0 | 9.22.0, 10.13.0 |
@biomejs/biome | 2.5.15 | >=2.3.0 <3.0.0 | 2.3.0, 2.6.3 |
A declared version outside its range is an INCOMPATIBLE finding that names the tool and the range.
--dry-run lists it, and applying the plan fails without changing any file. Pick a version inside the
range, or skip the gate.
Init detects Biome when package.json declares @biomejs/biome or the project root has its own
biome.json, biome.jsonc, .biome.json, or .biome.jsonc. It asks no question about Biome.
- A Biome project gets one
biomegate, which lints and checks formatting in one run. It replacesprettierand, unless the project has its own ESLint config,eslint. Withstrict, warnings fail the gate too (--error-on-warnings). - A project’s own ESLint config (
eslint.config.*or.eslintrc*) stays, and itseslintgate runs that config unchanged. Init writes no ESLint or Prettier config for a Biome project, and a Prettier config the project has stays in place without a gate. - Under
strict, a project’s ownbiome.jsonorbiome.jsoncis layered (see Layered configs). A.biome.jsonor.biome.jsoncis kept with a warning. Understandard, a project’s own Biome config is kept unchanged and listed as user-owned, with a warning that the preset’s Biome rules are not applied to it. Otherwise init writes an ownership-markedbiome.jsoncwith Biome’s recommended rules. It also turns on Git-ignore support, so Biome skips.ward/reports/and coverage output.strictsets these rules toerror:noExplicitAny,noNonNullAssertion,useConst,noUnusedImports,noUnusedVariables,noExcessiveCognitiveComplexity,noFloatingPromises, andnoMisusedPromises. - If the project does not declare
@biomejs/biome, init adds it at 2.5.15. A declared version outside>=2.3.0 <3.0.0is anINCOMPATIBLEfinding, and applying changes nothing. Biome 2.3.0 is the first release with the checkstyle reporter that Ward reads, and 3.x may change the config schema. - Edit hooks run Biome on the edited files only. An edited file that Biome ignores passes.
Where the thresholds live
Section titled “Where the thresholds live”The thresholds sit in the tools’ own config files, so you can see and tune them there:
- When the project has no test config, init writes a Ward-owned
vitest.config.mjsorjest.config.mjswith the preset’s coverage thresholds, and the test command carries no threshold flag. - When the project has its own config (
vitest.config.*orvite.config.*for Vitest;jest.config.*or a"jest"key inpackage.jsonfor Jest), init keeps it unchanged and lists it as user-owned. The command then passes the thresholds, for examplenpx vitest run --coverage --coverage.thresholds.lines=85 --coverage.thresholds.branches=75. - Bun reads its threshold from
bunfig.toml, and Stryker reads its minimum fromstryker.config.mjs. - Projects set up by an earlier Ward version keep the threshold flag on their test command. Running
init again moves it into
vitest.config.mjsorjest.config.mjs.
Layered configs
Section titled “Layered configs”Under strict, a TypeScript project’s own flat ESLint config (eslint.config.*), or the project’s
own biome.json or biome.jsonc, is not replaced. Init moves it to a base file and writes Ward’s
root config in its place:
| Project file | Base file | Ward’s root |
|---|---|---|
eslint.config.mjs | eslint.base.mjs | eslint.config.mjs |
eslint.config.js with "type": "module" | eslint.base.js | eslint.config.js |
eslint.config.js without "type" | eslint.base.js | eslint.config.mjs |
eslint.config.cjs | eslint.base.cjs | eslint.config.mjs |
eslint.config.ts | eslint.base.ts | eslint.config.ts |
biome.json | biome.base.json | biome.jsonc |
biome.jsonc | biome.base.jsonc | biome.jsonc |
- ESLint: Ward’s root imports the base file and appends the strict rules, so Ward’s rules win over
the base’s. The base’s own rules still apply, so a framework’s rules, such as Next’s
core-web-vitalsor Nest’s Jest globals, keep working. The base file is ignored by the lint. - Biome: Ward’s
biome.jsoncextends the base file and sets the eight strict rules itself, so they win over the base’s settings. Biome checks the same files it checked before. - The base file’s content is not changed. Editors,
npx eslint,npm run lint, andward checkall read the combined config. - Running
ward init --preset strictagain regenerates Ward’s root and leaves the base file alone. - If the base file name is already taken by a file Ward did not write, init stops with a conflict and changes no file. Rename or move that file, then run init again.
- To undo the layering, delete Ward’s root config and rename the base file back to the project’s own name. The project’s config is then the user’s again.
What init changes in package.json
Section titled “What init changes in package.json”- Init adds each dev dependency the selected gates need and the project does not declare, with an
exact version from Ward’s catalog and no range prefix. A package already in
dependenciesordevDependencieskeeps its version, and existing tool configs are reused. - The edit inserts entries into
devDependencies, or creates it, with the file’s own indentation; nothing else in the file changes. A file whose layout cannot be located, such as a minified one, fails init before anything is written. - Then init runs
<manager> installfrom the project root. When nothing is missing, no install runs.--dry-runlists the packages and the install without running it. - If the install fails, init fails naming the command, restores
package.jsonand the lock file byte for byte, removes a lock file the install created, and writes noward.toml. A later init failure rolls back the same way.node_modulesis not restored; the next install converges.
| Package | Version | Node |
|---|---|---|
eslint | 10.12.0 | ^20.19.0 || ^22.13.0 || >=24 |
@eslint/js | 10.0.1 | ^20.19.0 || ^22.13.0 || >=24 |
typescript-eslint | 8.71.1 | ^18.18.0 || ^20.9.0 || >=21.1.0, TypeScript >=4.8.4 <6.1.0 |
prettier | 3.9.10 | >=14 |
@biomejs/biome | 2.5.15 | >=14.21.3 (declared versions must be >=2.3.0 <3.0.0) |
vitest, @vitest/coverage-v8 | 4.0.18 | ^20.0.0 || ^22.0.0 || >=24.0.0 |
typescript | 6.0.3 | >=14.17 |
globals | 17.13.0 | >=18 |
@stryker-mutator/core, @stryker-mutator/vitest-runner, @stryker-mutator/jest-runner | 10.1.1 | >=22.0.0 |
dependency-cruiser | 18.6.0 | ^22.0.0 || ^24.0.0 || >=26.0.0, TypeScript >=2.0.0 <7.0.0 |
The eslint gate adds eslint, @eslint/js, and typescript-eslint (plus typescript when it is
missing) to a TypeScript project, and eslint, @eslint/js, and globals to a JavaScript project,
with no TypeScript package. The stryker gate adds @stryker-mutator/core and the runner plugin
for Vitest or Jest. Bun has no runner plugin, so Stryker runs bun test through its command
runner. Stryker 10 needs Node 22 or later. The dependency-cruiser gate adds dependency-cruiser,
and typescript to a TypeScript project when it is missing.
Compatibility checks
Section titled “Compatibility checks”Before writing, init checks each package it would add against the project’s Node version (from
.nvmrc, .node-version, or engines.node, else 24), and typescript-eslint against the declared
typescript version (the lower bound of its range, or 6.0.3 when init adds it).
- A package outside its range is an
INCOMPATIBLEfinding naming the package and the range.--dry-runlists it, and applying fails without changing any file. A Node or TypeScript version cannot be fixed by choosing another package version, so there is no approval option. - For example,
"typescript": "7.0.2"with theeslintgate fails becausetypescript-eslint8.71.1 supports TypeScript>=4.8.4 <6.1.0. Pin TypeScript 6.0 or lower, or skipeslint. - Node is compared on its major version. When a range needs a later minor, such as
^22.13.0for ESLint on Node 22, the plan notes the minimum version. - Ward supports Node 22 and 24. For a project that declares an older Node version, init shows a warning that Ward may not work with it. The warning does not stop init or change the files it writes, and Bun projects do not get it.
Generated files
Section titled “Generated files”-
eslint.config.js(with"type": "module") oreslint.config.mjs(otherwise) when theeslintgate is selected and the project has noeslint.config.*or.eslintrc*. It is a flat config ignoringdist/,coverage/,.ward/,.next/,out/, andbuild/(framework build output), and it starts with a// managed-by: wardmarker. Understrict, a TypeScript project’s own flat config is layered instead (see Layered configs). Otherwise an existing config is reused unchanged and listed as user-owned, and the plan warns that the preset’s ESLint rules are not applied to it. A project with no tests fails the test gate until it adds one, because a test gate that passes with no tests is misleading.- For TypeScript it enables the
@eslint/jsandtypescript-eslintrecommended rules. Withstrictit enablesstrict-type-checkedinstead, with type information fromtsconfig.jsonthrough the project service. Type-checked rules are off for.js,.mjs, and.cjsfiles, such as init’s own configs, because they sit outsidetsconfig.json. TypeScript files thattsconfig.jsondoes not include, such as excluded tests, fail with a project-service error: add them totsconfig.json. Understrict, empty classes that carry a decorator, such as a NestJS module, are allowed. - For JavaScript it enables the
@eslint/jsrecommended rules and declares both browser and Node globals fromglobals, sorequire,process, andwindowpassno-undef. It parses JSX in.jsxfiles. Without"type": "module"it also parses.jsfiles as CommonJS, because ESLint parses them as ES modules otherwise. With thejestgate it also declares Jest’s globals (test,expect,describe, …) for files under__tests__and*.test.*or*.spec.*files. Narrow the globals to your runtime once the first check passes.
- For TypeScript it enables the
-
vitest.config.mjsorjest.config.mjswith the coverage thresholds, unless the project has its own test config; see Where the thresholds live. -
bunfig.tomlforbun-testwhen none exists, with[test] coverageThreshold = { lines = 0.7 }(0.85 withstrict) and a#marker. An existingbunfig.tomlis kept, and init warns that the coverage threshold is enforced only if it sets one. -
stryker.config.mjsforstrykerwhen the project has nostryker.conf.*,stryker.config.*, or.stryker.conf.*. It names the test runner, enables thejson,clear-text, andprogressreporters, and setsthresholdswithhigh: 80andlow: 60. Withstrictit also setsbreak: 60, so Stryker fails below a 60% mutation score. An existing Stryker config is kept with its own thresholds. -
.dependency-cruiser.mjsfordependency-cruiserwhen the project has no dependency-cruiser config of its own (.dependency-cruiser.{json,js,cjs,mjs,ts,cts,mts}). In the chosen style, a module in one layer must not import another. A layer is a directory with that name in the module’s path, and the config does not cruisenode_modules:Style Modules in a path named Must not import modules in layeredcontrollerrepositoryhexagonaldomain,applicationadapter,infrastructurecleandomain,corecontroller,adapter,infrastructurestrictalso forbids circular dependencies. In a TypeScript project, type-only imports count, and the config namestsconfig.jsonwhen it exists. A layer folder that does not exist matches nothing, so the gate passes. A config the project wrote is used as it is. Init rewrites Ward’s file when you change the style or preset, and its first line is// managed-by: wardwith no preset. -
These generated configs are ES modules (
.mjs) in every project and start with a// managed-by: ward preset=<id>marker. Init rewrites them when you change the preset, and never touches a config you wrote. -
biome.jsoncfor thebiomegate when the project has no Biome config of its own. It starts with a// managed-by: ward preset=<id>marker; see Biome. -
.gitignoregets a Ward-owned section with.ward/reports/andcoverage/, created when the file does not exist. Withstrykerit also lists.stryker-tmp/andreports/mutation/. Prettier and Biome skip Git-ignored files, so they do not check these reports. -
ward.tomlgetspartialtables foreslintandprettier: an edit hook runs them on the edited TypeScript and JavaScript files only, with acommandthat drops the full run’s..tscand the tests have none, so they run in full inpre-pushandci.
Tools init can install
Section titled “Tools init can install”Init installs a missing tool only with your explicit consent, one tool at a time.
| Tool | How |
|---|---|
| Trivy, Gitleaks | Homebrew |
| Semgrep | Homebrew, or pipx as a fallback |
ZAP (zap-baseline.py) | Manual guidance only |
| OpenSpec | --install-tool openspec |
- In the wizard, each unavailable tool gets a Yes/No question. No is the default, and blank input or end of input declines.
- In non-interactive runs, consent with
--install-tool <gate>, for example--install-tool trivy. --yesnever installs tools and never bypasses unresolved prerequisites.--dry-runlists the install commands but runs none.ward checknever installs gate tools.
When trivy runs after a selected Maven or Gradle gate, init adds --offline-scan, so Trivy
resolves dependencies from the local repository instead of Maven Central
(why).
Optional generated files
Section titled “Optional generated files”Quality hooks
Section titled “Quality hooks”Quality hooks are opt-in and separate from agent integrations. --quality-hooks adds Ward-managed
sections to .git/hooks/pre-commit and .git/hooks/pre-push:
- Each section goes after the shebang and before your own logic. It does not replace the hook.
- pre-commit runs
ward check --profile pre-commit --format jsonwith each staged file as a--path, so partial checks cover the commit’s files; pre-push runsward check --profile pre-push --format jsonon the whole project. - Re-run the option to refresh the sections.
--remove-quality-hooksremoves only Ward’s marked sections. It keeps your content, executable permissions, and other Ward integrations.- A malformed or duplicate marker is reported as a conflict and left unchanged.
CI workflow
Section titled “CI workflow”Without --ci, init does not generate a CI workflow. --ci github writes
.github/workflows/ward.yml, which runs ward check --profile ci on pull requests and pushes to
main. The workflow:
- installs the Ward release that ran init (
WARD_VERSION); - for a Java project, sets up the JDK that runs your build (JDK 25 when init cannot read it), since the pinned tools were chosen for that JDK;
- pins the tools the selected gates need (gitleaks, trivy, semgrep, OpenSpec);
- fetches full history for gitleaks and caches the trivy database;
- passes the
NVD_API_KEYsecret todependency-check; - uploads the SARIF report to code scanning for pull requests from the same repository.
In a Node project the workflow sets up Node or Bun instead of Java and installs the dependencies from the lock file first; see CI for Node projects.
The file is Ward-owned, so re-running init refreshes it. An existing workflow at that path without
Ward’s marker blocks initialization as a conflict. Gates that need a running application, such as
zap, are not set up in the workflow.
Agent rules
Section titled “Agent rules”--agent-rules writes a Ward-owned section into AGENTS.md with the limits the selected gates
enforce: Checkstyle limits, PMD rule sets, SpotBugs effort, coverage and mutation minimums, and
enforcer rules.
-
For TypeScript it names the
tsc, Biome (or ESLint and Prettier), and test commands, the 70% line coverage minimum (85% of lines and 75% of branches withstrict), Stryker (with its 60% minimum understrict), and the ESLint config in use, and no Java tool. -
For JavaScript it names the same without
tsc. -
Content outside the section’s markers is kept.
-
When a rule file is user-owned, the section points to the file instead of quoting preset values.
-
Init changes
AGENTS.mdonly when you pass--agent-rules, and then rewrites the section from the preset. Pass it again after tuning rules only if you want the preset values back.
Choosing projects
Section titled “Choosing projects”Init configures the Maven, Gradle, TypeScript, or JavaScript project that contains the -C directory. Started inside a
build module, it configures the aggregator that lists the module, so ward.toml lands next to the
aggregator’s pom.xml or settings.gradle(.kts).
When the -C directory is not inside a project init supports, such as the root of a pnpm or Turbo
monorepo whose package.json has no JavaScript sources of its own, init looks for projects below it:
- It skips hidden directories and
node_modules,target,build,dist, andout. - Modules count toward their aggregator instead of being listed.
- With one project found, init configures it and says so.
- With several, the wizard asks which to configure, all checked. Without a terminal, or with
--yes, init stops and lists the--projectvalues to pass. - With none, init stops without changing any file. If the
-Cdirectory is inside a project of another build tool, such as Poetry or Cargo, the error names that build tool and says that init supports Maven, Gradle, TypeScript and JavaScript projects. Inside a JavaScript package without sources, it reports that no supported project was detected. .jsfiles inside a Java project, such assrc/main/resources/static/app.js, belong to that project, so they do not make the repository root a JavaScript project.- A root with a stray script of its own, such as
scripts/release.js, is a JavaScript project and stops discovery. Pass--projectto choose the projects below it instead.
--project <dir> names a project explicitly, relative to -C. Repeat it to configure several
projects in one run:
ward init --project apps/api --project services/billing --preset standard --integration noneEach value must be a Maven, Gradle, TypeScript, or JavaScript project root. A module or a subdirectory fails before anything
is planned, and the error names the project to use instead. The answers apply to every selected
project; run init again with a single --project to give one project different settings.
Monorepos
Section titled “Monorepos”Ward keeps two roots apart:
- The project root holds
ward.toml, the build file, the rule files, andAGENTS.md. - The repository root, the Git top-level, holds the files that Git, GitHub, and agent sessions read only there.
| File | Single-project repository | Project in apps/api |
|---|---|---|
| Quality hooks | .git/hooks/pre-commit, pre-push | the same files, with a section keyed by apps/api |
CI workflow (--ci github) | .github/workflows/ward.yml | .github/workflows/ward-apps-api.yml |
| Claude Code | .claude/settings.json, .claude/hooks/ward-edit-write | root .claude/settings.json, .claude/hooks/ward-edit-write-apps-api |
| OpenCode | .opencode/... | root .opencode/..., files suffixed -apps-api |
| Other agents (Codex, Gemini, Copilot, …) | .codex/hooks.json, … | the same root files, shared by every project |
| OpenSpec gates | openspec/ in the project | root = "../.." when openspec/ exists only at the repository root |
The plan shows repository-level files relative to the project root, such as
modify ../../.git/hooks/pre-commit. A run that configures several projects lists them once under
Repository root. A project whose root is the repository root gets exactly the files it always
did.
The other agents read a single hook file at the repository root, so every project shares one installation:
- The hook names no project. Ward checks the project whose
ward.tomlis closest above each edited file, and at turn end every configured project with changes. - Edits outside every configured project, such as
apps/web/page.tsx, run nothing. - Removing such an integration removes it for every project in the repository.
Wizard tips
Section titled “Wizard tips”- Each question starts with
Step N of M. The total can grow when an answer adds a follow-up question, such as the architecture style after you selectarchunit. - Press Enter to accept the default shown in brackets:
Enter=1(standard) at the preset question andEnter=layeredat the architecture question. - Gates whose tool is not installed are marked
[unavailable]. If you select one anyway, init configures it and prints how to make it available, for exampleSelected quality gate 'semgrep': UNAVAILABLE - .... - The arrow-key menus redraw in place, so your earlier answers stay on screen.
| Where | How to answer |
|---|---|
| macOS and Linux terminals | Up/Down to move, Space to toggle, Enter to confirm. q, Q, or Ctrl+C cancels. |
| Windows, piped input, or no raw terminal mode | Numbered prompts. Pre-selected items show as [x]; each number you enter toggles that item, and an empty line accepts the pre-selection. |
For example, after choosing standard, entering the number of pmd configures every standard gate
except pmd.
Reconfiguring
Section titled “Reconfiguring”Run ward init again and answer y to the overwrite question, or pass --overwrite.
- The preset question defaults to the preset recorded in
ward.toml(customwhen none is recorded). - The gate list starts with the currently enabled gates, so changing one gate takes a couple of key presses.
- Integrations, hooks, tools, and the architecture style are asked as usual.
- If the existing file can’t be read, the usual defaults apply.
Answering n keeps the file and prints Kept existing ward.toml. No changes were made.
Gate tool installation
Section titled “Gate tool installation”Review and dry run
Section titled “Review and dry run”Before writing anything, init works out exactly which files it would change. On a terminal it shows
that plan and asks Proceed? [Y/n]. Press Enter or answer y to apply it. Answer n, or end the
input, and nothing changes. The review is skipped with --yes and when input is not a terminal.
Ward will make these changes:
create ward.toml modify pom.xml + maven-pmd-plugin 3.28.0 ~ spotless-maven-plugin 1.0.0 -> 3.10.3 (requires approval) create config/pmd-ruleset.xml keep config/checkstyle.xml (user-owned, not changed) create .git/hooks/pre-commit create .claude/settings.json
Proceed? [Y/n]:| Plan entry | Meaning |
|---|---|
create, modify, remove | Every file init would change, including agent-integration files and Git hooks. |
+ plugin version | A plugin init would add to pom.xml or the Gradle build file. |
~ plugin old -> new | A managed plugin version update; (requires approval) when it still needs approval. |
keep | A user-owned file init leaves alone. |
conflict | A user-owned file that blocks initialization. Init stops with exit code 2 before writing anything. |
--dry-run resolves your answers the same way, prints the plan, and exits with code 0. It writes
no files, installs nothing (including OpenSpec), and asks for no compatibility approval:
ward init --dry-run --preset strict --integration none --architecture layeredHow questions are answered
Section titled “How questions are answered”Each question is answered by the first of these that applies:
- The option that answers it, such as
--preset,--gate,--integration,--architecture,--tool, or--quality-hooks.--ciand--agent-rulesare options only; the wizard does not ask for them. - Its default, when
--yesis given: presetstandardwith its gates, integrationnone, no tools, OpenSpec validations, or installation, no quality hooks, and architecturelayered.--yesnever overwrites an existingward.toml(use--overwrite) and never installs OpenSpec (use--install-tool openspec). - An answer read from input.
On a terminal, options never hide the questions they don’t answer, so
ward init --integration claude-code still asks for the preset, gates, tools, and quality hooks.
Without a terminal, for example in CI:
- When
--preset,--gate, or--integrationis given, init reads nothing. Unanswered questions take their defaults, so an open but silent standard input can’t make it wait. - Without options, answers can be piped, for example
printf '1\n1\n\n' | ward init.
Missing and invalid answers
Section titled “Missing and invalid answers”- The preset and gate selection is the only required answer. If input ends before it, or it is
missing in a non-interactive run, init exits with code
2and suggests--preset,--gate, or--yes. - If input ends at any other question, init uses its default and continues. The exception is the OpenSpec installation prompt, where initialization is cancelled and no files change.
- On a terminal, an invalid answer (such as
4at the preset question, an out-of-range number, or an empty gate selection) shows the error and asks again, keeping your earlier answers. The raw-key menus stay open after an empty confirmation. - With piped input, an invalid answer fails immediately with exit code
2and no changes.
Tools screen
Section titled “Tools screen”The wizard has a separate optional Tools screen. OpenSpec and its two validations are unselected by default:
- active:
openspec validate --all --strict --no-interactive - archived:
openspec validate --archived --no-interactive
Selecting a tool or validation is not consent to install it. --install-tool openspec is explicit
consent; without it, Ward only writes the selected validations and reports manual remediation.
Troubleshooting with verbose diagnostics
Section titled “Troubleshooting with verbose diagnostics”Add --verbose to the root command or to init to route initialization decisions to stderr. The
diagnostics identify ownership conflicts, applied changes, and rollback outcomes without printing
file contents:
ward --verbose initward init --verboseExit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 | Initialization succeeded, or --dry-run printed the plan. |
2 | Invalid usage, or init could not initialize: a conflict, a cancelled or invalid selection, or a missing required answer. |