Turns expert task notes into agent instructions with linked sources, tracks edits and approval state in PostgreSQL, and gates approved workflows behind test cases before they reach business systems.
Python 3.12, FastAPI, SQLAlchemy 2, Alembic, PostgreSQL 16, pydantic v2, structlog, Prometheus metrics.
expert note (markdown) reviewers business systems
│ │ ▲
v v │
POST /notes ──> compiler ──> instruction set v1 ──> edits ──> review ──> approved
│ │ (JSONB, versioned) │ │
└── citations ─┘ every step cites │ test cases run by a
note lines + registered sources │ deterministic executor
│ │
blocked <── red green + approved
│
POST /publish ──> signed webhook + Jira
receipts, rollback
- Notes ingest.
POST /notestakes an expert's markdown or plain-text task notes with source references (URLs,doc:ids, ticket keys). A deterministic, rule-based compiler extracts preconditions, steps, decision rules, tools, expected outcomes and forbidden actions into a versionedInstructionSet. Every step carries citations back to the note's line range and to any referenced source; sources are stored with content hashes so a citation can be re-verified later. - Edits and approval state in PostgreSQL. Each edit is a row with author, reason and
unified diff, guarded by optimistic concurrency (
expected_version). Review followsdraft -> in_review -> changes_requested -> approved -> published -> retiredwith per-set required approval counts, reviewer roles and a full audit trail. - Test cases gate publication. Test cases pair an input scenario with expectations
(required actions, forbidden actions, expected outcomes and tools, halt or complete).
POST /instruction-sets/{id}/run-testsexecutes the set against a local rule-following executor and records the run. Publishing needs stateapprovedand a green run on the current version; a failing case blocks publish and is named in the response. - Reach business systems.
publishdelivers the version snapshot to a signed webhook target and a Jira comment plus attachment target, records receipts per target, androllbackre-delivers the previous published version. - Executor plugins, coverage and ops overview. Executor conditions are evaluated by
a small plugin registry (
flags,membership,comparebuilt in; register your own withexpertloop.executor.registry.register).GET /instruction-sets/{id}/coveragereports which steps and decision rules the test cases exercise, every test run stores its coverage, andGET /ops/overviewaggregates sets by state, coverage, drift, review SLAs and publish statistics. - Version diff and branching.
GET /instruction-sets/{id}/diff?from=1&to=3returns a structured, step-level diff (added, removed and changed steps with per-field before and after, plus section entries).POST /instruction-sets/{id}/branchcopies a version (the published one by default) into a new draft for experimentation; the branch has its own edits, tests and review.POST /instruction-sets/{branch}/mergethree-way merges it back into the parent head, taking one-sided changes and refusing with the list of conflicting steps and fields when both sides changed the same thing. - Review policies and SLAs. Each set carries a review policy: required approval
count, reviewer roles that must be among the approvers, whether the author of the
current version may approve it (off by default), and a review deadline in hours.
Submitting starts the clock;
POST /reviews/escalate(also run by the scheduler) writes areview_escalatedaudit event for overdue sets, andGET /reviews/workloadshows the queue with deadlines and what each reviewer still owes. - Source drift detection. Sources are re-hashed on demand (
POST /sources/{id}/rehash,POST /sources/check-drift, or re-registering with new content) or on a schedule (EXPERTLOOP_DRIFT_CHECK_INTERVAL_SECONDS). When a hash changes, every step citing that source is flaggedstale;GET /instruction-sets/{id}/driftlists the flags and publish is blocked until an expert re-verifies the steps or edits them. - API keys per role (
expert,reviewer,admin), OpenAPI docs at/docs, Prometheus metrics at/metrics.
make setup # uv sync (Python 3.12)
make lint # ruff
make test # pytest against PostgreSQL via Testcontainers
make demo # compose up (api + postgres + fake targets), migrate, run the demo
make down # stop the stack and drop its volume
The demo API listens on http://localhost:8090, the fake webhook and Jira on
http://localhost:8081. The demo Compose stack binds both services to loopback and
explicitly configures public example keys (dana expert, ravi and mei reviewers,
ops admin). Use these keys only for the local demo.
make demo registers eight sources, ingests three sample notes written for the demo from
samples/ (a refund SOP, an onboarding checklist, an incident triage note), shows the
citations, routes an edit through review, runs test cases, blocks one set on a
forbidden-action test, fixes and re-reviews it, publishes to the fake webhook and Jira
targets, and rolls one set back. The summary it prints is computed from the API's own
records:
== Running test cases and publishing approved sets
set 2 v1: PASSED (3 passed, 0 failed)
set 3 v2: PASSED (2 passed, 0 failed)
set 1 v1: FAILED (2 passed, 1 failed)
FAIL high value refund needs manager approval: required action not taken: 'manager'; forbidden action taken: 'Issue the refund'; execution was expected to halt but ran to completion
set 2 v1: delivered to webhook (receipt whr-1)
set 2 v1: delivered to jira (receipt 10002)
set 3 v2: delivered to webhook (receipt whr-4)
set 3 v2: delivered to jira (receipt 10005)
set 1: publish BLOCKED: publish blocked: failing test cases: high value refund needs manager approval
== Summary
notes ingested: 3
steps compiled: 18
citations linked: 26 (18/18 steps cited, 100%)
edits recorded: 3
approvals: 7 (changes requested: 1)
test runs: 5 (4 green, 1 red; 13 cases passed, 1 failed)
publishes blocked: 1
publishes delivered: 8 deliveries (4 versions to 2 targets), rollbacks: 1
receipts: 5 webhook (signed), 5 Jira comments, 5 Jira attachments
states: set 1=published (live v2), set 2=published (live v1), set 3=published (live v2)
The instruction set ids come from the database, so those are the ids a fresh stack prints;
running make demo again without make down in between continues the sequence.
make demo-check runs the same script in process against an empty schema and asserts this
block line for line.
web/ is a static site that runs part of the platform in the browser. web/src/sim/ is a
TypeScript port of the compile, drift, review, versioning, gate and delivery paths, so the
walkthrough executes those paths instead of replaying a recording; the executor condition
plugin registry, the coverage report and GET /ops/overview are not ported. It covers
citation coverage, stale sources, review policy and escalation, branch and merge conflicts,
the publish gate and rollback, and replays the run above with the summary block printed
here. The port is pinned to this repository by fixtures: tests/test_golden.py writes what
the Python compiler and executor produce into samples/expected/, and the browser
self-check reads those files and has to reproduce them. cd web && npm ci && npm run verify
runs its 61 assertions and produces dist/; see web/README.md.
All endpoints take X-API-Key. admin may call everything.
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /sources |
expert | Register a source (url, doc, ticket) with optional content; returns its hash |
| GET | /sources |
expert, reviewer | List sources |
| POST | /sources/{id}/rehash |
expert, reviewer | Re-hash a source (optional new content); flags citing steps when the hash changed |
| POST | /sources/check-drift |
expert, reviewer | Re-scan every live instruction set against current source hashes |
| POST | /notes |
expert | Ingest a note and compile instruction set v1 (optional required_approvals, review_policy); returns coverage and sources linked |
| GET | /notes, /notes/{id} |
expert, reviewer | Read notes |
| GET | /instruction-sets |
expert, reviewer | List sets with state and versions |
| GET | /instruction-sets/{id} |
expert, reviewer | Full document |
| GET | /instruction-sets/{id}/prompt |
expert, reviewer | Rendered agent prompt (text) |
| GET | /instruction-sets/{id}/citations |
expert, reviewer | Coverage plus per-citation hash verification |
| GET | /instruction-sets/{id}/versions |
expert, reviewer | Version snapshots |
| GET | /instruction-sets/{id}/diff?from=&to= |
expert, reviewer | Step-level diff between two versions |
| POST | /instruction-sets/{id}/branch |
expert | Copy a version (from_version, default published) into a new draft branch |
| GET | /instruction-sets/{id}/branches |
expert, reviewer | Branches of a set |
| POST | /instruction-sets/{id}/merge |
expert | Merge a branch into its parent (reason, expected_parent_version); 409 with conflicts |
| GET | /instruction-sets/{id}/drift |
expert, reviewer | Stale steps and open or resolved drift flags |
| POST | /instruction-sets/{id}/drift/verify |
expert | Re-verify stale steps (step_ids, or all) against the changed source |
| PATCH | /instruction-sets/{id} |
expert | Edit: expected_version, reason, full document, optional register_unknown_sources; 409 on stale version, 422 when the document or a citation is not valid |
| GET | /instruction-sets/{id}/edits |
expert, reviewer | Edit history with diffs |
| POST | /instruction-sets/{id}/submit |
expert | draft or changes_requested to in_review |
| POST | /instruction-sets/{id}/review |
reviewer | approve or request_changes with comment |
| GET | /instruction-sets/{id}/reviews, /audit |
expert, reviewer | Review decisions and audit trail |
| PUT | /instruction-sets/{id}/policy |
expert | Set review_policy (required_roles, allow_self_approval, review_deadline_hours) and required_approvals; 409 while in review |
| GET | /reviews/workload |
reviewer | Queue of sets in review with deadlines, missing roles and per-reviewer pending, overdue and decided counts |
| POST | /reviews/escalate |
reviewer | Escalate every overdue review once; the scheduler runs this too |
| POST | /instruction-sets/{id}/test-cases |
expert, reviewer | Add a test case (scenario, expectations) |
| POST | /instruction-sets/{id}/run-tests |
expert, reviewer | Execute all cases on the current version |
| GET | /instruction-sets/{id}/test-cases, /test-runs |
expert, reviewer | Cases and recorded runs (each run carries its coverage) |
| GET | /instruction-sets/{id}/coverage |
expert, reviewer | Steps and decision rules exercised by the test cases on the current version |
| POST | /instruction-sets/{id}/publish |
admin | Deliver to targets; 409 when not approved, stale, or latest run is not green |
| POST | /instruction-sets/{id}/rollback |
admin | Re-deliver the previous published version |
| POST | /instruction-sets/{id}/retire |
admin | published to retired |
| GET | /instruction-sets/{id}/publications |
expert, reviewer | Delivery receipts |
| GET | /ops/overview |
expert, reviewer | Sets by state, coverage of latest runs, open drift, review SLAs, publish stats |
| GET | /ops/plugins |
expert, reviewer | Registered executor condition plugins in evaluation order |
| GET | /healthz, /metrics |
none | Health and Prometheus metrics |
Test case expectations: required_actions, forbidden_actions, expected_outcomes,
expected_tools (substring matches against the execution trace), must_halt,
must_complete. Scenarios carry facts (compared by conditions such as
amount is over 500, reason is fraud, error rate exceeds 5 percent) and flags
(free-text conditions that are simply true). Conditions are evaluated by the plugin
registry in expertloop/executor/plugins.py: a plugin returns True or False when it
understands a condition and None to pass; flags, membership (role is one of admin, owner, region is in eu, uk or region in (eu, uk); a bare English in is not a
membership operator, so logged in user is admin is a comparison) and compare ship built
in, and registry.register(plugin, first=True) puts a custom grammar ahead of them.
| Table | Purpose |
|---|---|
sources |
kind, ref, optional content, content_hash (SHA-256), last_checked_at, unique per kind and ref |
notes |
Raw expert notes with author |
instruction_sets |
Head document (JSONB), state, version, published_version, required_approvals, review_policy (JSONB), review_round, submitted_at, review_deadline_at, escalated_at, parent_id, branched_from_version, merged_at, merged_into_version |
instruction_set_versions |
Immutable document snapshot per version |
edits |
Author, from/to version, reason, unified diff |
review_decisions |
Reviewer, role, version, review round, decision, comment |
audit_events |
Actor, action, from/to state, detail for every change |
test_cases |
Name, author, scenario, expectations |
test_runs |
Version covered, status, pass/fail counts, per-case results and traces, coverage summary |
publications |
Version, action (publish, rollback), target, status, receipt |
drift_flags |
Step, source, cited and current hash, detected by and at, resolution (reverified, edited) |
Schema is managed by Alembic (alembic/versions/).
Environment variables (prefix EXPERTLOOP_, see .env.example): DATABASE_URL,
API_KEYS (name:role:key,...), WEBHOOK_URL, WEBHOOK_SECRET, JIRA_BASE_URL,
JIRA_ISSUE_KEY, JIRA_TOKEN, DEFAULT_REQUIRED_APPROVALS, DRIFT_CHECK_INTERVAL_SECONDS
(0 disables the scheduled scan).
The application has no default API keys: authenticated endpoints return 401 until
EXPERTLOOP_API_KEYS is configured. Generate a separate random key for each identity
(for example, python -c 'import secrets; print(secrets.token_urlsafe(32))') and store
the resulting name:role:key,... value in your deployment's secret configuration.
An explicitly empty api_keys=[] passed to create_app also denies all authenticated
access, even when other application instances have configured keys.
expertloop/
compiler/ note parser and rule-based compiler with citations
sources/ source registry and citation verification
workflow/ approval state machine
executor/ deterministic executor, condition plugin registry, test runner, coverage
targets/ signed webhook and Jira delivery adapters
fakes/ fake webhook receiver and Jira for demos and tests
routers/ FastAPI routes (sources, notes, instruction sets, reviews, ops)
drift.py source re-hashing, drift flags and the scan scheduler
reviews.py review policies, deadlines, escalation and reviewer workload
versioning.py step-level diff and three-way merge of documents
service.py audited business logic
demo.py end-to-end demo driver
alembic/ migrations
deploy/ docker-compose stack
samples/ three sample notes used by the demo and tests, and the compiler and
executor fixtures the browser port is checked against
tests/ pytest suite (PostgreSQL via Testcontainers)
web/ static browser demo (Vite, React; ports the compile, drift, review,
versioning, gate and delivery paths)
See ARCHITECTURE.md for the compiler, citation, state machine, gating and delivery
design, and CONTRIBUTING.md for the development workflow.
- The compiler is a fixed set of rules, not a parser for English. It has been exercised on
the three sample notes, the golden fixtures in
samples/expected/and the compiler unit tests. It handlesif X then Yandif X, Yrules,never/do not/must notclauses,so that/until/expected:outcomes, and readsdo not proceed until Xas a guard on the step rather than a forbidden action. Anotherwiseclause is left in the step's action:If X, do it, otherwise holdcompiles to one conditional step, so when X is false the executor skips the step and the else branch with it. - Delivery adapters have been exercised against the fake business systems in
expertloop/fakes/server.py, which check the shape of each request, and not against a Jira tenant or a production webhook receiver. - The executor is a rule follower, not a model: it decides conditions with the plugin registry and has no notion of a step it does not understand.
- The browser demo ports part of the platform;
web/README.mdlists what is not ported.
| Version | Theme | Adds |
|---|---|---|
| 1.0.0 | Compile, review, gate, deliver | Deterministic note compiler with citations, source registry with hashes, edit history with optimistic concurrency, review state machine, test-case publish gate, signed webhook and Jira targets, rollback |
| 2.0.0 | Source drift | Re-hash sources on demand or on a schedule, stale flags on citing steps, publish blocked until re-verified or edited, GET /instruction-sets/{id}/drift |
| 3.0.0 | Review policies and SLAs | Required reviewer roles, no self-approval, review deadlines with escalation events, GET /reviews/workload |
| 4.0.0 | Diff and branching | Step-level diff between versions, branch a draft from a published version, merge back with conflict detection |
| 5.0.0 | Plugins, coverage, ops | Executor condition plugin registry, test coverage report per set and per run, GET /ops/overview |
| 5.1.0 | Typed documents, closed-by-default keys, browser demo | Pydantic document schema with provenance-checked citations, row locking on every write, idempotent delivery with an ADF Jira comment, no default API keys, golden compiler and executor fixtures, and the browser demo in web/ extended to the drift, review, versioning, gate and delivery paths |
Releases 1.0.0 through 5.0.0 each ship an Alembic migration (0001 through 0005);
5.0.1 and 5.1.0 change no schema. Every release has tests against PostgreSQL and a
changelog entry below. Tags are v1.0.0 through v5.1.0.
expertloop/document.pytypes the instruction document with pydantic. A citation now has to be a line range in the note the set was compiled from or a reference to a source of a known kind, so an empty citation object or a line past the end of the note is a 422 rather than provenance. An edit that cites a source the registry does not hold is refused unless it passesregister_unknown_sources.- Documents that leave out an optional list no longer reach a
KeyError:render_promptand the executor read every section with a default, and a decision rule without a condition is rejected at the boundary. - The compiler reads
do not proceed until Xas a guard on the step rather than a forbidden action, and no longer treats a negated stop word as an instruction to halt.membershiprequires the operator to be spelled out, sologged in user is adminis a comparison. - Delivery carries a
delivery_idthat is stable across retries, skips a target that already holds that version, posts the Jira comment as an Atlassian Document Format body, and audits a failed rollback. - Every service function that writes state takes a row lock on the instruction set.
- Authentication has no default keys.
api_keysships empty instead of four documented demo keys,authenticatechecks only the keys configured for the running application, and an unconfigured or explicitly empty list returns401from every authenticated endpoint. The demo Compose stack passes its example keys explicitly and binds the API and the fake targets to loopback. tests/test_golden.pywrites what the compiler and executor produce intosamples/expected/, and the browser demo's self-check reproduces those files.- The static browser demo in
web/gained the source drift, reviewer workload, versions and merges, test gate and whole-run sections, so the port now covers the compile, drift, review, versioning, gate and delivery paths;web/README.mdlists what it leaves out. - The demo's approval state machine is HTML rather than a scaled SVG, so its labels hold their size at phone width, and the muted text tokens were adjusted to pass WCAG AA contrast.
GET /ops/overviewcounted a rollback once per delivery target; it now counts each rollback once, matching the demo summary.
- Executor conditions go through a plugin registry (
flags,membership,comparebuilt in); deployments register their own grammars. GET /instruction-sets/{id}/coveragereports the steps and decision rules the test cases exercise; every test run stores a coverage summary.GET /ops/overviewaggregates sets by state, coverage, drift, review SLAs and publish statistics;GET /ops/pluginslists the registry.- Migration
0005addstest_runs.coverage.
- Step-level diff between any two versions of an instruction set.
- Branch a draft from a published (or any) version, work on it in isolation, and merge it back with three-way conflict detection; conflicts are audited and returned as 409.
- Migration
0004addsparent_id,branched_from_version,merged_atandmerged_into_versiontoinstruction_sets.
- Review policies per instruction set: required reviewer roles, no self-approval of a version you authored (unless the policy allows it), and review deadlines.
- Overdue reviews are escalated with an audit event, on demand or by the scheduler;
GET /reviews/workloadlists the queue and per-reviewer load. - Migration
0003addsreview_policy,submitted_at,review_deadline_atandescalated_attoinstruction_sets.
- Source drift detection: re-hash sources on demand or on a schedule, flag citing steps
as stale, block publish until an expert re-verifies or edits them, and report it all
through
GET /instruction-sets/{id}/drift. - Migration
0002addsdrift_flagsandsources.last_checked_at.
- Initial release: deterministic note compiler with citations, source registry with hashes, edit history with optimistic concurrency, review state machine, test-case publish gate, signed webhook and Jira targets, rollback.
MIT