ADR-0009: Execution modes: CI, local/dry-run, explain, webhook service¶
| Status | Accepted (partial: one-shot arming restrictions per ADR-0017 §4; P2-E5) |
| Date | 2026-07-21 |
| Deciders | Konrad Heimel |
| Context links | ADR-0005 forge · ADR-0007 effects |
Context¶
The same decision pipeline must run: in CI per MR (primary); on a developer's machine before pushing ("what would the gate say?"); in a debugging session ("why did it say that?"); and — for orgs that prefer event-driven operation over per-repo CI jobs — as a webhook receiver. The v1 scaffold said "no long-lived service"; that hardens into: the core is a one-shot pipeline; the service is a thin wrapper around it, so serve-mode support is an architecture constraint now even if the wrapper ships later.
Decision (proposed)¶
One core pipeline (ingest → classify → evaluate → aggregate → publish), four entrypoints:
| Mode | Command | Publisher | Notes |
|---|---|---|---|
| CI | run |
real forge writes | reads MR context from CI env vars (GitLab CI first) |
| Local / dry-run | run --dry-run (also default when no MR context) |
recorder: prints decision, findings, and every action it would take | works against local branch vs target ref; no token needed unless providers require it |
| Explain / debug | explain (or run --explain) |
recorder + full trace | per-change: detected classes, routed packs/bindings, matched rules, predicate results, score arithmetic, aggregation path |
| Webhook service | serve |
real forge writes | long-lived; subscribes to MR events, clones/checks out the branch per event (ADR-0008 §4), then runs the identical pipeline |
| Historical scan | scan --since <date> \| --mrs <range> |
recorder only | replays past (even merged) MRs through the current policy set: backtesting a pack before enabling automerge, calibration ("what % would have automerged?"), regression checks after policy changes. Emits one JSON report per MR |
| Statistics | stats <reports-glob> |
n/a | aggregates JSON reports (from run or scan) into automerge rate, outcome distribution, top firing rules, score histograms — flat files, no database for now (ADR-0012) |
Side effects are isolated behind the Publisher port; dry-run swaps in a recorder — the decision core cannot tell the difference. The JSON report is emitted in every mode and is byte-identical between a dry-run and a real run on the same input (determinism gate).
Shipping order: run/--dry-run/explain in v1; serve in v1.x once the CI path is proven
(OQ-14) — but ports and CLI structure assume it from day one.
Consequences¶
- CI env parsing (which vars identify the MR) is adapter code, not core; adding GitHub Actions later touches only that layer + forge adapter.
serveintroduces state concerns (event dedup, re-evaluation on thread resolution) that the one-shot modes don't have; these must be spec'd before it ships, not bolted on.- Every doc example can show the dry-run first — the adoption path starts with zero risk.
Counterpoints considered¶
- "Webhook-first like a bot framework." — Event-driven is operationally heavier (state, HA, secrets custody) and most target orgs can add a CI job trivially; CI-first keeps the trust story simple ("it runs in your pipeline with your token").
Amendment (2026-07-21, adversarial review F2/F3): the challenge-resolution mechanism¶
One-shot CI cannot observe thread resolution (forges do not trigger pipelines on it), so the original "merges after all threads are resolved and re-evaluation passes" promise had no mechanism. Fixed as follows — the forge, not assent, enforces resolution:
- On
challengefindings (and no block), assent posts the resolvable threads, records the decision, approves conditionally and arms forge auto-merge pinned to the evaluated SHA (GitLab: "merge when pipeline succeeds" + all-discussions-resolved merge gate,merge?sha=; GitHub mapping per OQ-7). The forge merges when every thread is resolved. - Resolution alone does not re-run assent — this is now an explicit, documented
property, compensated by: any new push cancels the armed merge and re-evaluates
(forge-native), the SHA pin (ADR-0015 §2) guarantees only the evaluated commit can merge,
and fact staleness is bounded by
facts.max_age(ADR-0015 §3). - Repos that need genuine re-evaluation on resolution (e.g. re-checking facts at merge
time) use
servemode (v1.x) — that is its primary justification.
Adoption prerequisite (per ADR-0015 §4): the repo's forge settings must enable the
all-threads-resolved merge gate; assent doctor verifies this and the protected-pipeline
topology before the tool arms any auto-merge.
Amendment 2 (2026-07-21, second review P2-12): scan honesty¶
scan also records each historical MR's actual outcome (merged / closed / reverted) and
stats reports the decision-vs-outcome confusion matrix — "61% would-have-automerged" only
proves self-consistency; "of the MRs we would have automerged, how many did humans merge
unchanged?" is the number that earns trust. Batch apply over past MRs stays out of scope
(recorder-only; OQ-20).