Key design decisions
The short version: trust is a vector, not a scalar; every gating signal is opt-in and additive; verifiers can never raise confidence; deterministic checks fail closed; calibration is keyed by prompt and agent version so history never silently pools across configurations.
The full list, verbatim from the project’s own design notes:
| Decision | Rationale |
|---|---|
Single /webhook endpoint |
Source agnostic — parsers handle differences, pipelines don’t care |
Generic source requires explicit pipeline |
Callers control the sending tool, so they can always name the pipeline |
| YAML pipeline configs | Git-controlled, human readable, no UI needed |
| Structured JSON output from LLM | Makes flow control deterministic — runner reads confidence/proceed, not prose |
| Extra fields allowed on LLMOutput | Domain fields (jira_ticket, action, etc.) pass between steps without schema changes |
| Isolated session key per step | Prevents context bleed between concurrent runs and between steps |
| SQLAlchemy async ORM, dialect swap via config only | SQLite for zero-infra local dev, Postgres for production — same code path |
| Alembic with auto-upgrade-on-boot | The hand-rolled add-column/add-index mechanism only ever adds nullable columns — it outgrew that at ~20 columns and couldn’t rename, drop, backfill, or alter types. Auto-migrate-on-boot keeps the zero-ops experience; database.auto_migrate: false hands control to a DBA |
| DB-level partial unique index for in-flight dedup | The application-level pre-check narrows but cannot close a TOCTOU race on its own — the DB constraint is the actual correctness guarantee, the pre-check just avoids the round-trip in the common case |
| Adapter pattern for executors | Swap or mix backends with config changes only; steps in the same pipeline can use different executors |
| Runner owns flow decisions | LLM recommends, service decides — never blindly chain prompts |
executor:name agent identity in DB |
Disambiguates same agent name across different backends in run history and success rates |
| Artifact content on disk, not in DB | SQLite is not a blob store; large documents stay in the filesystem. DB row holds only the reference. |
{{artifacts.step.key}} explicit namespace |
Template authors know they are pulling a potentially large blob. Keeps when: conditions and steps.* references unambiguous. |
LocalArtifactStore behind ABC |
Swapping in S3 or another backend requires only a new class implementing four methods — no runner or config changes. |
| In-process APScheduler for cron | Zero extra infrastructure; same DB and runner code path as webhooks |
POST /reload + SIGHUP |
Config-driven system should never need a restart for a YAML edit |
Step library with use: references |
Eliminates step config duplication across pipelines; resolved at load time so runner is unaffected |
executor_config deep-merge on library steps |
Lets pipelines add model or thinking_level without repeating the full agent/session_key block |
| Structured run event log in DB | Per-run timeline queryable from the UI without grepping stdout; survives process restarts |
uvicorn.access separated from service logs |
HTTP request noise no longer pollutes run event output on stdout or in service.log |
In-flight dedup always wins regardless of window_seconds |
Prevents two overlapping triage/remediation runs for the same alert — the dangerous case — independent of how the recency window is tuned |
Alert status (firing/resolved) folded into the Alertmanager fingerprint |
A resolve notification must never be suppressed as a duplicate of the firing run it’s closing out |
trigger.dedup as a sibling of trigger.match, not inside it |
Keeps match purely about resolver conditions (_matches() iterates every key as a field/label comparison) — dedup is an execution-policy concern, not a matching condition |
stage defaults to testing, not production |
New/WIP pipelines are safe by default — nothing pages a real human or counts toward metrics until someone deliberately promotes it |
pipeline_runs.stage persisted per-run, not joined from the live config |
Captures stage-at-run-time — promoting a pipeline to production never retroactively moves prior testing runs into production metrics |
stage gates four outbound paths individually rather than one flag |
notifications:, executor: notify, on_failure.webhook, and executor: human are genuinely independent side-effect sources — muting the pipeline as a whole would still need per-path logic, so it’s implemented where each one fires |
No UI toggle for stage |
Consistent with tags/version — pipeline behaviour stays entirely git-controlled config, reviewable in a diff |