Skip to content

Commit 966d78d

Browse files
DylanMerigaudclaude
andcommitted
README: document co-approvers, fix the lever list + layout
- Co-approvers per gate: the differentiator line and the workflow section now say a gate routes to several named approvers, editable in plain language ("also require the controller"). - Condition levers 8 -> 9: add "exception amount" (it's a real editor lever, was missing from the list). - Reframe the stale "wire approver identity" next-step: named people are on gates now; what's left is per-approver decision tracking + auth (decisions are gate-level today). - Project layout: add workflow-edit-agent.ts (the bounded loop, not a Mastra agent), workflow-validate.ts, condition-fields.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4ade442 commit 966d78d

1 file changed

Lines changed: 8 additions & 4 deletions

File tree

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The two halves are one loop: the workflow you derive and edit on the left is **e
66

77
AI is used in the places it earns its keep, and nowhere else. **Extraction** reads the messy vendor PDF into structured data (vision). **Onboarding discovery** maps an org's titles to approval authority (genuinely fuzzy judgement). **Investigation** judges a flagged exception against unstructured records and recommends. Everything else — matching, the workflow engine, reconciliation — is deterministic code, because a payment decision must be exact and repeatable, never a model's guess. Nothing posts until a human approves. Built with [Mastra](https://mastra.ai).
88

9-
The differentiator vs the workflow builders (Ramp, Zip, Pivot): you don't draw the approval graph on a canvas by hand. **The agent derives it from the HRIS, and you maintain it in plain language** — "above $25k also require CFO approval" — with a preview you approve or revert.
9+
The differentiator vs the workflow builders (Ramp, Zip, Pivot): you don't draw the approval graph on a canvas by hand. **The agent derives it from the HRIS, and you maintain it in plain language** — "above $25k also require CFO approval", "the director review also needs the controller to sign off" — with a preview you approve or revert. A gate isn't limited to one signer: it can route to several named co-approvers, each added or dropped by name.
1010

1111
### ▶︎ [Try the live demo →](https://ledgerloop-eta.vercel.app/)
1212

@@ -89,7 +89,7 @@ The manager gate fires on any exception **or** a clean bill over a floor ($1,000
8989

9090
**AI at the edges, deterministic code in the core.** The ends are language/perception/judgement problems — reading a PDF, mapping titles to approval authority, judging a fuzzy exception — so they use a model. The middle (is this a 9%-over variance? which gates apply? did every gate approve?) is arithmetic and graph logic, so it's pure, unit-tested functions ([`lib/matching.ts`](lib/matching.ts), [`lib/approval-workflow.ts`](lib/approval-workflow.ts), [`lib/approval-engine.ts`](lib/approval-engine.ts), [`lib/erp.ts`](lib/erp.ts)): exact, auditable, identical on every run. An LLM never decides a payment amount.
9191

92-
**The conditional approval workflow.** Approval isn't a single tier — it's a DAG of conditional gates ([`lib/approval-workflow.ts`](lib/approval-workflow.ts)): each step carries a `when` condition (route on amount / variance / department / verdict / vendor / currency / match type / exception code, combinable with all/any) and parallel `next` edges. The conversational editor knows the real values present (the org's departments, the invoices' vendors + currencies), so it only ever builds a gate that can actually fire, and the "What can I change?" doc lists those levers and values — routing is discoverable, not guesswork. The engine ([`lib/approval-engine.ts`](lib/approval-engine.ts)) walks it per invoice with collect-all semantics: a skipped gate is a transparent pass-through, several gates can pend at once, one rejection blocks downstream. **The workflow the onboarding agent derived is what the run executes** — it's passed into the run as input (held in client state, never persisted, so the run stays stateless), and the pipeline routes through it. When no workflow has been derived yet, a default DAG ([`lib/client-profile.ts`](lib/client-profile.ts)) built from simple thresholds stands in, so the pipeline works on a cold visit and behaves the same as a derived one.
92+
**The conditional approval workflow.** Approval isn't a single tier — it's a DAG of conditional gates ([`lib/approval-workflow.ts`](lib/approval-workflow.ts)): each step carries a `when` condition (route on amount / exception amount / variance / department / verdict / vendor / currency / match type / exception code, combinable with all/any) and parallel `next` edges. A gate also carries its **approvers** — a primary the agent resolved from the org chart, plus any co-approvers a human added (`approversOf(step)` is everyone who signs it); the editor and the canvas show them, and "also require the controller" adds one in plain language. The conversational editor knows the real values present (the org's departments, the invoices' vendors + currencies), so it only ever builds a gate that can actually fire, and the "What can I change?" doc lists those levers and values — routing is discoverable, not guesswork. The engine ([`lib/approval-engine.ts`](lib/approval-engine.ts)) walks it per invoice with collect-all semantics: a skipped gate is a transparent pass-through, several gates can pend at once, one rejection blocks downstream. **The workflow the onboarding agent derived is what the run executes** — it's passed into the run as input (held in client state, never persisted, so the run stays stateless), and the pipeline routes through it. When no workflow has been derived yet, a default DAG ([`lib/client-profile.ts`](lib/client-profile.ts)) built from simple thresholds stands in, so the pipeline works on a cold visit and behaves the same as a derived one.
9393

9494
**The HRIS adapter is real, captured, replayed.** [`lib/hris.ts`](lib/hris.ts) reads BambooHR (`bambooHris`, live HTTP) or replays a fixture captured from that same API (`recordedHris`); one `defaultHris()` factory picks live-vs-recorded — the only place that branch exists. The committed fixture in [`db/fixtures/bamboohr/`](db/fixtures/bamboohr/) is **real BambooHR output**, captured on a dated run via `pnpm hris:capture` — not a mock — so the demo (and CI, which has no key) runs on real data and survives the trial key expiring. `pnpm hris:seed` / `hris:reset` stand up a curated org in a sandbox (scoped by a dedicated Division, server-side, so reset only removes what it created).
9595

@@ -122,8 +122,12 @@ src/mastra/
122122
lib/
123123
approval-workflow.ts the conditional DAG model + condition evaluator + diff
124124
approval-engine.ts executes the DAG per invoice (fan-out, collect-all)
125+
workflow-validate.ts structural + AP-best-practice checks (the validator)
126+
condition-fields.ts the condition levers + their real run-sourced values
125127
onboarding.ts derive a workflow from an org (+ onboarding-model.ts)
126-
workflow-edit.ts conversational edits + diff (+ workflow-edit-model.ts)
128+
workflow-edit.ts conversational edits + diff (the edit ops)
129+
workflow-edit-agent.ts the bounded edit loop (Claude SDK, not a Mastra agent)
130+
workflow-edit-model.ts the Claude planner (structured-output op selection)
127131
hris.ts BambooHR adapter: live + recorded, one factory
128132
erp.ts QuickBooks adapter: pull POs + master data, live + recorded
129133
client-profile.ts per-client config; flat policy → DAG bridge
@@ -196,7 +200,7 @@ The decision logic is pure, typed, and unit-tested; the read-side integrations (
196200
- the ERP **pull** is real (QuickBooks); swap the **post**-side stub (`fakeErp`) and the Slack/Jira integration stubs for real adapters of the same interfaces,
197201
- live BambooHR + QuickBooks (both adapters + captured fixtures already exist; keys unlock the live path) and a second HRIS / ERP behind the same `HrisAdapter` / `PoSourceAdapter`,
198202
- the audit trail persists today (append-only `agent_runs`, replayable, reset nightly); drop the reset and add per-tenant scoping for a multi-client deployment, and persist the paused-run snapshot if you want resume to survive a server restart (today it's recomputed from the decisions),
199-
- wire real approver identity to the per-step gates,
203+
- gates already carry named approvers (a primary plus co-approvers); add per-approver decision tracking and auth so a multi-signer gate records **which** person approved, not just that the gate cleared (today a decision is gate-level — the single operator acts as the reviewer),
200204
- accept real uploaded PDFs at intake.
201205

202206
---

0 commit comments

Comments
 (0)