An agentic AI platform that orchestrates the software development lifecycle: a Jira ticket enters, and specialised AI agents drive it through specification, implementation, review, deployment, and post-deploy monitoring — coordinated by webhooks from Jira, GitHub, GitHub Actions, and Argo CD.
Portfolio project. See docs/AGENTIC_SDLC_DESIGN.md
for the full design, docs/plans/phase-1.md for this
phase's implementation plan, and docs/adr/ for why key
decisions were made the way they were.
Durable, event-sourced ticket state machine; HMAC-verified, idempotent
webhook ingress for Jira/GitHub/GitHub Actions/Argo CD; a Postgres-backed
WorkflowEngine and an in-memory fake passing the same contract-test
suite; a dispatcher/runner/timer-sweeper worker; admin API for human
approval gates; docker compose up from a clean clone. No LLM calls
yet — every agent is adapters/fake/agent_runner.py returning canned
results, by design (see the design doc's phase gates).
Verified against a live docker compose stack (not just fakes):
scripts/drive_lifecycle.py walks a ticket INTAKE -> OBSERVING through
real HTTP + real Postgres; an identical webhook replayed twice is a
no-op (appended then duplicate, one event row); stopping the worker
container mid-lifecycle, firing the next webhook, then restarting worker
resumes exactly where the event log left off; and injecting a deliberate
import sqlalchemy into core/domain/ticket.py makes lint-imports fail
with the exact file and line, confirming the layering is actually
enforced, not just configured.
cp .env.example .env # fill in dev-only secret values, e.g. any string
docker compose -f deploy/compose/docker-compose.yml up --buildOnce api is healthy (curl localhost:8000/healthz):
uv sync --all-groups
uv run python scripts/drive_lifecycle.py PROJ-1This fires the full external-event sequence (Jira → spec approval →
GitHub PR → CI → deploy approval → Argo CD staging → Argo CD prod) against
the running stack and prints the ticket's state after each step. It stops
at OBSERVING: the final OBSERVING -> CLOSED transition is a real
30-minute durable timer, not something a demo script should block on.
Check on it later with:
curl localhost:8000/admin/tickets/PROJ-1uv sync --all-groups
make check # ruff check + format --check, mypy --strict core/, lint-imports, pytest
make test # pytest only (unit + integration + contract; contract spins up
# a real Postgres via testcontainers)
make demo # scripts/drive_lifecycle.py against a running compose stackContract tests need Docker. On Docker Desktop for macOS specifically,
tests/contract/conftest.py disables testcontainers' Ryuk reaper — Ryuk's
own container fails to start under Desktop's VM (bind-mounting the host
docker.sock isn't supported there); this doesn't affect Linux CI runners.
core/— domain types, the state machine (decide()is a pure function; seecore/statemachine/), and every port (Protocol). Imports nothing vendor-specific — enforced byimport-linterin CI.adapters/— one directory per vendor (fake/,postgres/,env/,stdout/,local_yaml/,system/), each implementing a subset of the ports.fake/andpostgres/both implementWorkflowEngineand pass the identical contract suite.api/— FastAPI webhook ingress (verify → persist raw → idempotent append → 200 fast, never process inline) and the read-only/admin routes.workers/— the dispatcher (event → state transition), runner (command → agent/adapter call → follow-up event), and timer sweeper. This is the only place ticket state actually changes.bootstrap.py— the composition root shared byapi/andworkers/, since import-linter's layers contract keeps those two independent siblings.
This is deliberately not org-scale infrastructure:
- Multi-tenancy and org-level RBAC / SSO
- SOC 2 evidence collection and compliance reporting
- Disaster recovery, multi-region, HA Postgres
- Automated key rotation
- A code-execution sandbox hardened enough for genuinely untrusted code (the current sandbox design assumes semi-trusted agent output)
- Fine-tuning, model training, or custom evals beyond the golden-ticket harness
- Cost optimisation beyond tiering and caching (no request batching, no distillation)
Each omission is a scoping decision, not an oversight — see design doc §13.