Runner Config & Acceptance Gates¶
The tasks/.runner-config.yml file in each project declares the acceptance gates that every task must pass before it can be marked complete. It also carries the honesty contract that governs agent behaviour.
Location and override¶
The default configuration lives at defaults/runner-config.yml in the craft-easy-agentic repository. It is synced into tasks/.runner-config.yml in each project via project sync.
Projects can customize their gate configuration without being overwritten by future syncs — project sync skips existing runner-config.yml files unless you pass --force.
Gate categories¶
Gates are grouped into eleven categories. Each gate has a required flag (skip fails the gate) and a manual_review flag (the agent must assert completion; no automated check).
1. build¶
| Gate | What it checks |
|---|---|
api_imports |
All Python imports resolve cleanly (python -c "import app") |
api_lint |
ruff check . && ruff format --check . passes |
api_typecheck |
mypy --strict passes |
frontend_build |
pnpm -w build (or npm run build) succeeds |
2. test¶
| Gate | What it checks |
|---|---|
api_unit |
pytest tests/unit — fast, no I/O |
api_integration |
pytest tests/integration — real MongoDB |
frontend_test |
Jest / Vitest suite per app |
3. verify¶
| Gate | What it checks |
|---|---|
verify_changes |
Static analysis: phantom imports, broken route references, missing i18n keys, Pydantic field truncation |
verify_spec_coverage |
Every acceptance criterion in the spec has a file:line proof (section 21 of the spec) |
4. smoke¶
Mount-level component render tests with mocks. One gate per app surface:
app_smoke, admin_smoke, sys_smoke, web_smoke, www_smoke
5. e2e¶
Full-browser or device flows:
web_e2e, www_e2e, sys_e2e (Playwright), app_maestro, admin_maestro (Maestro mobile)
6. tenants¶
Per-tenant fixture tests and isolation verification:
sverige_fixture, france_fixture, deutschland_fixture, cross_tenant_isolation
7. security¶
| Gate | What it checks |
|---|---|
pip_audit |
pip-audit — no known Python vulnerabilities |
npm_audit |
npm audit --audit-level=high — no high/critical JS vulnerabilities |
secrets_scan |
trufflehog or gitleaks — no credentials in committed code |
capability_check |
Manual: every new endpoint has require_capability(...) called |
8. performance¶
Both are manual review gates:
| Gate | Criterion |
|---|---|
api_p95 |
API endpoints respond in ≤ 300ms at p95 (local dev, non-cold-start) |
bundle_size_delta |
Frontend bundle size increase ≤ +50KB gzipped |
9. accessibility¶
All manual review:
contrast_check (≥ 4.5:1 text, ≥ 3:1 large text), keyboard_nav (all interactive elements reachable), screen_reader (NVDA / VoiceOver tested)
10. documentation¶
All manual review:
api_endpoint_spec (OpenAPI / docs/ updated), view_spec (UI spec sections filled), feature_inventory (docs/product/features/ updated), summary_changelog (CHANGELOG.md entry added)
11. evidence¶
The evidence gates are the "honesty gates" — they require the agent to explicitly assert completion:
| Gate | Description |
|---|---|
adversarial_review |
Section 19 of the spec filled; no unhandled HIGH-risk items |
vad_testade |
Section 20 of the spec: honest summary of what was actually run |
surface_matrix |
Section 13 of the spec: all touched surfaces marked |
spec_bullet_signoff |
Section 21 of the spec: all ACs mapped to file:line |
create_consume_pairs |
Section 14 of the spec: every create flow has a consume flow |
test_scenarios |
Section 15 of the spec: filled BEFORE implementation |
Honesty contract¶
The runner-config embeds a behavioural contract that Claude agents must follow:
Marking tasks complete¶
- Never mark
[x]in BACKLOG.md if any required gate is red. - Never silence failing tests with
@pytest.mark.skip,xfail,// eslint-disable, or similar. - Never bypass type checks without documenting the reason in
OWNER_TODO.md. - If a
manual_reviewgate cannot be verified: document inOWNER_TODO.mdand leave[ ]in BACKLOG.md.
Test quality¶
- Delete or rewrite broken tests. Do not skip them.
- Write new tests when the spec's "Required Tests" section demands them.
- Tests must use real fixtures, not mocked logic, for integration gates.
Scope discipline¶
- Undiscovered work →
OWNER_TODO.md. Never expand scope without owner approval. - If a dependency is missing, document it and leave the task
[ ]— do not implement a workaround.
Code quality¶
- Grep for every import, function call, and route reference before using it. Do not assume anything exists.
git greporfindfirst;catsecond; never assume.- Match existing code style exactly — no "this is cleaner" refactors in scope.
Commit discipline¶
One commit per task. No mid-task "WIP" commits in BACKLOG-driven runs.
Project-specific customization¶
Projects can override specific gate settings:
# tasks/.runner-config.yml (project override)
gates:
e2e:
app_maestro:
required: false # this project has no mobile app
tenants:
deutschland_fixture:
required: false # germany tenant not enabled for this project
Any gate not mentioned in the project override inherits the default from defaults/runner-config.yml.
Concurrency (runner.default_parallel)¶
Default parallel-slots per project is 3 — sized for safe multi-project use on an 8 GiB MemoryMax (each claude --print subprocess holds roughly 1 GiB resident, so the practical cap is around 6 concurrent tasks across all projects after dashboard + bash + system overhead).
Override per project by adding the runner section to tasks/.runner-config.yml:
Resolution order, highest priority first:
?parallel=Nquery string onrunner/start- CLI/UI override on the Start button
runner.default_parallelintasks/.runner-config.yml- Legacy
defaults.parallelin the same file - Hard default
3
RunnerStateSnapshot.effective_parallel exposes the value the last start_runner actually used (null when this dashboard session hasn't started a runner yet).
Smart concurrency (memory-aware throttle)¶
The dashboard watches the cgroup memory.peak / memory.max ratio and projects total RSS for the requested parallel value. Behaviour:
| Peak ratio | UI | Backend |
|---|---|---|
< 70 % |
No banner | Start always allowed |
70–85 % |
Yellow banner + tooltip on Start | Start allowed (warn-only) |
> 85 % |
Red banner + Start button disabled | 409 memory_pressure on Start |
Constants live in services/concurrency.rs:
PER_TASK_RSS_BYTES = 1.0 GiB
SYSTEM_HEADROOM_BYTES = 1.5 GiB
PEAK_BLOCK_RATIO = 0.85
PEAK_WARN_RATIO = 0.70
The 409 body carries current_runners, current_active_tasks, would_be_total, memory_peak_pct, memory_max_bytes, and a Swedish suggestion string the UI renders directly. The frontend disables the Start button proactively at 85 % so the user doesn't have to click and get refused.
Force-start (power-user opt-in)¶
Settings → "Tillåt force-start vid memory pressure" enables a secondary ⚠ Start button. Clicking opens a confirmation modal with the actual cgroup numbers; confirming sends force=true to the backend, which logs a WARN and bypasses the throttle. Use only when other runners are about to finish — the kernel cgroup OOM-killer is not patient.
CLI / scripts can pass ?force=true to POST /api/projects/{slug}/runner/start to achieve the same bypass.