Build, Test, and Lint Configuration
Depth:
After implementation, the skill can optionally run a build verification command to catch regressions introduced by the task’s changes. This is configured via aitasks/metadata/project_config.yaml and applies to all three implementation skills: /aitask-pick, /aitask-pickrem, and /aitask-pickweb.
Configuration
# Single command
verify_build: "cargo build"
# Multiple commands (run sequentially, stop on first failure)
verify_build:
- "npm install"
- "npm run build"
- "npm test"
An inline-list item containing a comma must use the block (- …) form
instead: the inline [a, b] form splits on every comma, including one inside
quotes. A single scalar command is unaffected —
test_command: "pytest -k 'a,b'" is read whole.
If verify_build is not set (or the file doesn’t exist), the step is skipped entirely.
How It Works
The project_config.yaml file is:
- Git-tracked — shared across the team (unlike
userconfig.yamlwhich is per-user) - Project-specific — not tied to execution profiles (the same build command applies regardless of which profile you use)
- Installed from seed — a template is copied during
ait setup; edit it to match your project
Failure Handling
If the build fails, the agent analyzes whether the failure is caused by the task’s own changes:
- Task-related failure: The agent automatically goes back to fix the errors and re-runs the build. This repeats until the build passes.
- Pre-existing failure: The agent logs the build failure details in the plan file’s “Final Implementation Notes” and proceeds without attempting to fix unrelated issues.
Common Examples
| Project Type | verify_build |
|---|---|
| Android (Gradle) | "JAVA_HOME=/opt/android-studio/jbr ./gradlew assembleDebug" |
| Rust | "cargo build" |
| Node.js | "npm run build" |
| Go | "go build ./..." |
| Python | "python -m py_compile main.py" |
| Shell/scripts | (leave empty — no build step) |
Test and Lint Commands
In addition to build verification, project_config.yaml supports test_command and lint_command keys used by /aitask-qa for test execution and linting.
These three are the project’s command keys, all run after implementation. A fourth key, resource_admission_command, is asked before implementation starts and follows a different exit contract of its own — it is not part of the gate machinery described on this page.
# Test command — used by /aitask-qa Step 4
test_command: "bash tests/test_*.sh"
# Lint command — used by /aitask-qa Step 4
lint_command: "shellcheck .aitask-scripts/aitask_*.sh"
# Multiple commands (run sequentially)
test_command:
- "pytest tests/"
- "npm test"
An inline-list item containing a comma must use the block (- …) form
instead: the inline [a, b] form splits on every comma, including one inside
quotes. A single scalar command is unaffected —
test_command: "pytest -k 'a,b'" is read whole.
These are distinct from verify_build:
verify_buildruns automatically after implementation (Step 9 of/aitask-pick) to catch build regressionstest_commandandlint_commandrun on demand when/aitask-qaanalyzes a task’s test coverage
Auto-detection Fallback
When test_command is not configured, /aitask-qa auto-detects test files matching common patterns:
| Pattern | Language |
|---|---|
tests/test_*.sh | Bash |
test_*.py | Python |
*.spec.ts, *.test.ts | TypeScript |
*_test.go | Go |
Explicit configuration is recommended for reliable results — auto-detection may miss project-specific test layouts.
Common Examples
| Project Type | test_command | lint_command |
|---|---|---|
| Shell/bash | "bash tests/test_*.sh" | "shellcheck .aitask-scripts/*.sh" |
| Python | "pytest tests/" | "ruff check ." |
| Node.js | "npm test" | "npm run lint" |
| Rust | "cargo test" | "cargo clippy" |
| Go | "go test ./..." | "golangci-lint run" |
Reporting “did not run” from a command
Every non-zero exit from these three commands — verify_build, test_command
and lint_command — is treated as a failure by default. That is wrong for a
command that deliberately reports “I did not run”: a test runner serialized
behind a host-global lock, for example, exits without running anything when
another agent holds the lock. Recorded as a failure it also holds back every
task that depends on this one, because the tests_pass gate blocks dependents.
gate_command_exit_contract lists the command keys whose commands speak this
exit contract, so their exit 2 is read as a skip instead:
test_command: "tools/run-tests.sh"
# test_command's exit 2 now means "did not run", not "failed"
gate_command_exit_contract: [test_command]
| Command exit | Key listed | Key not listed |
|---|---|---|
0 | pass | pass |
1 | fail | fail |
2 | skip — “evaluated, not applicable” | fail |
3 | error — the command’s framework could not run | fail |
75 | error — the host refused resources | fail |
| anything else | fail | fail |
Only the documented codes qualify — any other non-zero exit, 64 (usage)
included, is a failure, so an unexpected status can never become a skip or an
error. An error is neither a code failure nor a pass: a gate retries it within
its retry budget, and the task does not archive until the command really runs.
Where the rule applies
The contract covers both ways the framework runs these commands, so a project gets one answer per command rather than two. What each path does with a skip differs, because each has a different thing to do with it:
| Where the command runs | An opted-in exit 2 means |
|---|---|
As a gate (build_verified, tests_pass, lint) | The gate is satisfied and dependents are released, while staying distinct from pass in the ledger history. |
As the build-verification step after implementation (/aitask-pick, /aitask-pickrem, /aitask-pickweb) | The agent proceeds — it is not sent back to fix a build that never ran — and says so. On a profile that records gates, it is recorded as a build_verified skip. |
In /aitask-qa test execution | The component is N/A in the health score (its weight is redistributed, not scored 0), and any “all tests pass” claim is reported as unverified — neither a pass nor a failure. |
A command that is simply not configured is a separate case everywhere: the step is skipped and nothing is recorded.
Why it is opt-in, and per key. Exit 2 is not free to reserve: GNU make
exits 2 on a build error and pytest exits 2 on interrupt. Reserving it for
every project would turn verify_build: "make" failures into green gates. A
project may also want the contract on for test_command and off for
verify_build, so each key opts in separately.
Accepted entries are verify_build, test_command and lint_command.
Anything else is a typo: it is ignored — it never changes a result — and
reported (on the gate-run block’s Note: line for a gate, and to the agent on
the build-verification path), so a misspelling does not look identical to
“not opted in”.
With a list of commands, a failure stops the list and a skip does not: any failure makes the whole run a failure, otherwise any skip makes it a skip, otherwise it passes.