ADR-0064 — The eval executor runs on Temporal (orchestrator workflow drives signal-stepped execution of the real maestro workflows)
- Status: Proposed (scoped 2026-06-20)
- Deciders: Gera (+ coordinator session 245816c7)
- Supersedes / relates: ADR-0061 (maestro package→queue), ADR-0031 (maintenance on Temporal), ADR-0026 (Temporal PII boundary). Builds on the evidence ledger (PR #2524) + Pipeline Lab.
- Name: the executor's user-facing name is Morpheus — it runs the real eval and reveals the real (Matrix-universe sibling of Agent Smith; persona-named like Maestro). The honesty fake-detector (#2528) is Sherlock — it deduces lies from real-source evidence. The Temporal workflow stays domain-named
evalExecutorWorkflow(name-by-domain rule); "Morpheus" lives on the UI/feature only. The system cast: Maestro routes inbound → Morpheus runs the eval for real → Sherlock catches the lies → Wall of Shame posts the verdict → Evidence Ledger keeps the record.
Amendment (2026-07-09 — D1/D2 locked,
docs/planning/replication-morpheus-gate.md§0): Morpheus's charter expands from executor-only to scenario supply + drive: Morpheus creates AND runs the simulations. He owns the test corpus — the hand-authored goldens become few-shot exemplars that teach the generator what a good scenario/spec looks like, not merely the fixed tests — plus the net-new generator (perturbation as the stepping stone; exemplar-guided flow-suite generation as the target; every generated spec passes human review before it is gate-eligible). The executor described below is unchanged and remains Morpheus's "drive" half. Per the same decision set (D2), Sherlock = ALL validation: the in-run checkpoint grading is re-labeled as Sherlock's in-run pass alongside his post-hoc fake-detector — firewalled from Morpheus, because the thing that generates tests must never be the thing that grades them. Where the "Name" line above reads "runs the real eval," read "creates AND runs the simulations." The build loop that consumes these verdicts is The Replication (workers: Replicas).
Context
The maestro/maintenance golden eval ("does Clara behave?") today runs only as a local CLI harness (scripts/temporal/run-golden-eval.ts): it boots a local temporal dev server + workers, drives the leak-204 scenario turn-by-turn against isolated *-eval queues + prod DDB, grades each checkpoint, and captures the run to EVAL_RUN#maestro:<runId> (which the /admin/dev/evals Maestro tab now renders as an evidence ledger).
We want to run/test the evals FROM the page like a debugger: a list of evals (each described high-level — "work-order issue → back-and-forth → the Clara→maestro packages it expects"), pick one, press play, watch it execute the whole flow live — play-all or play-to-checkpoint with pause/step — clicking entity tabs (Work Order → AppFolio link + PropFlow link) as they're created, with the maestro diagram showing where the flow is. And it must be read-verified: everything shown is an independent READ of the real surface we claim to have written (read the Twilio Messages API for the SMS; read AppFolio for the WO; read DDB for the row) — never "print what we just wrote."
The load-bearing question: where does the run execute? It can't be the user's laptop forever (not shareable, not "anywhere"), and it can't be a Vercel serverless function (short-lived, stateless — can't boot temporal/workers or hold a paused/stepped session).
Prime directive — honesty over correctness (the Wall of Shame)
PropFlow is an AI-agentic-native platform; our agents are only as trustworthy as our ability to tell when they're lying to us. So the eval playground's prime directive is honesty over correctness: failing an eval is fine — lying about it is not, because a lie hides real breakage and tricks us into thinking something's OK when it isn't. The eval space must be as real as it can possibly get (real phone numbers, real AppFolio, real DDB, real visibility), and any fakeness is caught and recorded.
Definition of truthful (the bar)
A claim is truthful iff it is backed by an independent READ of the real surface it claims to have affected. "We sent a message" is true ONLY if a Twilio read confirms the message exists. The UI showing tenant→handyman "how's it going" is honest ONLY if it reflects a Twilio / AppFolio / DDB read, not the value we just wrote/printed. Read-verified = truth; an unbacked claim is a fake. We read the surface we claim to write — never echo the write.
The Wall of Shame (a first-class, data-backed honesty ledger)
Every detected act of fakeness is forced to a durable, append-only schema and surfaced on the eval page as a growing shame leaderboard. The point isn't punishment theater — it's that we literally have data points for agent dishonesty, trended over time, so the lesson lands: the goal is not to pass evals; the goal is to never lie.
- Schema (DDB, append-only)
EVAL_SHAME#<id>:{ id, at, runId, scenarioId, actor (model / agent / component that made the claim), kind, claim (what was asserted to have happened), reality (what the independent read actually showed), evidence (the read + its provenance: twilio-read / appfolio-read / ddb-read), severity }. kindvocabulary (the named offenses):assumption(asserted without checking),mock-data(used fabricated data),pretended-result(claimed a result it didn't get),unbacked-claim(no read supports it),fabricated-id(minted a system-of-record id locally — ADR-0030),fabricated-link(a URL not derived from a real entity),fallback-stub(?? 'N/A'/|| 'TBD'on a field treated as real).- Detection (where the rows come from — not self-reported, caught): the executor cross-checks
every action-claim against its independent read-watcher; a claim of "happened" that the read
contradicts (or that no read backs) writes a shame row. The existing drift guards
(
no-pf-stub-drift, the ADR-0030 id rules) are detectors too. The agent does not get to grade its own honesty — the read is the judge. - Surfacing: an eval-page "Wall of Shame" tab — leaderboard by actor + kind, recent offenses with the contradicting read shown as evidence, and the all-time count that only grows.
This pillar binds the rest of the design: the read-watchers (below) are both the truth source AND the fakeness detector.
Decision
Run the eval executor on Temporal, as a two-layer workflow design on the existing worker fleet (Temporal Cloud + Fargate), with the Vercel page as a thin client — built directly on Temporal (no separate local MVP step).
Layer 1 — the eval-orchestrator workflow (the "debugger session")
One evalExecutorWorkflow per run (workflowId eval-<scenarioId>-<runId>), on a dedicated eval-orchestrator queue. It:
- drives the golden scenario turn-by-turn (the same turn-drive + per-checkpoint grade the CLI harness uses — ONE harness, the executor adds a hook, no fork);
- holds the pause/step gate natively: between checkpoints it
await condition(() => resumeRequested)— Temporal's signal/condition primitive IS the pause button (durable, no in-memory session to lose); - receives signals from the page:
step(advance one checkpoint),resume(play-all from here),pause; - exposes queries for live state (current checkpoint, graded cells, created entities, decisions) so the page draws the playhead without polling DDB;
- captures the final run to
EVAL_RUN#maestro:<runId>(reusecaptureMaestroEvalRun) so every executed run is also a viewable ledger run.
Layer 2 — the real execution it kicks off
The orchestrator starts the actual maestro/maintenance Temporal workflows — the inbound-package → reasoning-brain path (ADR-0061), maintenanceWorkflow, the comms workflow — on the isolated *-eval queues (INBOUND_PACKAGE_TASK_QUEUE_OVERRIDE, MAINTENANCE_TASK_QUEUE_OVERRIDE, MAINTENANCE_COMMS_TASK_QUEUE_OVERRIDE). This is the real execution, not a simulation — the same code prod runs, physically isolated from real customers' queues.
Read-based verification (a hard invariant)
Every value the page shows is produced by an independent read of the real surface, with a provenance badge (the evidence-ledger contract, extended live): twilio-read (Twilio Messages API), appfolio-read (L4 read / audit), ddb-read (getItem), judge. A "watcher" activity reads the real surface after each action to confirm it landed — we read where we claim to write, never echo the write. Depicted ⊆ verified.
Where each piece runs
- Execution: Temporal Cloud + the Fargate worker fleet (prod infra; isolated eval queues). Not the laptop.
- UI + control plane: Vercel page (
/admin/dev/evals) — starts the orchestrator (via an API route that signals/starts the workflow), sends step/resume/pause signals, reads queries + the ledger, renders. Thin client; holds no run state. - Batch (secondary): GitHub Actions remains the lane for nightly / on-PR eval runs (non-interactive) feeding the same ledger.
The truth pipeline — always-emit → listen → read-verify
The UI must NEVER "assume it wrote, so proceed." It reflects only what the real surface confirms. The chain, end to end:
- Always-emit (no eval/prod flag). The real maestro/maintenance workflows emit a domain
event whenever they trigger or complete an action (
message.sent,wo.created,wo.dispatched,photo.attached,maestro.decision, …) onto an event bus — unconditionally, in every environment. Decision: noisEvalflag gates emission. Rationale (Gera): none of this is time-critical, always-on capture is simpler than flag-branching, and a permanent truth feed is strictly more useful. The emit is cheap (an event publish); "it's OK if it's a little slow" — correctness/honesty beats latency here. - Listen (the "got it", not the "assume"). The eval session is a listener on that bus (the webhook-listener model Gera described): when an event lands, it knows a thing happened and when — the trigger to go verify. It does not infer success from "we called the function."
- Read-verify (the truth shown). On each caught event, a read-watcher does an independent READ of the real surface — Twilio Messages API for the SMS, AppFolio for the WO, DDB for the row — and THAT read is what the UI shows, with its provenance badge. Emit says when; read says what's real.
- Honesty gate. If an emit claims an action but the read can't confirm it (or nothing emitted for a claimed action) → a Wall-of-Shame row. The read is the judge.
Controls — pause / play, no rewind
The orchestrator workflow gives play-all and play-to-checkpoint, with pause and
play/step to advance. No rewind (a run is a real forward execution against real surfaces —
you can't un-send a text; replay a past run from the ledger instead). On pause, everything
shown is linked to the real place: a created WO links to its work-order page (/maintenance/<id>)
- its AppFolio URL; a conversation links to the conversation page — because the data genuinely exists at that moment. (After teardown the links 404 — the data was deleted; the ledger keeps the captured evidence. That's expected and honest: live = real links, post-teardown = archived evidence.)
Build directly on Temporal (no MVP step)
Per Gera: skip the local-first MVP — build the Temporal implementation directly. The page talks to
the run via one controller contract — start(scenario, mode) / step() / resume() / pause() / subscribe() — implemented over Temporal: start = start evalExecutorWorkflow; step/resume/
pause = signals; live state = the event-bus stream the listener accumulates (+ workflow
queries for a consistent snapshot). The catalog, read-watchers, CheckpointGrid/MaestroGraph
render, and ledger capture are built once against this contract.
Alternatives considered
- Vercel executes the run. Rejected: serverless functions are short-lived + stateless — can't boot temporal/workers, can't hold a paused/stepped session across requests.
- Local-only forever. Rejected as the end state (not shareable / not "anywhere"); kept as the MVP + the dev-debugging path.
- GitHub Actions executes interactively. Rejected for the debugger UX (batch/log-only, no live step from the page); kept as the batch lane.
- A bespoke long-running Node service (ECS) holding in-memory sessions. Rejected: reinvents what Temporal gives free — durable pause (
condition), signals, queries, replay, history-as-record.
Consequences
- Pros: pause/step/resume are native Temporal primitives (durable, survives restarts); execution is prod-faithful + isolated; the page is a thin client; the run is replayable from history; the MVP isn't wasted.
- Cons / costs: a new orchestrator workflow + queue + worker registration; a signal/query API surface; bridging workflow events → the page's live stream (SSE/poll over queries); the orchestrator must drive child workflows + read-watch their effects (more moving parts than the CLI loop). PII boundary (ADR-0026) applies — conversation content rides DDB scratch / the ledger, never workflow history.
Resolved design decisions (the former open questions)
- (a) Child workflows — observe, don't await. The orchestrator starts the real maestro/maintenance workflows then observes via the emitted events + read-watchers — it does NOT block on child completion inline. The real pipeline is async/debounced; a faithful spectator watches its emitted truth, it doesn't reach in. (Matches "listener catches it" over "assume it wrote.")
- (b) Live transport — the always-emit event bus is primary; queries are the snapshot. The page
receives updates from the listener over the bus (push, the webhook-listener model), with workflow
queries available for a consistent point-in-time snapshot (e.g. on pause / reconnect). No
isEvalflag — emission is always on. - (c) Dedicated eval-orchestrator worker + queue. Yes — its own queue so eval orchestration
never competes with prod maintenance throughput and can be deployed / scaled / killed
independently. The real execution still runs on the isolated
*-evalqueues (existing pattern). - (d) Admin-gated + explicit go. Firing a run does real bench-persona sends, so: admin-only
- an explicit confirm, announce "~N real texts to <bench #>" before firing, bench personas only (Ferris Bueller + fake-range numbers; never a real personal cell — see the no-personal-phone rule). Gate live-send on a human "go".
Still genuinely open (call these out, don't pretend they're settled)
- Exact event-bus substrate (a lightweight DDB/
EVAL_EVENT#append + the listener tailing it, vs. a real bus). Leaning DDB-append for v1 (simplest, durable, already our datastore) — confirm. - Whether "always-emit" domain events warrant their own ADR (they touch prod workflows fleet-wide, even if only consumed by eval today). Flag for review before wiring emission into prod paths.
Scope of the first ADR-aligned build
MVP (local-first) lands the UX + the seam now. The Temporal layer (this ADR's core) is the follow-up that swaps the seam — tracked as the next slice. leak-204 is the first scenario; the catalog generalizes after.