diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5fd407167..ef43d72f4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,6 +33,10 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: node scripts/test/simulator-packages.mjs
+ - name: Verify simulator infrastructure profiles
+ run: |
+ pnpm nx run @moltzap/simulator:local-profile-check
+ pnpm nx run @moltzap/simulator:gke-profile-check
- run: pnpm typecheck
- run: pnpm lint
# Exact runs fail when a required project target disappears; run-many
diff --git a/.gitignore b/.gitignore
index 28c0a1c51..5a60c9f17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -153,6 +153,9 @@ vite.config.ts.timestamp-*
!.codex/skills/
/.tmp/
+# Local scratch space; holds the Node compile cache
+/.scratch/
+
.nx/cache
.nx/workspace-data
.nx/polygraph
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0089bbe1b..eb0c7ba0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added: container societies on Kubernetes
+
+The simulator runs a society of container agents on Kubernetes through one
+`RunSpec`. A run reserves its whole cohort before any agent starts, so a society
+that cannot fit never half-starts, and every run writes a ledger it can be read
+back from. Two profiles share that path: a local cluster for development and a
+GKE profile for experiments at size.
+
+### Changed: one end-to-end experiment, sized by its run
+
+`packages/simulator/local/end-to-end.mjs` replaces the two-, four-, ten-, and
+hundred-agent modules. The path is the same at two agents and at a hundred, so
+the roster size is an input rather than four near-copies of one file.
+`MOLTZAP_COHORT_SIZE` carries it, defaulting to two, and travels the same
+validated path as the startup budget: the submitter refuses what could never be
+a count, the controller bounds it, and the experiment reads it through the
+controller's own configuration rather than the process.
+
+### Added: a GKE profile that scales agents on demand
+
+`packages/simulator/gke/cluster.sh` covers the profile's whole lifecycle with
+`setup`, `up`, `run`, `down`, and `delete`. Agent nodes autoscale from zero, so
+an idle profile costs only its resident controller, and a run provisions the
+nodes its cohort needs and gives them back afterwards. `run` builds the
+controller image, pushes it, and submits by the digest the registry reports,
+which removes the hand-copied reference that could name an image that does not
+exist. Destroying the profile refuses while its bucket still holds run ledgers.
+
+### Fixed: a large cohort survives from admission to teardown
+
+Runs of about a hundred agents failed partway through, and the failures read as
+infrastructure loss with no cause attached.
+
+- The run worker is now installed by waiting for the revision just installed
+ rather than any available replica. Every submission installs the image it
+ built, so every submission rolls the worker; counting the outgoing replica as
+ ready handed the run to a Pod the rollout then deleted.
+- The controller's liveness signal runs on its own schedule for the whole
+ attempt. Admitting one agent at a time means a large cohort takes longer to
+ prepare than the liveness deadline allows, and the signal cannot wait for the
+ observation loop to start.
+- Installing the worker onto a cluster that had already hosted one no longer
+ conflicts, so a profile can serve more than one run.
+- A cohort's startup budget is now configurable end to end. The controller read
+ the setting but nothing supplied it, leaving its two minute default as the
+ only reachable value.
+- A cluster failure reports which operation failed and why. The ledger recorded
+ errors that named neither.
+
### Added: daemon-backed `HarnessClient`
`@moltzap/client` exposes an Effect `HarnessClient` for runtime adapters. Its
diff --git a/README.md b/README.md
index a3a6856ba..565bb8771 100644
--- a/README.md
+++ b/README.md
@@ -158,41 +158,37 @@ you have two supported surfaces:
## Simulating agent societies
-`@moltzap/simulator` is the code-first simulator for agentic societies. A
-versioned `simulator.define` call closes over the complete typed event catalog.
-`Society.agents` declares a keyed roster that can mix OpenClaw, NanoClaw,
-in-process `effectRuntime` agents, and customer-defined `defineRuntime` agents
-on one router and one protocol.
-
-The experiment is an Effect program. It receives exact started-agent values
-through `roster.startedAgents`, emits customer events through `Society.Events`,
-and reads committed evidence through `Society.Ledger`. Each started value
-separates the participant's router-issued `.agent`, runtime-native `.gateway`,
-and `.termination` observation. OpenClaw keeps its gateway RPC, NanoClaw keeps
-its CLI socket, and `effectRuntime({ build })` exposes exactly the customer
-gateway returned beside its autonomous `behavior`.
-
-All autonomous social behavior still uses the production client, protocol,
-and router. `Network` creates experiment-controlled diagnostic, workload, and
-observer endpoints; it is not a replacement principal API for roster agents.
-When the outer Effect completes after the kernel acquires an active ledger,
-`Society.run` returns either `ProgramFinished` or `RunInfrastructureFailed`.
-`ProgramFinished` carries the program `Exit`; both outcomes carry the durable
-ledger receipt retained during finalization. Customer code decides when the
-experiment is done and how the ledger is graded or swept.
-
-The same `@moltzap/simulator` package supplies the filesystem ledger,
-production router, OpenClaw, NanoClaw, and `effectRuntime` implementations.
-Customer code defines other runtimes with `defineRuntime`. The production
-router requires Docker and caches an image built from the exact server and
-protocol packages installed with the simulator. Start with the
-[simulator guide](docs/simulator/overview.mdx).
-
-The one package has four supported entry points. Experiment definitions and
-runs use `@moltzap/simulator`; autonomous runtime contracts and shipped
-implementations use `@moltzap/simulator/runtime`; router and link
-implementations use `@moltzap/simulator/network`; storage implementations and
-offline analysis tools use `@moltzap/simulator/ledger`.
+`@moltzap/simulator` is the code-first simulator for agentic societies. An
+experiment exports one immutable `RunSpec` containing a versioned definition
+id, closed event catalogs, an exact keyed container-runtime roster, the
+local-Kubernetes or GKE infrastructure Layer, and one customer `execute`
+Effect. The in-cluster controller invokes `Run.execute(runSpec)` once.
+
+Each started roster value separates its router-issued `.agent`, exact
+runtime-native `.gateway`, and `.termination` observation. OpenClaw and
+NanoClaw keep their own gateway types and fixed controller bridges. Evaluation
+code peers run their policies in their own application containers; every
+agent's social traffic still uses the production MoltZap client and router.
+
+The customer Effect receives `{ agents, events, network, ledger }`. It owns
+completion policy, scenarios, sweeps, and grading. `ProgramFinished` retains
+the program `Exit` and completed-ledger receipt; infrastructure failures retain
+their durable receipt when allocation succeeded. Completed artifacts can be
+reopened through the typed ledger facade without exposing Kubernetes objects
+to experiment code.
+
+Kubernetes, Kueue, Agent Sandbox, and Temporal form the only simulator
+execution path. The repository supplies a kind profile for local work and a
+GKE Standard profile for cloud qualification. Docker may build images and run
+the local kind nodes, but it is not a simulator backend. Start with the
+[simulator guide](docs/simulator/overview.mdx) and the
+[local profile](packages/simulator/local/README.md).
+
+The package has four supported entry points: experiment definitions and runs
+at `@moltzap/simulator`, container runtimes at
+`@moltzap/simulator/agents`, network contracts at
+`@moltzap/simulator/network`, and offline evidence tools at
+`@moltzap/simulator/ledger`.
## Packages
diff --git a/docs/decision-evidence/20260801-main-kubernetes-society-execution-cold-review.md b/docs/decision-evidence/20260801-main-kubernetes-society-execution-cold-review.md
new file mode 100644
index 000000000..b0676d31e
--- /dev/null
+++ b/docs/decision-evidence/20260801-main-kubernetes-society-execution-cold-review.md
@@ -0,0 +1,256 @@
+# Blind teammate review — main Kubernetes society execution
+
+## Candidate identity
+
+- Repository: `/home/tapanc/moltzap-pr-917-main`
+- Branch: `impl/917-main-local-society`
+- Candidate commit: `0f152696e588538ffcbfac0162bcd1cf17bbaab3`
+- Candidate tree: `0374e97d203e02a8a2b7d001c51f2f5f8958f047`
+- Merge base with `origin/main`: `314ece9e115e18d443363d42577d567f087d644d`
+- Subject: `WIP: define main Kubernetes society execution`
+- Worktree: clean
+- UTC start: `2026-08-01T22:01:54Z`
+- UTC end: `2026-08-01T22:10:44Z`
+- Duration: 8 minutes 50 seconds
+
+## Reviewer and isolation attestation
+
+Reviewer: fresh Codex teammate `/root/candidate_blind_review_1`.
+
+I received only the candidate repository root, isolation rules, and the fixed questions. I had no inherited author conversation, compaction, memory, private state, or earlier blind-review output. I used only the candidate repository, normal navigation, history, search, and repository-local checks. I did not browse external sources.
+
+A path listing exposed the names of existing quarantined review artifacts. I did not open, read, or content-search any `*-cold-review.md` or invalid-review record. No command returned an answer or verdict from one. The review remained in one uninterrupted fresh-agent context.
+
+Author interventions: none. I asked for no hints and received none.
+
+## Exact prompt questions
+
+1. What decision does this candidate make current, what problem does it resolve, and which statements are binding versus context or non-normative explanation?
+2. What earlier outcomes does it replace, retain, or leave untouched, and where does the current normative contract live?
+3. What must an implementer now do or avoid, which layers or consumers are affected, and under what fault, trust, safety, liveness, and compatibility assumptions?
+4. Which humans are named as decision-makers, which source events does the compacted trajectory cite for their calls, alternatives, reversals, and deferrals, and what source gaps does it explicitly record? Report only what the event ledger states; do not infer motives, confidence, urgency, or rationale.
+5. Find the strongest apparent contradiction, stale instruction, or broken lineage elsewhere in the repository. Resolve it using the authority order or report it as a blocker.
+6. Could a teammate implement the decision without chat or guessing? List every missing link or unresolved choice and classify each as a deliberate deferral or an accidental gap.
+
+## Unedited answers
+
+### 1. Current decision, problem, and authority
+
+The candidate makes `docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md` current with status `accepted`.
+
+It governs main-track v1 society execution in `packages/simulator` and the `packages/evals` consumer. `@moltzap/simulator` remains the single owner of real society execution, but Kubernetes becomes its only real distributed backend. Local kind and GKE use the same schema-bound `RunSpec`, container roster, `Run.execute` path, private kernel, Kueue aggregate admission, Agent Sandbox lifecycle, Temporal orchestration, durable artifacts, and evidence model. Evaluations use that path rather than a separate or host-based executor.
+
+The decision resolves the existing host-process/in-process engine’s lack of reconstructible distributed execution, all-roster admission, durable start-or-attach identity, generation-aware container lifecycle, and fail-closed platform qualification. It also prevents a Kubernetes example or second backend from leaving two execution semantics and allowing evaluations to continue testing the old host engine.
+
+The record explicitly makes the following binding:
+
+- its `Decision Outcome`;
+- the public `RunSpec`, `Agent`, `Infrastructure`, and `Run` contract;
+- lifecycle, generation, admission, dispatch, cleanup, and evidence invariants;
+- security, trust, safety, liveness, and compatibility assumptions;
+- normative ownership;
+- deliberate deferrals; and
+- the main/v1 scope declared in `Scope and authority`.
+
+The context and problem statement explain why the decision exists. The consequences explain its effects. Issue #936 is explicitly non-normative. The source-event trajectory is evidence, not authority. Historical ADR bodies and transition notices describe lineage or current pre-cutover implementation and do not extend the new contract.
+
+The v2 simulator/testbed split, v2 `Simulator.define` port, Gate 1 manifest, `v2/*`, and draft issue #917 decisions are explicitly outside scope.
+
+### 2. Replaced, retained, and untouched outcomes
+
+The new ADR is the primary replacement for three partially superseded main/v1 records.
+
+From `20260727-code-first-simulator-kernel.md`, it retains TypeScript/Effect authoring, the closed `EventCatalog`, typed `RunLedger`, producer-bound evidence, customer-owned scenarios/sweeps/completion/grading, one simulator package, and the production v1 router/protocol. It replaces `Simulator.define`, definition-bound `.run`, `simulatorLayer`, host/mixed runtime acquisition, Docker/process/filesystem execution composition, in-process production runtimes, and the prior restart/replacement deferral.
+
+From `20260729-principal-io-uses-runtime-gateways.md`, it retains runtime-native principal control versus MoltZap social traffic, exact typed gateways, no universal gateway union or correlation ID, no synthetic-principal shortcut, the gateway/social evidence distinction, `replyToId` removal, and the evaluation identities and behavioral intent. It replaces `AgentRuntime.acquire`, `RunningAgent`, `StartedAgent`, host readiness and lifetime, in-process production peers, and the blanket restart/rebinding deferral with stable slots and generation-aware gateways.
+
+From `20260729-effect-native-evaluation-results.md`, it retains the sixteen-by-two evaluation catalog, typed reports, deterministic and semantic grading, sanitized provenance, SQLite authority, Phoenix materialization, and old-report reading. It replaces host runtime snapshots and outcomes, runtime factories/in-process peers, and rerunning missing cells with schema-bound container inputs and `attemptId === executionId` start-or-attach.
+
+The changed frontmatter, visible `Supersession` sections, and decision index agree on these statuses and replacements.
+
+The accepted v2 simulator-system-driver decision, Gate 1 manifest, v2 package split, and v2 specifications remain untouched. Existing ledger format version 1, admitted legacy schemas, existing event tags, legacy event classes, old reports, and UUID ledger references remain only where the new compatibility section says they remain readable.
+
+The current normative main/v1 contract lives in the new ADR until its named implementation owners encode it. `packages/simulator/AGENTS.md` already owns the package boundary and dependency law. The ADR assigns the remaining contracts to `src/definition.ts`, the runtime facade, `src/execution.ts`, events/ledger, kernel, private platform/orchestration/controller/artifact modules, deployment/CLI/Nx assets, and `packages/evals`.
+
+### 3. Implementation obligations and assumptions
+
+An implementer must:
+
+- expose the frozen root namespaces `RunSpec`, `Agent`, `Infrastructure`, and `Run`, without a new export subpath;
+- accept schema-bound, finite-JSON input/result/failure values and deterministic, exact, nonempty container rosters;
+- support only digest-pinned container descriptors with typed bridges, fixed resource fields, constrained persistence, and exact logical Secret slots;
+- keep Kubernetes as the sole real backend, with only local and GKE infrastructure selections, and keep platform/orchestration APIs private;
+- create one stable AgentId and direct single-application-container Sandbox per roster slot;
+- model Pod UID plus application-container restart count as generations and never replay active calls, turns, subscriptions, streams, or cursors;
+- compare-create a durable execution binding, attach exact retries, preserve terminal outcomes and receipts, and reject conflicting execution identities before creating further resources;
+- admit the complete homogeneous roster through one manual aggregate Kueue Workload before creating Sandboxes;
+- recheck the exact ready generation set and durably fence at most one customer-program invocation;
+- use one non-replacing controller and one Temporal Workflow, with controller loss terminal;
+- let only the controller append simulator lifecycle events and seal the ledger;
+- let the Temporal finalizer clean and verify resources, publish completion, and store terminal artifacts without inventing or rewriting events;
+- implement the exact closed outcome, pre-ledger error, receipt, and Kubernetes event contracts;
+- fail closed on schema drift, mutation, incomplete observation, residue, or inability to prove qualification;
+- cut evaluations over to the same container path, preserving grading/SQLite/Phoenix ownership and attaching resume to the same durable execution; and
+- remove executable old runners and aliases without a compatibility executor.
+
+Affected surfaces are the v1 simulator’s definition, runtime bridge, network, ledger, kernel, Kubernetes/Sandbox/Kueue platform, Temporal orchestration, controller, artifact, CLI/deployment, and evaluation-consumer boundaries. The decision does not amend the v2 layers or packages.
+
+Trusted components for the claimed safety properties are submitted ESM, the cluster administrator, simulator controller/worker/finalizer, Kubernetes control plane/API, Kueue and Agent Sandbox controllers, Temporal and its persistence, artifact/binding/ledger storage, registry digest resolution, DNS/policy enforcement, and the v1 router/server. Application containers and their output may be faulty or malicious.
+
+The container boundary depends on a qualified runtime such as gVisor, policy, and the trusted control plane. Local kind assumes a trusted rootful Linux/amd64 host and cannot claim hostile-code or managed-isolation parity before its gates pass. Only a passing managed GKE suite may claim managed isolation qualification.
+
+Safety depends on durable binding, dispatch fencing, storage, and controller/finalizer behavior. It is at-most-once program dispatch, not exactly-once customer side effects. Controller loss, partitions, or deletion cannot authorize replay or weaker admission.
+
+Liveness additionally requires Temporal, storage, registry, DNS, router, Kubernetes/controllers, quota, physical capacity, all current agent generations, bridges, and any provider proxy to remain available. Their loss may stop progress.
+
+Compatibility keeps ledger format 1 and existing tags, adds a separate exact Kubernetes catalog, keeps legacy readers read-only, requires a new evaluation definition version, and intentionally makes the execution cut source-breaking.
+
+### 4. Decision-makers, source events, and source gaps
+
+The ADR names one human decision-maker: Tapan Chugh.
+
+The trajectory identifies Codex session `019fbbdd-7cff-7753-8541-4f66f0248d43` and cites these stored events:
+
+- User message `msg_019fbbe1-770d-7d11-8475-0f2f7b3bd7b1`, turn `0a25724d-258f-41b3-a256-f8c95db5bd3a`, at `2026-08-01T05:52:23.309Z`: target main first with the original simulator.
+- User message `msg_019fbdeb-1743-7470-be76-7ed53d7f2420`, turn `019fbdeb-1371-7be3-8e61-babd80ff5ffc`, at `2026-08-01T15:22:08.579Z`: make it part of the core simulator rather than one example and ask for the next slice.
+- User message `msg_019fbded-2372-72b0-b859-61f6fe80ac47`, turn `019fbded-227b-70a3-9d9e-9a52a461b990`, at `2026-08-01T15:24:22.771Z`: plan the final shape first.
+- Assistant proposal `msg_0141f487830063b4016a6e17e648d481939b073eea4e50a234`, turn `019fbdeb-1371-7be3-8e61-babd80ff5ffc`, at `2026-08-01T15:59:39.573Z`: one `RunSpec` with a customer `execute` callback.
+- Directly following user message `msg_019fbe0e-7474-7e53-9f4e-40faac7ac654`, turn `019fbe0e-71e3-76e0-9b67-78ce9cab69e0`, at `2026-08-01T16:00:46.197Z`: “okay do this.”
+- User message `msg_019fbe84-b81b-7312-ad62-03432f57cdf2`, turn `019fbe84-b775-7542-94b0-788b9b0a79d7`, at `2026-08-01T18:09:56.763Z`: pull the GKE sandbox work into the core.
+- User message `msg_019fbe88-7cd4-7c62-9b8c-e9060c44f8d8`, turn `019fbe88-7c3a-7c10-a1af-ec026b6309e2`, at `2026-08-01T18:14:03.732Z`: use Kubernetes, Kueue, Temporal, and the complete setup, targeting local Kubernetes or GKE.
+- User message `msg_019fbe9a-2e94-7430-8da7-f71f0e533f15`, turn `019fbe9a-2ddc-7cd1-b15b-c1447e2310aa`, at `2026-08-01T18:33:23.349Z`: land on main.
+- User message `msg_019fbe9c-4f9a-7970-adb5-15463aea8686`, turn `019fbe9c-4ede-7d12-ae11-e054cf83a684`, at `2026-08-01T18:35:42.874Z`: target `packages/simulator`, not v2.
+- User directive `msg_019fbf11-b878-7e83-902a-db4e3868e856`, turn `46fbcdbe-0654-4ba4-8e69-d2de6baaa959`, at `2026-08-01T20:43:57.432Z`: work on issue #936, keep agent-maintained issue notes, and run evaluations end to end through the new path.
+- Separate mechanical events record the main merge, baseline checks, and agent-published issue comments.
+
+The events show the alternatives “core versus example” and “main/packages/simulator versus v2.” The trajectory records no explicit human reversal. The movement of infrastructure selection from the accepted assistant example’s `RunSpec` into `Run.execute` is explicitly identified as a later agent-proposed refinement, not a retained human choice.
+
+The trajectory explicitly records these source gaps:
+
+- Codex supplied no parent locators.
+- “okay do this” has meaning only relative to the directly preceding assistant proposal.
+- No separate user event chooses the final infrastructure-field placement.
+- The retained human messages do not separately decide every resource shape, failure variant, security control, event field, or platform mechanism.
+- Exact versions, schemas, providers, timeouts, storage mechanisms, scale limits, and cost budgets are not human decisions in the excerpts.
+- The issue plan and checkpoint prose are agent-authored mechanical artifacts.
+- Private instructions, hidden reasoning, irrelevant output, private URLs, and credentials are omitted.
+
+The repository does not retain an event in which Tapan Chugh reviews or accepts the comprehensive 571-line final outcome after these agent refinements. The trajectory identifies stored actors only as `user`; it does not establish that the session account is the named decision-maker. Under the repository’s provenance law, the frontmatter and Git identity do not themselves prove human acceptance of the detailed binding choices.
+
+### 5. Strongest apparent contradiction or stale instruction
+
+The strongest repository-local stale instruction is the newly added `examples/simulator/README.md` and root `simulator:example` command. They present a host-Node/Docker three-container runner using `simulator.define`, `simulatorLayer`, and `openClawRuntime`, while the accepted ADR says Kubernetes is the only real backend and prohibits a Docker executor, host executor, or compatibility runner. Generated `docs/modules/simulator/src.mdx` and `packages/simulator/src/MODULE.md` also still expose `simulatorLayer` without a transition banner.
+
+The authority chain resolves the semantic conflict:
+
+1. The accepted new ADR explicitly owns the current main/v1 execution decision.
+2. `packages/simulator/AGENTS.md` repeats the one-Kubernetes-path law and forbids preserving the old executable aliases.
+3. Root and package simulator guides label the old APIs as pre-cutover implementation rather than extension points.
+4. The example calls itself the “original simulator” and a precursor.
+
+Therefore these files describe current implementation state, not the target contract. They must not guide new implementation. The example and generated API pages should receive an explicit transition pointer or be removed at cutover, but they do not override the accepted ADR.
+
+The accepted v2 simulator ADR’s preservation of `Simulator.define` is another apparent conflict, but it is fully resolved by the repository’s two-track authority: that record governs v2, while this candidate explicitly governs main/v1 and leaves v2 untouched.
+
+### 6. Implementability and unresolved choices
+
+No. A teammate can understand the intended architecture, but cannot implement every binding guarantee without making unrecorded public or persistence choices.
+
+Accidental gaps and blockers:
+
+1. **Execution authority contradicts its binding key.** The immutable binding is keyed by `(infrastructure authority, definition id, executionId)`, and cluster recreation intentionally creates a different authority. The ADR nevertheless requires a changed authority to return `RunExecutionConflict` and create no new binding or resource. Changing authority changes the lookup key, so the stated compare-create cannot discover the old binding without an additional cross-authority uniqueness index or a differently scoped key. Neither is specified. The decision must choose whether execution identity is authority-scoped or globally conflicts across authorities.
+
+2. **The comprehensive accepted outcome lacks final human-accountable source approval.** The retained user accepts a much smaller one-`RunSpec` proposal and later directs the platform scope. The trajectory itself says the final infrastructure placement and detailed lifecycle, persistence, event, security, and error mechanics are agent refinements without separate human events. No retained event admits or approves the final outcome, and no explicit delegation gives the agent decision authority. This leaves binding choices attributed only through frontmatter, which repository law says is insufficient proof.
+
+3. **The declared public interface is incomplete.** The ADR calls the names, fields, and semantics binding but does not provide complete Effect signatures and closed shapes for `Run.execute`, `Run.open`, slot/generation stream values, bridge unavailability types, receipts, or all namespace inventories. The future owner files do not yet encode the replacement contract. An implementer must make public type decisions.
+
+4. **The public CLI contract is missing.** The ADR says the package owns the public CLI and specifies signal exits 130/143, but gives no command names, arguments, input/output JSON schemas, ordinary exit mapping, attachment/query behavior, or error rendering. Issue #936 is explicitly non-normative, so it cannot fill this contract.
+
+5. **Durable cross-process identifier encoding is incomplete.** The Workflow ID is a domain-separated SHA-256 over three values, but the domain separator and byte encoding of the tuple are not frozen. Stable AgentId allocation and the exact resolved-roster projection shared by submitter and controller are also not assigned a complete persisted encoding. These choices affect durable attachment and compatibility rather than only private code structure.
+
+6. **Pre-cutover documentation remains inconsistently marked.** The active Docker example and generated simulator API pages lack the new transition pointer present in the primary guides. Authority resolves the target, but a cold implementer can still encounter an apparently supported forbidden runner.
+
+Deliberate deferrals, clearly identified by the ADR:
+
+- exact upstream versions, digests, checksums, served Sandbox schemas, and aggregate Kueue projection;
+- one-container OpenClaw and NanoClaw bootstrap and bridge wire envelope;
+- local runtime/CNI behavior and regional GKE add-on behavior;
+- durable Temporal deployment, artifact-authority schemes, timeouts, and profile limits;
+- production Temporal hosting/HA, fairness, borrowing, preemption, physical gang scheduling, multicluster dispatch, hostile submitted-module isolation, automatic execution-ID reuse, non-Linux/rootless local support, at-rest certification, and exactly-once external effects;
+- persistent-agent storage and artifact design above the 100-agent gate; and
+- 1,000/5,000/10,000-agent feasibility and latency/resource/throttling/cost budgets.
+
+Those deliberate deferrals block a profile when its spike fails and do not authorize a fallback executor or weaker lifecycle. They are not the reason for the review failure; the accidental contract, provenance, and identity gaps are.
+
+## Independently discovered paths and headings
+
+- `AGENTS.md`
+ - `Project`
+ - `Architecture decision records`
+ - `Decision provenance`
+ - `Lifecycle and landing`
+ - `Blind teammate review gate`
+- `docs/decisions/README.md`
+ - `Canonical reading guidance`
+ - `Records`
+- `docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md`
+ - `Scope and authority`
+ - `Decision Outcome`
+ - `Start-or-attach identity and durable artifacts`
+ - `Security, trust, safety, and liveness assumptions`
+ - `Compatibility and evaluation cutover`
+ - `Normative owners`
+ - `Deliberate deferrals`
+ - `Earlier outcomes replaced and retained`
+- `docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md`
+ - `The main simulator runs container societies on Kubernetes`
+ - `Source gaps, stated plainly`
+- The `Supersession` sections of the three changed earlier ADRs
+- `packages/simulator/AGENTS.md`
+ - `Boundary`
+ - `Laws`
+ - `Structure`
+- `v2/VISION.md`
+ - `Authority`
+ - `Packages and versions`
+- `docs/decisions/20260728-simulator-is-the-system-driver.md`
+- `docs/decisions/20260729-v2-authority-lives-with-v2.md`
+- Transition notices in root, simulator, evaluation, and development guides
+- `examples/simulator/README.md`
+- `docs/modules/simulator/src.mdx`
+- `packages/simulator/src/MODULE.md`
+
+## Discovery trail
+
+1. Identified the clean candidate commit, tree, merge base, history, and changed paths.
+2. Read repository ADR law and the decision index.
+3. Read the complete new ADR and its complete source-event trajectory.
+4. Compared all three superseded ADRs and the index against the new lineage.
+5. Read package law, transition documentation, current v2 authority, and the v2 simulator decision.
+6. Searched non-quarantined repository content for old and new simulator public APIs.
+7. Inspected the active Docker example and generated simulator API documentation.
+8. Checked the binding-key and authority language across all non-quarantined sources.
+9. Ran `pnpm docs:check`; Mint reported `success no broken links found`.
+10. Reconfirmed the worktree remained clean and the candidate identity unchanged.
+
+## Per-question verdicts
+
+1. **PASS** — The current decision, problem, scope, and binding/non-binding distinction are explicit and discoverable.
+2. **PASS** — Supersession, retained scope, v2 exclusion, index status, and normative ownership are consistent and discoverable.
+3. **PASS** — Implementation duties and fault/trust/safety/liveness/compatibility assumptions are unusually detailed and discoverable.
+4. **FAIL** — The trajectory is source-faithful, but it does not contain final human approval of the comprehensive accepted outcome or establish that the stored `user` is the named decision-maker. It explicitly identifies major agent-proposed refinements.
+5. **PASS** — The strongest stale main example/generated-doc conflict and the v2 API conflict can be resolved through the accepted ADR, package law, transition notices, and two-track scope.
+6. **FAIL** — The authority/key contradiction and incomplete public API/CLI/durable identity contracts require guessing; the provenance gap also prevents treating the detailed choices as admitted human decisions.
+
+## Blockers
+
+- Resolve the execution-binding authority/key contradiction.
+- Obtain and retain human review or acceptance of the complete candidate outcome, or narrow the accepted outcome to the choices actually supported by retained events.
+- Freeze the missing public API, CLI, and durable identity encodings, or explicitly classify and bound them as non-public implementation choices or deliberate deferrals.
+- Re-run the blind gate with a different fresh reviewer after any semantic correction.
+
+## Overall result
+
+**FAIL — blocks landing.**
+
+Mechanical links pass and the decision’s broad architecture, scope, lineage, and assumptions are discoverable. The source-attribution failure and unresolved binding/authority contract prevent an overall PASS.
diff --git a/docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md b/docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md
new file mode 100644
index 000000000..67d7e73d6
--- /dev/null
+++ b/docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md
@@ -0,0 +1,485 @@
+# Main Kubernetes society execution source-event ledger
+
+This is a curated, non-normative ledger of stored public events from Codex
+session `019fbbdd-7cff-7753-8541-4f66f0248d43`. Every retained Codex entry is
+a top-level `response_item` whose payload type is `message`; the source gives
+an enclosing turn and message id but no parent locator. Timestamps are UTC.
+Excerpts are literal, including spelling, punctuation, questions, and terse
+replies. The linked ADR is normative; this trajectory does not reconstruct a
+rationale or strengthen a proposal into a human statement.
+
+
+
+## The main simulator runs container societies on Kubernetes
+
+[ADR: `20260801-main-simulator-runs-container-societies-on-kubernetes.md`](../decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md)
+
+1. **Stored user messages: main first, then core rather than an example.**
+
+ Source system: Codex. Source session: the session above. Native locator:
+ message `msg_019fbbe1-770d-7d11-8475-0f2f7b3bd7b1`; enclosing turn
+ `0a25724d-258f-41b3-a256-f8c95db5bd3a`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T05:52:23.309Z`. The source supplies
+ no parent locator.
+
+ > we should target this on main first with the original simulator
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbdeb-1743-7470-be76-7ed53d7f2420`; enclosing turn
+ `019fbdeb-1371-7be3-8e61-babd80ff5ffc`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T15:22:08.579Z`. The source supplies
+ no parent locator.
+
+ > this should be part of the core simulator. why are we making this one example. what is the next slice
+
+2. **Stored user message: decide the final shape before implementation.**
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbded-2372-72b0-b859-61f6fe80ac47`; enclosing turn
+ `019fbded-227b-70a3-9d9e-9a52a461b990`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T15:24:22.771Z`. The source supplies
+ no parent locator.
+
+ > lets plan the final shape first
+
+3. **Stored assistant proposal and directly following user acceptance: one
+ RunSpec and customer execute callback.**
+
+ Source system and session: the same. Native locator: message
+ `msg_0141f487830063b4016a6e17e648d481939b073eea4e50a234`; enclosing turn
+ `019fbdeb-1371-7be3-8e61-babd80ff5ffc`; top-level response-item message;
+ stored actor role `assistant`; phase `final_answer`; parent locator absent;
+ `2026-08-01T15:59:39.573Z`.
+
+ > The “program” is the host-side conductor for a run. It:
+ >
+ > - Starts only after the router and every agent container are ready.
+ > - Creates the conversation/task or sends the initial stimulus.
+ > - Interacts with agents through their gateways.
+ > - Waits for messages and evidence.
+ > - Decides when the run is finished and returns the result.
+ >
+ > It is not agent code, container startup code, or deterministic peer behavior. Those belong inside the agent containers.
+ >
+ > For evals, it is essentially the case runner. “Program” is probably unnecessary vocabulary; the single `RunSpec` can simply have an `execute` callback:
+ >
+ > ```ts
+ > export default RunSpec.define({
+ > id: "echo/v1",
+ > events: [echoEvents],
+ > agents: { alice, bob },
+ > infrastructure,
+ >
+ > execute: ({ agents, events, network }) =>
+ > Effect.gen(function* () {
+ > // Stimulate the society, observe it, and determine completion.
+ > }),
+ > })
+ > ```
+ >
+ > So the final public model can be just `RunSpec` and `Run.execute(spec)`.
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbe0e-7474-7e53-9f4e-40faac7ac654`; enclosing turn
+ `019fbe0e-71e3-76e0-9b67-78ce9cab69e0`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T16:00:46.197Z`. The source supplies
+ no parent locator.
+
+ > okay do this
+
+4. **Stored user messages: pull the sandbox platform into the core and use the
+ complete Kubernetes stack.**
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbe84-b81b-7312-ad62-03432f57cdf2`; enclosing turn
+ `019fbe84-b775-7542-94b0-788b9b0a79d7`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T18:09:56.763Z`. The source supplies
+ no parent locator.
+
+ > I think the GKE sandbox stuff should be pulled here bro
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbe88-7cd4-7c62-9b8c-e9060c44f8d8`; enclosing turn
+ `019fbe88-7c3a-7c10-a1af-ec026b6309e2`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T18:14:03.732Z`. The source supplies
+ no parent locator.
+
+ > I think we should do a k8s + kueue + temportal + everything setup. the target can be a local k8s cluster or GKE cluster.Go through the actual ADRs and lets work on everything together
+
+5. **Stored user messages: main and `packages/simulator`, not v2.**
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbe9a-2e94-7430-8da7-f71f0e533f15`; enclosing turn
+ `019fbe9a-2ddc-7cd1-b15b-c1447e2310aa`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T18:33:23.349Z`. The source supplies
+ no parent locator.
+
+ > this will go to main
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbe9c-4f9a-7970-adb5-15463aea8686`; enclosing turn
+ `019fbe9c-4ede-7d12-ae11-e054cf83a684`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T18:35:42.874Z`. The source supplies
+ no parent locator.
+
+ > the implementatiion will target packages/simulator, not v2
+
+6. **Stored assistant issue-plan summary and directly following user start
+ directive.**
+
+ Source system and session: the same. Native locator: message
+ `msg_0141f487830063b4016a6e40cd78048193bca36ecb2c05a8a2`; enclosing turn
+ `019fbe82-b219-7da0-b12e-428543cb9e76`; top-level response-item message;
+ stored actor role `assistant`; phase `final_answer`; parent locator absent;
+ `2026-08-01T20:45:27.703Z`.
+
+ > Done — [issue #936](https://github.com/chughtapan/moltzap/issues/936) now defines the final shape:
+ >
+ > - Targets `main` and `packages/simulator`; no implementation under `v2/*`.
+ > - One immutable `RunSpec` containing the roster and customer `execute` Effect.
+ > - One Kubernetes backend with local kind and regional GKE profiles.
+ > - Aggregate Kueue admission, Agent Sandbox participants, and one Temporal workflow per run.
+ > - Exact-generation readiness barrier and exactly-once customer-program invocation.
+ > - Ten implementation slices, explicit failure semantics, test ownership, security gates, GKE parity, and scale qualification.
+ > - Engineering review: clean, 28 findings incorporated, zero unresolved decisions or critical gaps.
+ >
+ > Current worktree:
+ >
+ > - Path: `/home/tapanc/moltzap-pr-917-main`
+ > - Branch: `impl/917-main-local-society`
+ > - HEAD: `515aa590`
+ > - Status: clean
+ > - Relative to `origin/main`: 12 behind, 1 ahead
+ > - The ahead commit is still the original example implementation; I did not modify or rebase it.
+ >
+ > The next slice is Slice 0: admit the main-track replacement ADR and lock the `RunSpec`, `Run.execute`, and declarative Kubernetes profile contracts. Slice 1 is then the upstream compatibility lab before implementation commits begin.
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbf10-e051-75d0-92d7-bfb32174edfb`; enclosing turn
+ `019fbf10-debf-78b3-b71f-5bb6e72b88fe`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T20:45:27.704Z`. The source supplies
+ no parent locator.
+
+ > start
+
+7. **Stored user work directive: issue #936, durable issue notes, and an
+ end-to-end evaluation run.**
+
+ Source system and session: the same. Native locator: message
+ `msg_019fbf11-b878-7e83-902a-db4e3868e856`; enclosing turn
+ `46fbcdbe-0654-4ba4-8e69-d2de6baaa959`; top-level response-item message;
+ stored actor role `user`; `2026-08-01T20:43:57.432Z`. The outer goal wrapper
+ is omitted; the objective is literal. The source supplies no parent
+ locator.
+
+ > you are now working on https://github.com/chughtapan/moltzap/issues/936 in /home/tapanc/moltzap-pr-917-main. keep your durable notes updated on the issue as comments. run the implementation end-to-end running the evals through this new path
+
+8. **Earlier stored selections: one run, one container per agent, a strict
+ gate, and ten agents before scale.** These events were previously compacted
+ in `docs/decision-evidence/20260729-distributed-society-execution-trajectory.md`
+ on candidate commit `a2b55f32e8b8cc688c8a290972267492a3dbfc0b`. They
+ are repeated here because that candidate belongs to the v2 branch while the
+ current decision belongs to main. The literal option text and results are
+ unchanged.
+
+ Source system: Codex. Source session
+ `019fab08-15ca-7a10-a9af-f2a8441a45f5`; enclosing turn
+ `019fab0d-a1e8-7432-b3f6-a767cff72c52`; function call
+ `call_vlz2QouoKyvTCXhmbDB9Hiny` at `2026-07-28T23:39:27.783Z` and
+ function-call output at `2026-07-28T23:39:43.060Z`; stored actor role is
+ absent on the call and output.
+
+ > Single-run cluster (Recommended): Provision one society for one Society.run, require every agent to be alive at a cohort gate, dispatch the program once, then tear down; stage validation from 100 to 1,000 to 10,000 without router HA or a queue/operator.
+
+ > `{"answers":{"cluster_scope":{"answers":["Single-run cluster (Recommended)"]}}}`
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_PU6nJTGPlpeJ3PATixSc2ef8` at `2026-07-28T23:42:00.531Z` and
+ function-call output at `2026-07-28T23:43:52.229Z`; stored actor role is
+ absent.
+
+ > 2A Strict gate (Recommended): Human ~3–5d / agent ~1–2h; medium implementation risk, low maintenance. Pros: one bulk router-visible snapshot proves the whole cohort is online; any pre-gate exit aborts with typed evidence and scoped cleanup. Con: adds a cohort-ready phase and batching contract.
+
+ > `{"answers":{"plan_eng_review_cohort_gate":{"answers":["2A Strict gate (Recommended)"]}}}`
+
+ Source system and session: the same. The same enclosing turn; direct user
+ message at `2026-07-29T00:03:38.313Z`; stored actor role `user`. The source
+ supplies no separate message id or parent locator.
+
+ > I don't want to support that cheating. I want one container per agent
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_J4GjN5U25rt7aNh4Jo8eY8L9` at `2026-07-28T23:45:04.450Z` and
+ function-call output at `2026-07-28T23:46:37.089Z`; stored actor role is
+ absent. The prompt offered tiered, live-model, and infrastructure-only 10k
+ gates. No offered option was selected.
+
+ > `{"answers":{"plan_eng_review_10k_acceptance":{"answers":["None of the above","user_note: defer 4A and 4B scale. lets get to 10 agents first and then scale"]}}}`
+
+9. **Earlier stored selections: Kubernetes, Kueue, Temporal, GKE, and the
+ experiment-facing surfaces.** These events have the same source session and
+ prior checked-in trajectory as item 8.
+
+ Direct user messages in turn
+ `019fab37-ced4-7b41-8e9c-37c3822a7342`, stored actor role `user`, with no
+ separate message id or parent locator, were recorded at
+ `2026-07-29T00:14:21.664Z` and `2026-07-29T00:16:16.056Z`:
+
+ > or general kubernetes; we start with basic OpenClaw image and we can deliver instructions to connect to moltzap over the principal channel (which should work directly with the base image); increases the latency per experiment but that's the gold standard path
+
+ > if it can't thats a bug: having this image can be an optimization but not a requirement; and honestly if we use GKE or barebones K8s we can actually point them to a private registry?
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_SnFa3x3617eQul6H1zPNZeCm` at `2026-07-29T00:19:00.774Z` and
+ function-call output at `2026-07-29T00:19:34.352Z`; stored actor role is
+ absent.
+
+ > Temporal + Kueue (Recommended): Temporal owns durable run/sweep lifecycles, Kueue admits cluster capacity, and no Redis queue is added.
+
+ > `{"answers":{"run_queue_model":{"answers":["Temporal + Kueue (Recommended)"]}}}`
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_mbMK8n64ZfjzAGVA69nzjhIw` at `2026-07-29T00:20:26.953Z` and
+ function-call output at `2026-07-29T00:21:21.293Z`; stored actor role is
+ absent.
+
+ > Local first, defer prod: Use the local Temporal dev server for the first milestones and leave production hosting deliberately unselected.
+
+ > `{"answers":{"temporal_hosting":{"answers":["Local first, defer prod"]}}}`
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_8Tj66rC9ATIk5wZqXIiFtRia` at `2026-07-29T00:28:04.980Z` and
+ function-call output at `2026-07-29T00:28:15.312Z`; stored actor role is
+ absent.
+
+ > Standard regional (Recommended): Use a pre-sized dedicated agent node pool for predictable 1k–10k cohort admission and tuning.
+
+ > `{"answers":{"gke_profile":{"answers":["Standard regional (Recommended)"]}}}`
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_0HQBCkj6yDpE4i7yXzEsTp8g` at `2026-07-29T00:30:27.347Z` and
+ function-call output at `2026-07-29T00:31:56.397Z`; stored actor role is
+ absent.
+
+ > In-cluster controller (Recommended): A stable controller image fetches the content-addressed experiment bundle, owns the router/barrier, and runs close to all agents.
+
+ > `{"answers":{"program_location":{"answers":["In-cluster controller (Recommended)"]}}}`
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_0OO9tWVFfZHYPNu61PoPXcqN` at `2026-07-29T00:37:27.742Z` and
+ function-call output at `2026-07-29T00:38:23.511Z`; stored actor role is
+ absent.
+
+ > CLI + library (Recommended): Provide a `moltzap simulator run ` command backed by a reusable TypeScript submission API.
+
+ > `{"answers":{"submission_surface":{"answers":["CLI + library (Recommended)"]}}}`
+
+ Source system and session: the same. The same enclosing turn; function call
+ `call_z5VtaeUzaAe4BaD0DJh3UnVU` at `2026-07-29T00:41:44.786Z` and
+ function-call output at `2026-07-29T00:43:35.416Z`; stored actor role is
+ absent.
+
+ > Terraform + Helm (Recommended): Make cluster, IAM, registry, storage, node pools, and pinned Kueue installation reproducible.
+
+ > `{"answers":{"gcp_iac":{"answers":["Terraform + Helm (Recommended)"]}}}`
+
+10. **Earlier Agent Sandbox selection.** This event was previously compacted
+ in
+ `docs/decision-evidence/20260730-distributed-society-execution-agent-sandbox-trajectory.md`
+ on candidate commit `a2b55f32e8b8cc688c8a290972267492a3dbfc0b`.
+
+ Source system: Codex stored-session interaction. Source session
+ `019fab08-15ca-7a10-a9af-f2a8441a45f5`; enclosing turn
+ `019faffd-b6a0-7b90-bcc2-e6f59ba339dd`; native call
+ `call_wGDKczyyYEXYTVNWIhEoXYbN`; request timestamp
+ `2026-07-29T23:28:32.756Z`; result timestamp
+ `2026-07-29T23:29:50.407Z`; stored actor role `user`.
+
+ The preceding agent prompt offered this choice:
+
+ > Agent Sandbox gold (Recommended): Direct Sandbox CRs retain one stable logical agent while their backing Pods can restart.
+
+ The stored result was:
+
+ > `{"answers":{"gold_backend":{"answers":["Agent Sandbox gold","user_note: lets see what do we need to revisit? go through the provenence of our decisions regarding why we made them and estimate what are the tradeoofs"]}}}`
+
+11. **Mechanical repository and GitHub events.** These record execution state,
+ not human rationale.
+
+ Source system: git. On 2026-08-01 the worktree branch
+ `impl/917-main-local-society` merged `origin/main` revision `314ece9e` in
+ commit `2d3fc41295ae66b95d19c0df2d448a41781c9b07`. The merged baseline passed
+ the simulator and eval build, test-typecheck, lint, and test targets: 215
+ simulator tests and 75 eval tests.
+
+ Source system: GitHub. Issue
+ `https://github.com/chughtapan/moltzap/issues/936` holds the agent-maintained
+ non-normative implementation plan. Durable checkpoint comments were posted
+ as issue comments `5153357233` at `2026-08-01T20:46:52Z` and `5153393832`
+ at `2026-08-01T20:54:08Z`; the second was last updated at
+ `2026-08-01T21:02:01Z`. Issue comment `5153770731`, stored actor/account
+ `chughtapan`, was posted at `2026-08-01T22:35:05Z` and records the first
+ candidate's failed review plus the correction gate. The issue body and
+ comments are agent-published mechanical artifacts, not independent
+ human-authored rationale. Issue comment `5173168998`, also stored under
+ account `chughtapan`, records the acceptance checkpoint after the live user
+ reply; the connector exposed no creation timestamp, so none is invented.
+
+ Source system: git and isolated Codex review. The simplified candidate was
+ frozen as commit `1939ee8b92e95151473c323de8dd702e880dbde5`, tree
+ `b2a487141545e4cced7bc7ab0e0d08f344cebea3`. Fresh reviewer
+ `/root/candidate_blind_review_2` ran from `2026-08-04T00:32:44Z` through
+ `2026-08-04T00:43:38Z` with no author intervention. Its overall result was
+ `FAIL`: questions 1, 2, and 4 passed; questions 3, 5, and 6 failed because
+ the candidate required every code peer to run in its own container while
+ retaining the earlier host-local `effectRuntime({ build })` gateway and
+ shared-state realization. The unedited result is retained at
+ [`20260804-main-kubernetes-society-execution-cold-review.md`](./20260804-main-kubernetes-society-execution-cold-review.md).
+
+ Source system: GitHub. Issue comment `5173321800`, stored under account
+ `chughtapan`, records the failed-review checkpoint and correction gate. The
+ connector exposed no creation timestamp, so none is invented. This is an
+ agent-published mechanical artifact, not human rationale.
+
+ Source system: git and isolated Codex review. The corrected candidate was
+ frozen as commit `2749adbd99eaffd16f063a45de7be01c253f7ef1`, tree
+ `ce6655004c93d03e6276a07756a5086ce68aa662`. Different fresh reviewer
+ `/root/candidate_blind_review_3` ran from `2026-08-04T01:03:05Z` through
+ `2026-08-04T01:10:58Z` with no author intervention or repository
+ modification. All six questions passed and the overall result was `PASS`
+ with no blockers. The reviewer states that maintainer acceptance remains
+ required because the result is not self-certifying. The unedited result is
+ retained at
+ [`20260804-main-kubernetes-society-execution-second-cold-review.md`](./20260804-main-kubernetes-society-execution-second-cold-review.md).
+
+Source gaps, stated plainly:
+
+- The retained Codex events supply no parent locator. Their message id,
+ session, enclosing turn, event kind, exact timestamp, and stored actor role
+ are retained; no missing locator is invented.
+- The assistant proposal is an agent event. The terse `okay do this` is read
+ only with that directly preceding retained proposal; it is not independent
+ rationale for every later mechanism.
+- The retained assistant example places `infrastructure` inside the RunSpec.
+ The simplified candidate retains that placement. No retained user event
+ chooses the exact Layer-constructor spelling, so the example's
+ `infrastructure` value remains the binding shape while its construction is
+ ordinary implementation detail.
+- The accepted assistant proposal states that deterministic peer behavior
+ belongs inside agent containers, and the earlier retained selection requires
+ one container per agent. The source does not choose a bridge transport or
+ wire schema. The corrected candidate therefore admits only the minimum
+ runtime-specific controller bridge needed to expose each container
+ runtime's exact gateway and termination observation; exact transport details
+ remain private implementation choices, not a universal gateway contract.
+- The retained `start` reply is read only with the immediately preceding issue
+ summary. That summary stated exactly-once customer-program invocation. The
+ simplified candidate instead says that the controller invokes the customer
+ Effect once and does not replay it, while making no exactly-once claim for
+ external effects. That wording is not silently attributed to the terse
+ `start` reply; the later explicit acceptance retained below resolves it.
+- The user chooses main, the core simulator, one RunSpec/execute model, the
+ GKE sandbox work, Kubernetes/Kueue/Temporal, local or GKE profiles, durable
+ issue notes, and end-to-end eval execution. The retained events do not
+ separately state reasons for every resource shape, failure variant,
+ security control, event field, or platform mechanism in the ADR.
+- Exact upstream versions, API schemas, chart/provider choices, timeouts,
+ storage mechanisms, cost budgets, generation protocols, artifact
+ authorities, identity derivations, and recovery schemes are not human
+ decisions in these excerpts. The simplified ADR treats them as private
+ implementation details, deliberate deferrals, or non-goals rather than
+ attributing them to the decision-maker.
+- Two later live user messages rejected the overbuilt candidate as
+ overcomplicated experiment infrastructure and directed that the previous
+ checked-in requirement conversations form the boundary, with anything not
+ discussed there treated as a non-goal. The workspace-readable session logs
+ checked on 2026-08-01 did not contain those two messages, so no native
+ message id, enclosing turn, timestamp, or actor-role record is invented.
+ Their literal text is retained in this source-gap report:
+
+ > this is overcomplicated. This is simulation infrastructure for experiments. Don't over complicate
+
+ > you can go throught he previous conversations that are checked-in regarding the requirements. anything which is not discussed there is a NON GOAL
+- The immediately following live exchange accepted the simplified ADR. The
+ workspace-readable session logs checked on 2026-08-03 did not contain the
+ exchange, so no native message id, enclosing turn, timestamp, parent locator,
+ or stored actor-role record is invented. The public assistant prompt and
+ terse user reply are both retained because the reply has no independent
+ meaning without the prompt.
+
+ The assistant prompt was:
+
+ > Done. The plan now treats checked-in requirement conversations as the hard boundary.
+ >
+ > Final shape:
+ >
+ > - `RunSpec { id, events, agents, infrastructure, execute }`
+ > - `Run.execute(spec)`
+ > - Local Kubernetes or GKE via the infrastructure Layer
+ > - Kueue admission → one Sandbox/container per agent → full-roster gate → execute once → existing ledger/outcome → Temporal cleanup
+ > - Two-agent, ten-agent, and all 32 OpenClaw/NanoClaw evaluation runs
+ > - No generations, artifact authority, start-or-attach system, custom serialization grammar, or recovery platform
+ >
+ > [Issue #936](https://github.com/chughtapan/moltzap/issues/936) and the 244-line ADR now match. Formatting, docs, Mermaid, and repository gate checks pass.
+ >
+ > One explicit ADR decision remains: if the controller fails, the run fails and cleans up; `execute` is never replayed, with no exactly-once guarantee for external effects.
+ >
+ > If you accept that final shape, say **“accept this ADR”** and I’ll freeze it, run the fresh blind review, then start implementation.
+
+ The user replied exactly:
+
+ > accept this ADR
+
+ This records acceptance of the preceding proposal, including its explicit
+ controller-failure and no-replay wording. It does not add rationale or
+ authorize mechanisms absent from that proposal and the earlier retained
+ requirements.
+- The GitHub issue body and checkpoint prose were composed and updated by the
+ agent. They preserve the current mechanical plan but do not replace the
+ human source events above.
+- Irrelevant tool output, private system and developer instructions, hidden
+ reasoning, environment diagnostics, and credential values are omitted. No
+ private session URL or Secret value is retained.
+
+## Later corrections
+
+Dated additions recording what a later reading found. The retained events above
+are unchanged; nothing here rewrites them.
+
+### 2026-08-06 — the field is named `cluster`, and no retained event chose it
+
+The source-gap note above states that the example's `infrastructure` value
+"remains the binding shape". That is no longer true of the admitted record,
+which names the field `cluster`, matching `packages/simulator/src/definition.ts`
+and the orientation docs. No retained event chooses either spelling, so this
+remains a gap in the ledger rather than a human call it can cite. The rename is
+recorded as a point correction in the decision's own changelog.
+
+### 2026-08-06 — the cohort-size gate no longer names a number
+
+The only retained human statement on cohort size is `lets get to 10 agents
+first and then scale`, and the accepted final-shape prompt says `Two-agent,
+ten-agent, and all 32 OpenClaw/NanoClaw evaluation runs`. A later amendment
+replaced ten with four while citing no event, and the profile tooling continued
+to enforce ten. The blind review recorded at
+[`20260806-main-kubernetes-society-execution-third-cold-review.md`](./20260806-main-kubernetes-society-execution-third-cold-review.md)
+reported that contradiction against candidate `78ff2f94`.
+
+A live exchange then directed one end-to-end experiment sized by its run rather
+than any fixed number, accepted point corrections to the record, and stated the
+reason autoscaling was selected. The workspace-readable session logs checked on
+2026-08-06 did not contain that exchange, so no native message id, enclosing
+turn, timestamp, parent locator, or stored actor-role record is invented. Its
+literal text is retained here:
+
+> okay, so we have run hundred. that's fine. also, instead of making it the thing be specific to number of agents, just make it an end-to-end test for the simulator that can run with varying numbers of agents. that's good enough. for the other things fine to update the ADRs using point changes: autoscaling was selected because it was easier simply
+
+`we have run hundred` is the only statement retained about the hundred-agent
+run. No source event states where that run's evidence lives, and this ledger
+does not supply one; the repository records the exported ledger's location
+nowhere, so a reader cannot verify the run from the repository alone.
+
+The excerpt directs the end-to-end change and accepts point corrections. It
+does not mention the scale-claim non-goal, and no retained event states whether
+dropping `100-` from that list was intended. **No source event located** for
+that specific removal.
diff --git a/docs/decision-evidence/20260804-main-kubernetes-society-execution-cold-review.md b/docs/decision-evidence/20260804-main-kubernetes-society-execution-cold-review.md
new file mode 100644
index 000000000..5a8271b18
--- /dev/null
+++ b/docs/decision-evidence/20260804-main-kubernetes-society-execution-cold-review.md
@@ -0,0 +1,366 @@
+# Blind teammate review
+
+**Overall result: FAIL**
+
+The candidate leaves a binding cross-process runtime/gateway boundary unresolved. The Kubernetes ADR requires every code/Effect peer to run in its own Sandbox container while the controller receives that peer’s exact native gateway. The retained gateway ADR defines those gateways as in-process values and forbids adding a generic proxy protocol.
+
+## Audit record
+
+- Candidate commit: `1939ee8b92e95151473c323de8dd702e880dbde5`
+- Candidate tree: `b2a487141545e4cced7bc7ab0e0d08f344cebea3`
+- Branch: `impl/917-main-local-society`
+- Worktree: clean at start and end; branch was 15 commits ahead of its tracking branch
+- Review start: `2026-08-04T00:32:44Z`
+- Review end: `2026-08-04T00:43:38Z`
+- Duration: 654 seconds (`00:10:54`)
+- Reviewer identity: `/root/candidate_blind_review_2`
+- Author interventions: none
+- Repository modifications: none
+- Mechanical checks:
+ - `git diff --check origin/main...HEAD`: passed
+ - `pnpm docs:check`: passed with no broken links
+- Isolation attestation: I received only the repository root, the fixed questions, and isolation instructions. I did not author or reconcile the candidate, receive a design summary, diff tour, ADR pointer, search term, expected result, or earlier review output. I did not ask questions or accept hints.
+- Quarantine attestation: directory/diff listings exposed the path `docs/decision-evidence/20260801-main-kubernetes-society-execution-cold-review.md`, which is explicitly allowed. I never opened it or searched its contents. Every `rg` command excluded `**/*-cold-review.md` and `**/*invalid-review*`; no quarantined answer or verdict content was returned.
+
+## Exact prompt
+
+1. What decision does this candidate make current, what problem does it resolve, and which statements are binding versus context or non-normative explanation?
+2. What earlier outcomes does it replace, retain, or leave untouched, and where does the current normative contract live?
+3. What must an implementer now do or avoid, which layers or consumers are affected, and under what fault, trust, safety, liveness, and compatibility assumptions?
+4. Which humans are named as decision-makers, which source events does the compacted trajectory cite for their calls, alternatives, reversals, and deferrals, and what source gaps does it explicitly record? Report only what the event ledger states; do not infer motives, confidence, urgency, or rationale.
+5. Find the strongest apparent contradiction, stale instruction, or broken lineage elsewhere in the repository. Resolve it using the authority order or report it as a blocker.
+6. Could a teammate implement the decision without chat or guessing? List every missing link or unresolved choice and classify each as a deliberate deferral or an accidental gap.
+
+## Independently discovered paths and headings
+
+- `AGENTS.md` → “Architecture decision records”, “Blind teammate review gate”, “Docs”
+- `docs/decisions/README.md` → “Canonical reading guidance”, “Records”
+- `docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md` → all headings under “Decision Outcome”, “Non-goals”, “Current owners and earlier outcomes”
+- `docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md` → “The main simulator runs container societies on Kubernetes”, “Source gaps, stated plainly”
+- `docs/decisions/20260727-code-first-simulator-kernel.md` → “Supersession”, “Public Boundary”
+- `docs/decisions/20260729-principal-io-uses-runtime-gateways.md` → “One society, two interaction boundaries”, “Runtime contract and keyed gateway types”, “Scenario ownership”, “Normative Owners”, “Consequences”
+- `docs/decisions/20260729-effect-native-evaluation-results.md` → “Supersession”, “Trust, availability, and compatibility”
+- `packages/simulator/AGENTS.md` → “Boundary”, “Laws”, “Structure”
+- `packages/evals/README.md` → “Execution model”
+- `packages/simulator/src/runtime/runtime.ts` → `AgentRuntimeDefinition`, `RunningAgent`
+- `packages/simulator/src/runtime/effect.ts` → `EffectAgent`, `effectRuntime`
+- `packages/evals/src/peer.ts` → `peerRuntime`
+- `v2/AGENTS.md` → “Authority and reading order”, “Simulator provenance gate”
+- `docs/decisions/20260728-simulator-is-the-system-driver.md` → “Decision Outcome”
+- Transitional documentation in the root, simulator, eval, and example READMEs
+
+## Answers
+
+### 1. Current decision, problem, and authority
+
+The candidate makes current a main/v1 production contract in which:
+
+- An experiment is one code-first `RunSpec` containing `id`, events, an exact keyed runtime roster, an infrastructure Layer, and one customer `execute` Effect.
+- `Run.execute(spec)` is the new execution entry point.
+- Local Kubernetes and GKE are profiles of one private Kubernetes path.
+- Each execution creates one society: Temporal starts one coarse workflow, Kueue admits the complete roster, one Agent Sandbox/application container is created per roster entry, the exact roster passes one readiness gate, the in-cluster controller invokes `execute` once, existing simulator evidence is retained, and Temporal drives cleanup.
+- Principal control continues through exact runtime-native gateways; social traffic continues through the production MoltZap router.
+- The old host `simulator.define(...).run(...)` path and Docker example are transitional and removed only after replacement evidence exists.
+
+It resolves the gap between an example-only local Docker proof and the requested core simulator path capable of running the same experiment society on local Kubernetes or GKE.
+
+Binding material is:
+
+- The accepted frontmatter and “Scope and authority”.
+- The complete “Decision Outcome”, including acceptance gates and “Non-goals”.
+- “Current owners and earlier outcomes”.
+- The explicit retained/replaced scope in the predecessor ADR’s “Supersession” section.
+
+The `RunSpec.infrastructure` field placement is binding. The trajectory explicitly classifies exact Layer-constructor spelling as implementation detail.
+
+“Context and Problem Statement” and “Consequences” explain the decision. Source trajectories, issue comments, git history, earlier implementation plans, and old ADR context/implementation-plan prose are non-normative.
+
+**Verdict: PASS**
+
+### 2. Replacement, retention, and current normative owners
+
+The candidate replaces only these main/v1 portions of `20260727-code-first-simulator-kernel.md`:
+
+- Public `simulator.define(...).run(...)` naming.
+- The host-only concrete execution path.
+
+That predecessor is now `partially-superseded`, names the new ADR as its primary `superseded-by`, and visibly retains:
+
+- Code-first TypeScript/Effect authoring.
+- Closed typed EventCatalog.
+- Typed RunLedger and producer-bound writers.
+- Exact keyed runtime gateways.
+- Customer-owned scenario, sweep, completion, and grading policy.
+- The single `@moltzap/simulator` package.
+- The production v1 router/protocol and absence of social callback shortcuts.
+
+`20260729-principal-io-uses-runtime-gateways.md` remains accepted and governs principal gateways, exact gateway types, social-router traffic, mixed societies, termination policy, and behavioral evidence.
+
+`20260729-effect-native-evaluation-results.md` remains partially current for cases, grading, report resume, SQLite, and Phoenix; its synthetic-sender portions remain replaced by the principal-gateway ADR.
+
+The v2 simulator driver, v2 package ownership, `Simulator.define`, and v2 distributed-execution contracts remain untouched. The apparent v1/v2 naming difference is explicitly scoped by both tracks.
+
+Current normative authority therefore lives in the new main Kubernetes ADR together with the explicitly retained code-first ADR scope and accepted principal-gateway ADR. `packages/simulator/AGENTS.md` repeats the intended package-level implementation laws. Trajectories are provenance, not authority.
+
+The decision index, frontmatter status, visible supersession section, and normative-owner statements agree.
+
+**Verdict: PASS**
+
+### 3. Implementation obligations and assumptions
+
+An implementer must:
+
+- Add `RunSpec` and `Run.execute` to `packages/simulator`.
+- Keep the roster, event, ledger, network, outcome, and exact gateway concepts rather than introduce another simulator model.
+- Hide Kubernetes, Kueue, Agent Sandbox, Temporal, Helm, Terraform, and cloud-provider objects behind the Effect Layer/private platform boundary.
+- Supply local-Kubernetes and GKE profiles through the same execution path.
+- Admit the complete roster, create one Sandbox/application container per entry, wait for exact readiness, invoke the customer Effect once without replay, preserve evidence, and clean all run-owned resources.
+- Preserve native principal control and production-router social traffic.
+- Migrate all 32 OpenClaw/NanoClaw evaluation cells before deleting the host path.
+- Prove the fake-platform, two-agent, ten-agent, GKE, evaluation, and zero-residue acceptance gates.
+
+It must avoid compatibility aliases, a Docker backend, warm pools, public Kubernetes objects, generation/rebind/recovery APIs, customer Effect replay, exactly-once external-effect claims, artifact authority, global execution identities, custom serialization grammar, per-agent Temporal workflows, new schedulers, and premature scale claims.
+
+Affected owners are `packages/simulator` and its private platform implementation; `packages/evals` remains a consumer. No `v2/*` contract changes.
+
+The discoverable assumptions are:
+
+- Autonomous agents may ignore instructions, misbehave, terminate, or remain unavailable.
+- Gateway adapters and simulator evidence machinery are trusted evaluation instruments.
+- Controller or platform loss is an infrastructure failure and starts cleanup.
+- Service availability affects progress and operational results, not behavioral truth.
+- Complete-roster readiness is the pre-dispatch safety gate.
+- Post-dispatch runtime termination is evidence interpreted by customer policy.
+- The controller does not replay `execute`; this is not exactly-once safety for external effects.
+- Temporal/Kubernetes status is operational observation, not simulator evidence authority.
+- Production Temporal HA, router HA, autoscaling, recovery, and scale beyond ten agents are not claimed.
+- The v2 Byzantine/fault assumptions do not silently apply to this v1 decision.
+
+However, the implementation obligations are not mutually satisfiable for current code/Effect runtimes. The retained exact in-process gateway contract has no selected cross-container representation, described under question 5.
+
+**Verdict: FAIL**
+
+### 4. Decision-makers, events, alternatives, reversals, deferrals, and source gaps
+
+The sole human named in ADR frontmatter is **Tapan Chugh**. The event ledger identifies stored actors as `user`, `assistant`, absent, or mechanical accounts; it does not independently map each stored `user` event to Tapan. I do not infer such a mapping.
+
+The main trajectory cites:
+
+- Codex session `019fbbdd-7cff-7753-8541-4f66f0248d43`:
+ - `msg_019fbbe1-770d-7d11-8475-0f2f7b3bd7b1` and `msg_019fbdeb-1743-7470-be76-7ed53d7f2420`: target main first and make the work part of the core simulator.
+ - `msg_019fbded-2372-72b0-b859-61f6fe80ac47`: plan the final shape before implementation.
+ - Assistant `msg_0141f487830063b4016a6e17e648d481939b073eea4e50a234`, followed by user `msg_019fbe0e-7474-7e53-9f4e-40faac7ac654`: one `RunSpec`, one customer `execute` callback, and `Run.execute`; the user reply is `okay do this`.
+ - `msg_019fbe84-b81b-7312-ad62-03432f57cdf2` and `msg_019fbe88-7cd4-7c62-9b8c-e9060c44f8d8`: pull GKE Sandbox into the core and use Kubernetes, Kueue, Temporal, and local/GKE targets.
+ - `msg_019fbe9a-2e94-7430-8da7-f71f0e533f15` and `msg_019fbe9c-4f9a-7970-adb5-15463aea8686`: main and `packages/simulator`, not v2.
+ - Assistant `msg_0141f487830063b4016a6e40cd78048193bca36ecb2c05a8a2`, immediately followed by user `msg_019fbf10-e051-75d0-92d7-bfb32174edfb`: the overbuilt issue-plan summary and contextual reply `start`.
+ - `msg_019fbf11-b878-7e83-902a-db4e3868e856`: work on issue #936, keep issue notes, and run evaluations end to end through the new path.
+- Earlier Codex session `019fab08-15ca-7a10-a9af-f2a8441a45f5`:
+ - `call_vlz2QouoKyvTCXhmbDB9Hiny`: selected one single-run society.
+ - `call_PU6nJTGPlpeJ3PATixSc2ef8`: selected a strict cohort gate.
+ - Direct user event at `2026-07-29T00:03:38.313Z`: one container per agent.
+ - `call_J4GjN5U25rt7aNh4Jo8eY8L9`: rejected the offered larger-scale gates and deferred them until ten agents.
+ - Direct user events at `2026-07-29T00:14:21.664Z` and `00:16:16.056Z`: general Kubernetes, stock OpenClaw compatibility, and prebuilt images only as optimization.
+ - `call_SnFa3x3617eQul6H1zPNZeCm`: Temporal plus Kueue.
+ - `call_mbMK8n64ZfjzAGVA69nzjhIw`: local Temporal first, production hosting unselected.
+ - `call_8Tj66rC9ATIk5wZqXIiFtRia`: regional GKE Standard.
+ - `call_0HQBCkj6yDpE4i7yXzEsTp8g`: in-cluster controller.
+ - `call_0OO9tWVFfZHYPNu61PoPXcqN`: CLI plus library.
+ - `call_z5VtaeUzaAe4BaD0DJh3UnVU`: Terraform plus Helm.
+ - `call_wGDKczyyYEXYTVNWIhEoXYbN`: Agent Sandbox.
+- Mechanical events:
+ - Merge commit `2d3fc41295ae66b95d19c0df2d448a41781c9b07`.
+ - GitHub issue comments `5153357233`, `5153393832`, `5153770731`, and `5173168998`, all expressly classified as agent/mechanical artifacts rather than independent human rationale.
+
+The retained code-first trajectory cites session `019fa613-7f9a-7103-99b0-a42fda0754de` for code-first customer policy, a closed event universe, mixed societies, customer-owned runtime-termination policy, ledger terminology, Effect services/SQL, branded types, and one simulator package.
+
+The retained principal-gateway trajectory cites the same session, principally turn `39d5505f-efa9-417d-b97f-14af5a270f73` and attachment `f4eee480-6d7d-4bb2-b8e7-0d6c57e60b6e` with SHA-256 `23a57ba9d5b83e186006dcfa43960e70d734fec3b3cf3fc25f2be98008b71622`, for exact native gateways, no gateway union, no synthetic principal, native evidence correlation, `replyToId` removal, and behavioral-evaluation reclassification. Later cited events place restart/replacement outside v0, reject compatibility preservation, request Effect SQL/evaluation-result tooling, and state that a code agent’s Effect API is already its native gateway.
+
+The reversal is explicit:
+
+- The contextual `start` followed an agent-authored plan containing exact generations, start-or-attach machinery, and exactly-once invocation language.
+- Two later live user messages, retained only in the source-gap report, rejected the overcomplicated design and made checked-in requirement conversations the boundary.
+- A later assistant prompt presented the simplified shape, including no generations/artifact authority/start-or-attach/recovery and controller failure with no replay.
+- The unlocated user reply was `accept this ADR`.
+
+Explicit source gaps include:
+
+- Current Codex events have no parent locator.
+- Several earlier events have no separate message ID or stored actor role; the available session, turn/call, event kind, and timestamps are retained.
+- Terse replies are meaningful only with their immediately preceding retained prompts.
+- The final Layer-constructor spelling was never selected.
+- Reasons were not separately stated for every resource shape, failure variant, security control, or mechanism.
+- Versions, upstream API schemas, provider/chart choices, timeouts, storage mechanisms, generation protocols, artifact authorities, identity derivations, and recovery schemes were not human decisions in the excerpts.
+- The two overcomplication-rejection messages and final acceptance exchange could not be recovered from workspace-readable session logs, so they have no native IDs, timestamps, parent locators, or stored actor-role record.
+- The GitHub issue and comments are agent-authored mechanical artifacts; one comment has no exposed creation timestamp.
+- The principal-gateway handoff does not locate its preceding conversations and does not choose concrete gateway APIs, commands, transports, or response shapes.
+
+These gaps are stated rather than silently repaired.
+
+**Verdict: PASS**
+
+### 5. Strongest contradiction or broken lineage
+
+The strongest contradiction is between two current main-track contracts.
+
+`20260729-principal-io-uses-runtime-gateways.md` says:
+
+- A code agent’s in-process Effect API is itself its native gateway.
+- `effectRuntime({ build })` returns the exact customer gateway and autonomous behavior, which may share scoped Effect state.
+- The simulator must not add a generic command queue, actor mailbox, second request protocol, or universal gateway normalization.
+- Evaluation peers remain ordinary `effectRuntime({ build })` policies.
+
+The new Kubernetes ADR says:
+
+- Every roster entry, including real and code/scripted agents, is one Agent Sandbox application container.
+- Infrastructure containers are not agents.
+- A controller invokes the one customer `execute` Effect.
+- That Effect retains the exact keyed runtime gateways.
+- All 32 evaluation cells move through this path.
+
+The checked-in implementation confirms the collision:
+
+- `packages/simulator/src/runtime/effect.ts → EffectAgent` places `gateway` and `behavior` in the same acquired in-process runtime.
+- `packages/evals/src/peer.ts → peerRuntime` uses a shared in-process `Deferred` as the peer’s observation gateway.
+- `packages/evals/README.md` says every one of the 32 societies contains autonomous in-process Effect peers.
+- `scriptedRuntime` appears only in the new ADR example; it has no checked-in contract or symbol.
+
+Once such a peer runs in its own Sandbox container, the controller cannot receive the same in-process gateway value. Resolving that requires one of:
+
+1. A remote proxy/serialization protocol for arbitrary gateway values.
+2. Co-locating the code peer with the controller.
+3. Removing or replacing `effectRuntime` peers from the Kubernetes path.
+
+Each option violates or changes a current binding statement.
+
+The authority order cannot resolve this. The new ADR expressly says the principal-gateway ADR remains current and replaces only public naming and the host execution path. Root ADR law prohibits silently replacing an accepted outcome. `packages/simulator/AGENTS.md` repeats both sides instead of selecting a reconciliation.
+
+Other apparent contradictions are resolved:
+
+- Old main documentation and Docker code are explicitly marked transitional until acceptance evidence exists.
+- v2 continues to use `Simulator.define`, but both tracks explicitly scope that contract to v2.
+
+The runtime/gateway collision remains a blocker.
+
+**Verdict: FAIL**
+
+### 6. Implementability and unresolved choices
+
+No. A teammate cannot implement all binding requirements without inventing a new public or private contract that changes retained semantics.
+
+Accidental gaps:
+
+- No contract maps the existing executable `AgentRuntime.acquire` closure to a remotely deployed Sandbox application container.
+- No contract transports an arbitrary exact `Gateway` and `termination` Effect from an agent container to the controller.
+- No decision explains how `effectRuntime` builder closures and their shared scoped state execute remotely.
+- `scriptedRuntime` is used in the normative example but is neither defined nor reconciled with the retained decision against a generic scripted-agent gateway.
+- The required 32-cell migration cannot preserve the current in-process Effect peers without resolving those boundaries.
+- Consequently, the “one container per roster entry”, “exact native gateway”, “controller invokes execute”, and “reuse rather than replace the existing runtime model” requirements cannot all be implemented simultaneously.
+
+Deliberate deferrals/non-goals:
+
+- Generation/rebind/rejoin/replacement/recovery APIs.
+- Customer Effect replay and exactly-once external effects.
+- Artifact authority, start-or-attach storage, global execution IDs, and normative Kubernetes naming.
+- New serialization grammar and universal input/result/failure schemas.
+- Public Kubernetes objects, arbitrary Pod templates, and per-agent Temporal workflows.
+- Warm pools, multi-run scheduling, fairness, borrowing, preemption, autoscaling, router HA, and production Temporal HA.
+- Scale qualification above ten agents.
+- Nomad, Slurm, managed batch, and GKE Autopilot.
+- Exact Secret-provider protocols, persistent state recovery, exhaustive NetworkPolicy, and general multi-tenant security.
+- Exact upstream versions, API schemas, chart/provider selection, cache/transport details, timeouts, and storage mechanisms.
+- Exact production Temporal hosting.
+
+Explicit provenance limitations, not silent design gaps:
+
+- Missing native locators/timestamps for the later rejection and final acceptance exchange.
+- Missing independent human rationale for most private mechanisms.
+
+The accidental runtime/gateway gaps are architectural, not ordinary private Kubernetes mechanics.
+
+**Verdict: FAIL**
+
+## Blockers
+
+1. Define and admit how a separately containerized `effectRuntime` or customer code runtime exposes its exact gateway and termination observation to the controller without violating the retained prohibition on a generic second protocol.
+2. Decide whether `scriptedRuntime` is a new public runtime contract, remove it from the binding example, or explicitly supersede the retained `effectRuntime` evaluation-peer requirement.
+3. Update the ADR lineage, normative ownership, package instructions, and evaluation transition together once that choice is made, then freeze a new candidate for a different blind reviewer.
+
+## Discovery trail and commands
+
+All commands ran read-only from `/home/tapanc/moltzap-pr-917-main`.
+
+```text
+date -u +'%Y-%m-%dT%H:%M:%SZ'
+git rev-parse HEAD
+git rev-parse HEAD^{tree}
+git branch --show-current
+git status --short --branch
+
+sed -n '1,280p' AGENTS.md
+sed -n '281,560p' AGENTS.md
+git log --oneline --decorate --graph -30
+git diff --name-status origin/main...HEAD
+git diff --stat origin/main...HEAD
+
+sed -n '1,280p' docs/decisions/README.md
+sed -n '1,340p' docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md
+sed -n '1,460p' docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md
+sed -n '1,360p' docs/decisions/20260727-code-first-simulator-kernel.md
+sed -n '361,720p' docs/decisions/20260727-code-first-simulator-kernel.md
+sed -n '1,360p' docs/decisions/20260729-principal-io-uses-runtime-gateways.md
+sed -n '1,340p' docs/decisions/20260729-effect-native-evaluation-results.md
+
+rg -n --glob '!**/*-cold-review.md' --glob '!**/*invalid-review*' '^## |^Source gaps' docs/decision-evidence/20260727-code-first-simulator-trajectory.md docs/decision-evidence/20260729-principal-runtime-gateway-trajectory.md
+sed -n '1,190p' docs/decision-evidence/20260727-code-first-simulator-trajectory.md
+sed -n '1,330p' docs/decision-evidence/20260729-principal-runtime-gateway-trajectory.md
+
+rg -n --hidden --glob '!**/*-cold-review.md' --glob '!**/*invalid-review*' --glob '!node_modules/**' --glob '!.git/**' 'simulator\.define|Simulator\.define|RunSpec|Run\.execute|Docker execution backend|Kubernetes' .
+sed -n '1,240p' packages/simulator/AGENTS.md
+sed -n '140,230p' README.md
+sed -n '1,130p' packages/simulator/README.md
+sed -n '1,260p' docs/simulator/running.mdx
+sed -n '1,140p' examples/simulator/README.md
+
+sed -n '1,180p' v2/AGENTS.md
+sed -n '1,280p' docs/decisions/20260728-simulator-is-the-system-driver.md
+sed -n '1,220p' v2/inputs/simulator-handoff-20260728.md
+
+rg -n --hidden --glob '!**/*-cold-review.md' --glob '!**/*invalid-review*' --glob '!node_modules/**' --glob '!.git/**' 'start-or-attach|generation (API|stream|identifier)|exactly-once|at-most-once|artifact authority|execution-id|Temporal.*replay|replay.*Temporal' .
+rg -n --glob '!**/*-cold-review.md' --glob '!**/*invalid-review*' 'docs:check|check:links|mermaid' package.json tools packages -g 'package.json' -g 'project.json' -g '*.ts' -g '*.mjs'
+sed -n '1,320p' .github/workflows/ci.yml
+
+git diff --check origin/main...HEAD
+pnpm docs:check
+git status --short --branch
+git rev-parse HEAD
+git rev-parse HEAD^{tree}
+
+rg -n --glob '!**/*-cold-review.md' --glob '!**/*invalid-review*' 'decision-evidence|decision-makers|partially-superseded|MADR|ADR' scripts tools package.json -g '*.ts' -g '*.mjs' -g '*.json'
+
+rg -n --glob '!**/*-cold-review.md' --glob '!**/*invalid-review*' 'effectRuntime|defineRuntime|scriptedRuntime' packages/simulator/src packages/evals/src docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md packages/simulator/AGENTS.md
+sed -n '200,320p' packages/simulator/src/runtime/runtime.ts
+sed -n '1,330p' packages/simulator/src/runtime/effect.ts
+sed -n '430,530p' packages/evals/src/peer.ts
+
+git diff origin/main...HEAD -- docs/decisions/20260729-principal-io-uses-runtime-gateways.md docs/decisions/20260729-effect-native-evaluation-results.md
+git rev-parse origin/main
+git merge-base origin/main HEAD
+git diff --name-status origin/main..HEAD -- docs/decisions/20260729-principal-io-uses-runtime-gateways.md docs/decisions/20260729-effect-native-evaluation-results.md
+git diff --name-status origin/main...HEAD -- docs/decisions/20260729-principal-io-uses-runtime-gateways.md docs/decisions/20260729-effect-native-evaluation-results.md
+git diff --name-status origin/main...HEAD
+
+sed -n '1,120p' packages/evals/README.md
+sed -n '1,130p' packages/evals/src/README.md
+sed -n '1,130p' packages/simulator/src/runtime/runtime.ts
+
+rg -n --hidden --glob '!**/*-cold-review.md' --glob '!**/*invalid-review*' --glob '!node_modules/**' --glob '!.git/**' '\bscriptedRuntime\b|\beffectRuntime\b' .
+
+review_end=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
+review_start_epoch=$(date -u -d '2026-08-04T00:32:44Z' +%s)
+review_end_epoch=$(date -u -d "$review_end" +%s)
+review_duration_seconds=$((review_end_epoch-review_start_epoch))
+printf '%s\n' "$review_end" "$review_duration_seconds"
+git status --short --branch
+git rev-parse HEAD
+git rev-parse HEAD^{tree}
+```
diff --git a/docs/decision-evidence/20260804-main-kubernetes-society-execution-second-cold-review.md b/docs/decision-evidence/20260804-main-kubernetes-society-execution-second-cold-review.md
new file mode 100644
index 000000000..05f862599
--- /dev/null
+++ b/docs/decision-evidence/20260804-main-kubernetes-society-execution-second-cold-review.md
@@ -0,0 +1,348 @@
+# Blind teammate review
+
+## Audit metadata
+
+- Candidate root: `/home/tapanc/moltzap-pr-917-main`
+- Candidate commit: `2749adbd99eaffd16f063a45de7be01c253f7ef1`
+- Candidate tree: `ce6655004c93d03e6276a07756a5086ce68aa662`
+- Review start: `2026-08-04T01:03:05Z`
+- Review end: `2026-08-04T01:10:58Z`
+- Duration: 473 seconds (`00:07:53`)
+- Reviewer: `/root/candidate_blind_review_3`
+- Author interventions: none
+- Repository modifications: none; final `git status --porcelain=v1` was empty.
+
+## Isolation attestation
+
+I did not author or reconcile this candidate. I received no design summary, diff tour, ADR/file pointer, search term, expected answer, inherited conversation, compaction, private state, or earlier blind-review output.
+
+I used only repository navigation, checked-in content, and Git history reachable from the supplied candidate root. I did not open, read, or search any `*-cold-review.md` or `*invalid-review*` artifact. Their paths appeared only in permitted directory and name-status listings. The current non-quarantined trajectory itself contains a mechanical summary of an earlier review; root `AGENTS.md` expressly classifies engineering-review evidence inside candidate trajectories as ordinary reviewable evidence.
+
+Every `rg` repository-content search used all four exclusions:
+
+```text
+--glob '!*-cold-review.md'
+--glob '!**/*-cold-review.md'
+--glob '!*invalid-review*'
+--glob '!**/*invalid-review*'
+```
+
+## Discovery trail
+
+Principal commands, in order:
+
+```text
+date -u +%Y-%m-%dT%H:%M:%SZ
+git rev-parse HEAD
+git rev-parse 'HEAD^{tree}'
+git status --short --branch
+pwd
+
+ls -la
+find . -maxdepth 2 -type f ...
+find docs -maxdepth 3 -type f ...
+find v2 -maxdepth 3 -type f ...
+
+sed -n ... AGENTS.md
+sed -n ... docs/decisions/README.md
+sed -n ... docs/decision-evidence/README.md
+
+sed -n ... docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md
+sed -n ... docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md
+
+git merge-base HEAD origin/main
+git diff --name-status ..HEAD
+git diff --stat ..HEAD
+
+sed -n ... docs/decisions/20260727-code-first-simulator-kernel.md
+sed -n ... docs/decisions/20260729-principal-io-uses-runtime-gateways.md
+sed -n ... docs/decisions/20260729-effect-native-evaluation-results.md
+sed -n ... docs/decision-evidence/20260727-code-first-simulator-trajectory.md
+sed -n ... docs/decision-evidence/20260729-principal-runtime-gateway-trajectory.md
+
+rg ... '\bRunSpec\b|Run\.execute|simulator\.define|effectRuntime|Docker execution backend|Kubernetes|Kueue|Temporal|Agent Sandbox' ...
+rg ... 'fault|trust|safety|liveness|availability|Byzantine|failure|compatib|security|assum|retry|idempot|replay|recovery|cleanup' ...
+rg ... '20260801-main-simulator-runs-container-societies-on-kubernetes|Main Kubernetes society execution|main simulator runs container societies' ...
+rg ... 'only execution entry point|one execution path|second simulator backend|supported Docker|host execution path|implementation transition|v2 simulator|testbed.*platform|platform.*testbed' ...
+rg ... '\bscriptedRuntime\b|generic scripted|gateway proxy|command language|actor mailbox|shared in-process|shared scoped' ...
+
+sed -n ... packages/simulator/AGENTS.md
+sed -n ... README.md
+sed -n ... packages/simulator/README.md
+sed -n ... packages/evals/README.md
+sed -n ... docs/simulator/overview.mdx
+sed -n ... docs/simulator/running.mdx
+sed -n ... docs/development/evals.mdx
+sed -n ... docs/development/eval-add-evaluation.mdx
+
+sed -n ... v2/AGENTS.md
+sed -n ... v2/VISION.md
+sed -n ... docs/decisions/20260729-v2-authority-lives-with-v2.md
+sed -n ... docs/decisions/20260728-simulator-is-the-system-driver.md
+sed -n ... v2/inputs/simulator-handoff-20260728.md
+sed -n ... docs/spec/layer-interfaces.md
+sed -n ... docs/architecture/components.md
+
+git cat-file -e a2b55f32...:
+git cat-file -e a2b55f32...:
+
+git diff --name-status 1939ee8b...HEAD
+git diff ... 1939ee8b...HEAD --
+
+git status --porcelain=v1
+```
+
+Independently discovered paths and headings:
+
+- `AGENTS.md` → `Project`, `Architecture decision records`, `Blind teammate review gate`, `Docs`
+- `docs/decisions/README.md` → `Canonical reading guidance`, `Records`
+- `docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md` → `Scope and authority`, `Decision Outcome`, `Container runtimes preserve exact native gateways`, `One execution is one experiment society`, `Failure and evidence retain the existing simulator semantics`, `Local and GKE are two profiles of one path`, `Acceptance is experiment evidence, not platform completeness`, `Non-goals`, `Current owners and earlier outcomes`
+- `docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md` → `The main simulator runs container societies on Kubernetes`, `Source gaps, stated plainly`
+- `docs/decisions/20260727-code-first-simulator-kernel.md` → `Supersession`
+- `docs/decisions/20260729-principal-io-uses-runtime-gateways.md` → `Supersession`, `One society, two interaction boundaries`, `Runtime contract and keyed gateway types`, `Normative Owners`
+- `docs/decisions/20260729-effect-native-evaluation-results.md` → `Supersession`, `Trust, availability, and compatibility`
+- `packages/simulator/AGENTS.md` → `Boundary`, `Laws`, `Structure`, `Tests`
+- `v2/AGENTS.md` → `Authority and reading order`, `Structure`, `Simulator provenance gate`
+- `docs/decisions/20260729-v2-authority-lives-with-v2.md` → `Binding outcome`
+- `docs/spec/layer-interfaces.md` → `Package graph`, `Simulator and testbed`
+
+## Unedited answers
+
+### 1. What decision does this candidate make current, what problem does it resolve, and which statements are binding versus context or non-normative explanation?
+
+The candidate makes the accepted main/v1 decision that the core `packages/simulator` executes experiment societies through one Kubernetes path, selected by either a local-Kubernetes or GKE Effect Layer. Its public authoring facade is one `RunSpec` containing the versioned definition id, closed event catalogs, exact keyed runtime roster, infrastructure Layer, and customer `execute` Effect; `Run.execute(spec)` is the sole new execution entry point.
+
+Each execution is one non-reusable society. Temporal owns one coarse run lifecycle and cleanup workflow, Kueue admits complete-roster capacity, the controller creates one Agent Sandbox/application container per roster entry, runtime-specific bridges attach and preserve exact native gateway types, the full roster passes one readiness gate, the controller invokes `execute` once, and the existing simulator ledger/outcome retain evidence.
+
+This resolves the mismatch between the existing host/process/Docker execution path and the requested core Kubernetes cohort. The Docker example can prove two OpenClaw containers but is neither the core execution path nor able to exercise the requested Kubernetes/Kueue/Agent Sandbox/Temporal society locally and on GKE.
+
+Binding material is:
+
+- root and package `AGENTS.md`;
+- the accepted ADR’s `Decision Outcome`, including ownership, failure semantics, acceptance gates, non-goals, and retained/replaced outcomes;
+- the visible `Supersession` sections of the two partially superseded earlier ADRs.
+
+Within the public example, the `RunSpec` field shape and placement of `infrastructure` are binding. The exact constructor spelling for already-constructed runtime descriptors and the infrastructure Layer is explicitly not selected.
+
+The ADR’s `Context and Problem Statement` and `Consequences` explain the decision. Historical bodies below a partially superseded record’s `Supersession` section are context where they describe replaced host mechanisms. Decision trajectories, Git/GitHub mechanical events, issue comments, transition documentation, and earlier review evidence are non-normative provenance or explanation.
+
+Verdict: **PASS**.
+
+### 2. What earlier outcomes does it replace, retain, or leave untouched, and where does the current normative contract live?
+
+It partially replaces `20260727-code-first-simulator-kernel.md`. Retained for main are the TypeScript/Effect code-first model, immutable closed EventCatalog, typed RunLedger and producer-bound writers, exact keyed gateways, network capabilities, customer-owned scenario/sweep/completion/grading policy, one `@moltzap/simulator` package, and the production v1 router/protocol. Replaced are the public `simulator.define(...).run(...)` naming and host-only execution/acquisition path, including host-local `AgentRuntime.acquire` and `effectRuntime({ build })` acquisition.
+
+It partially replaces `20260729-principal-io-uses-runtime-gateways.md`. Retained are exact runtime-native gateway types, principal-versus-social-traffic separation, production-router social traffic, mixed societies, distinct gateway/router evidence, customer interpretation of termination, and the prohibition on a universal gateway union, command language, correlation model, or social shortcut. Replaced is the Kubernetes-path realization in which code peers and gateways share in-process Effect state. Each Kubernetes runtime instead owns a portable application entrypoint and runtime-specific controller bridge returning the same exact `RunningAgent` shape after readiness.
+
+It leaves the retained portions of `20260729-effect-native-evaluation-results.md` untouched: cases, grading, report resume, SQLite authority, Phoenix publication, and behavioral truth. Only the location/mechanism of evaluation execution changes.
+
+The Docker example and host executor remain explicitly transitional until evaluation plus local/GKE replacement evidence exists. After cutover they are removed without a compatibility facade. Docker may still build images or support a local Kubernetes cluster.
+
+All v2 contracts, its six-package simulator/testbed split, process map, generation model, trust contracts, and `v2/*` code are untouched. `20260729-v2-authority-lives-with-v2.md` and the main ADR’s scope make that boundary explicit.
+
+The current normative contract lives in:
+
+- `AGENTS.md`;
+- `docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md`;
+- the retained scopes in the `Supersession` sections of `20260727-code-first-simulator-kernel.md` and `20260729-principal-io-uses-runtime-gateways.md`;
+- the retained evaluation-result ADR scope; and
+- `packages/simulator/AGENTS.md` for package-local implementation law.
+
+`packages/simulator` is the execution owner. `packages/evals` owns evaluation cases, runtime conditions, grading, reports, resume, and publication as a consumer.
+
+Verdict: **PASS**.
+
+### 3. What must an implementer now do or avoid, which layers or consumers are affected, and under what fault, trust, safety, liveness, and compatibility assumptions?
+
+An implementer must:
+
+- add `RunSpec` and `Run.execute` as the one new root execution facade;
+- retain the existing event, ledger, network, exact keyed gateway, Effect failure, outcome, and customer-completion concepts;
+- implement one private platform boundary with local-Kubernetes and GKE Layers;
+- represent every roster value on the Kubernetes path as a container runtime descriptor owning an application-container entrypoint and runtime-specific controller bridge;
+- preserve each runtime’s exact `Gateway` type and termination observation across that bridge;
+- put code-peer policy inside its peer container and keep `packages/evals` responsible for its exact observation bridge/adapter;
+- admit the whole roster through Kueue, create one Sandbox/application container per logical agent, attach every bridge, and dispatch only after the exact roster is ready;
+- run one coarse Temporal workflow, invoke the customer Effect once, retain simulator-ledger/outcome evidence, and clean up all run-owned Kubernetes resources;
+- provide the same library path through a small repository-local CLI;
+- qualify the private fake, two-agent local smoke, ten-agent local run, all 32 real evaluation cells, GKE smoke, and at least one GKE OpenClaw evaluation before removing the transitional host/Docker path.
+
+It must avoid:
+
+- a second Docker backend or compatibility facade;
+- exposing Kubernetes, Kueue, Sandbox, Temporal, Helm, Terraform, or cloud-provider objects through the customer contract;
+- serializing arbitrary JavaScript gateways, Effect closures, or shared state;
+- a universal gateway proxy, union, command language, mailbox, correlation/session/model protocol, or generic `scriptedRuntime`;
+- social shortcuts or synthetic participants impersonating an agent’s principal;
+- warm-pool reuse, per-agent Temporal workflows, automatic customer-Effect replay, customer-visible generation/restart/rebind/rejoin/recovery, exactly-once external-effect claims, or changes under `v2/*`.
+
+Affected owners are `packages/simulator` and its private platform/profile/controller assets, plus `packages/evals` as the migrating consumer. The production MoltZap router, ledger concepts, runtime gateway types, and evaluation evidence semantics are reused rather than redefined. Kubernetes, Kueue, Agent Sandbox, and Temporal are private mechanisms, not customer-facing layers.
+
+Fault and liveness assumptions are explicit:
+
+- before dispatch, a backing Pod restart leaves the slot outside the gate until both application and bridge are usable;
+- an unrecoverable or never-ready agent or bridge fails acquisition and starts cleanup;
+- after dispatch, runtime termination is typed ledger evidence and customer policy chooses whether to stop, fail, or continue;
+- controller loss or infrastructure failure fails the run and starts cleanup;
+- `execute` is invoked once and never automatically replayed, but this is not an exactly-once guarantee for external effects;
+- customer code owns application retry and idempotency;
+- automatic recovery, production Temporal HA, router HA, and larger-scale availability are not claimed.
+
+The retained trust model treats gateway adapters/event writers as trusted evaluation instruments while autonomous agents and runtime processes may ignore instructions, misbehave, terminate, or be unavailable. Missing evidence remains an operational/evidence failure and cannot become a behavioral pass. The candidate makes no Byzantine or multi-tenant security guarantee for the Kubernetes control path; Secret-provider protocols, exhaustive NetworkPolicy design, and a general multi-tenant security platform are explicit non-goals. V2’s Gate 1 Byzantine/trust envelope is not imported into this v1 decision.
+
+Safety comes from the complete-roster gate, one logical agent per application container, exact native gateway preservation, no social shortcut, one controller invocation without replay, and canonical simulator evidence. Progress depends on the required platform, bridge, runtime, router, and evaluation services remaining available; failure may end the run.
+
+Compatibility assumptions are deliberately breaking: the digest-pinned stock OpenClaw image is the baseline, a prebuilt MoltZap image is only an optimization, Docker ceases to be a supported executor after replacement evidence, and no host API compatibility alias survives. V2 remains unaffected.
+
+Verdict: **PASS**.
+
+### 4. Which humans are named as decision-makers, which source events does the compacted trajectory cite for their calls, alternatives, reversals, and deferrals, and what source gaps does it explicitly record? Report only what the event ledger states; do not infer motives, confidence, urgency, or rationale.
+
+The ADR names one human decision-maker: **Tapan Chugh**. The ledgers separately record stored actor roles and account names; they do not independently prove who controlled an account or that the named decision-maker authored every ADR sentence.
+
+The main trajectory cites:
+
+- Codex session `019fbbdd-7cff-7753-8541-4f66f0248d43`:
+ - message `msg_019fbbe1-770d-7d11-8475-0f2f7b3bd7b1`, turn `0a25724d-258f-41b3-a256-f8c95db5bd3a`, `2026-08-01T05:52:23.309Z`: target main first with the original simulator;
+ - message `msg_019fbdeb-1743-7470-be76-7ed53d7f2420`, turn `019fbdeb-1371-7be3-8e61-babd80ff5ffc`, `2026-08-01T15:22:08.579Z`: make it core rather than one example;
+ - message `msg_019fbded-2372-72b0-b859-61f6fe80ac47`, turn `019fbded-227b-70a3-9d9e-9a52a461b990`, `2026-08-01T15:24:22.771Z`: plan the final shape first;
+ - assistant proposal `msg_0141f487830063b4016a6e17e648d481939b073eea4e50a234`, followed by user message `msg_019fbe0e-7474-7e53-9f4e-40faac7ac654`, `2026-08-01T16:00:46.197Z`: accept the `RunSpec`/`Run.execute` proposal;
+ - messages `msg_019fbe84-b81b-7312-ad62-03432f57cdf2` and `msg_019fbe88-7cd4-7c62-9b8c-e9060c44f8d8`: pull GKE sandbox work into the core and use Kubernetes, Kueue, Temporal, local Kubernetes or GKE;
+ - messages `msg_019fbe9a-2e94-7430-8da7-f71f0e533f15` and `msg_019fbe9c-4f9a-7970-adb5-15463aea8686`: land on main and target `packages/simulator`, not v2;
+ - assistant plan summary `msg_0141f487830063b4016a6e40cd78048193bca36ecb2c05a8a2`, followed by user `start` in `msg_019fbf10-e051-75d0-92d7-bfb32174edfb`;
+ - work directive `msg_019fbf11-b878-7e83-902a-db4e3868e856`: work issue #936, keep durable issue notes, and run evaluations end to end.
+
+- Earlier Codex session `019fab08-15ca-7a10-a9af-f2a8441a45f5`, with exact calls/results repeated in the main trajectory:
+ - `call_vlz2QouoKyvTCXhmbDB9Hiny`: single-run cluster;
+ - `call_PU6nJTGPlpeJ3PATixSc2ef8`: strict cohort gate;
+ - direct user message at `2026-07-29T00:03:38.313Z`: one container per agent;
+ - `call_J4GjN5U25rt7aNh4Jo8eY8L9`: no offered scale gate selected; defer 100/1,000/5,000/10,000 claims and reach ten agents first;
+ - direct messages at `2026-07-29T00:14:21.664Z` and `00:16:16.056Z`: general Kubernetes, stock OpenClaw image baseline, prebuilt image only an optimization;
+ - `call_SnFa3x3617eQul6H1zPNZeCm`: Temporal plus Kueue;
+ - `call_mbMK8n64ZfjzAGVA69nzjhIw`: local Temporal first, production hosting deferred;
+ - `call_8Tj66rC9ATIk5wZqXIiFtRia`: regional GKE Standard;
+ - `call_0HQBCkj6yDpE4i7yXzEsTp8g`: in-cluster controller;
+ - `call_0OO9tWVFfZHYPNu61PoPXcqN`: CLI plus library;
+ - `call_z5VtaeUzaAe4BaD0DJh3UnVU`: Terraform plus Helm;
+ - `call_wGDKczyyYEXYTVNWIhEoXYbN`, turn `019faffd-b6a0-7b90-bcc2-e6f59ba339dd`: Agent Sandbox selection.
+
+The linked retained code-first trajectory cites session `019fa613-7f9a-7103-99b0-a42fda0754de` for code-first customer policy, closed typed events, simplification, mixed societies, customer-owned termination policy, ledger vocabulary, Effect services, branded SQL/Effect SQL, and one simulator package.
+
+The linked principal-gateway trajectory cites the same session’s attachment `f4eee480-6d7d-4bb2-b8e7-0d6c57e60b6e` and its digest for the principal/runtime/MoltZap boundary, exact gateway result, prohibited synthetic-principal actions, gateway/router evidence distinction, `replyToId` removal, and behavioral-evaluation reclassification. It also cites the direct no-restart/replacement scope, compatibility cleanup, evaluation-result-management requests, and the message questioning a generic code-agent command queue.
+
+The main trajectory records a reversal only as an explicit source gap: two later live messages rejected the overbuilt candidate and directed that checked-in requirement conversations be the boundary, with undisclosed matters treated as non-goals. It also records an immediately following live assistant prompt and terse `accept this ADR` reply accepting the simplified shape and explicit controller-failure/no-replay wording.
+
+Explicit source gaps are:
+
+- primary retained Codex messages lack parent locators;
+- terse replies are meaningful only with their directly preceding prompts;
+- no user event chooses exact Layer-constructor spelling;
+- no source event chooses a bridge transport or wire schema;
+- the issue summary’s “exactly-once” wording is not attributed to `start`; the later missing-session acceptance supplies the final once/no-replay/no-external-exactly-once wording;
+- retained events do not independently state reasons for every resource shape, failure variant, security control, event field, or platform mechanism;
+- no human selection is recorded for exact upstream versions, API schemas, chart/provider choices, timeouts, storage, cost budgets, generation protocols, artifact authorities, identity derivations, or recovery schemes;
+- the two simplification messages and final acceptance exchange could not be located in workspace-readable session logs, so no session id, native locator, timestamp, parent locator, or stored actor role is invented;
+- issue bodies/comments are agent-published mechanical artifacts, not independent human rationale;
+- irrelevant tool output, private instructions, hidden reasoning, diagnostics, credentials, and private session URLs are omitted.
+
+Verdict: **PASS**.
+
+### 5. Find the strongest apparent contradiction, stale instruction, or broken lineage elsewhere in the repository. Resolve it using the authority order or report it as a blocker.
+
+The strongest apparent contradiction is inside the historical body of `20260729-principal-io-uses-runtime-gateways.md`: it permits an in-process Effect gateway and behavior to share scoped state and its `Consequences` still describes code peers as `effectRuntime({ build })` policies. That conflicts with the new requirement that every Kubernetes roster agent run in its own application container and that arbitrary Effect values/shared state do not cross the process boundary.
+
+It is resolved by the authoritative lineage:
+
+1. That ADR’s frontmatter is `partially-superseded`.
+2. Its visible `Supersession` section says the host-bound `AgentRuntime.acquire` and `effectRuntime({ build })` realization is replaced.
+3. It explicitly classifies later historical statements requiring in-process/shared state as descriptions of the replaced host implementation.
+4. The accepted replacement defines per-runtime application entrypoints and controller bridges while retaining exact gateway types and the ban on a universal protocol.
+5. `packages/simulator/AGENTS.md` repeats the corrected binding rule.
+6. Current host code and examples are visibly labeled transitional and are removed only after replacement acceptance evidence exists.
+
+A second apparent conflict is that v2 assigns platform acquisition to `testbed`, while this main decision assigns Kubernetes integration to `packages/simulator`. Root branch law, `20260729-v2-authority-lives-with-v2.md`, and the candidate’s scope resolve it: the new decision governs v1 on main only and does not amend v2.
+
+I found no broken supersession link, missing normative owner, or unresolved authority conflict.
+
+Verdict: **PASS**.
+
+### 6. Could a teammate implement the decision without chat or guessing? List every missing link or unresolved choice and classify each as a deliberate deferral or an accidental gap.
+
+Yes. A teammate can implement the observable contract without chat. The public facade, ownership boundaries, lifecycle ordering, exact-gateway invariant, failure behavior, transition rule, and acceptance evidence are discoverable in the repository.
+
+Deliberate private implementation choices:
+
+- exact local/GKE Layer constructor names and the smallest private platform service shape;
+- module/file placement for private platform/controller code within `packages/simulator`;
+- each runtime’s fixed bridge transport and schema;
+- experiment bundle transport, cache, and checksums;
+- exact Kubernetes, Kueue, Agent Sandbox, Temporal, Helm, Terraform, and provider versions/APIs;
+- timeouts, storage mechanics, and cost budgets;
+- concrete Secret-provider integration and non-exhaustive NetworkPolicy details.
+
+Deliberate deferrals/non-goals:
+
+- production Temporal hosting and HA;
+- router HA;
+- generation ids/streams and restart/rebind/rejoin/replacement/recovery;
+- replay/resume and exactly-once external effects;
+- durable artifact authority, start-or-attach database, global execution-id namespace, synthetic UUID/name hashing rules;
+- a new general serialization grammar;
+- public Kubernetes objects or arbitrary Pod templates;
+- universal gateway proxy/protocol/correlation model;
+- warm pools, multi-run scheduling, fairness, borrowing, preemption, and autoscaling;
+- qualification above ten agents;
+- Nomad, Slurm, managed batch, or GKE Autopilot;
+- persistent agent-state recovery and a general multi-tenant security platform;
+- all v2 implementation or contract changes.
+
+Customer-owned choices retained from earlier decisions:
+
+- experiment completion policy;
+- post-dispatch reaction to runtime termination;
+- application-level retry/idempotency;
+- case/scenario/sweep/grading/report policy;
+- runtime-specific gateway semantics and evidence correlation.
+
+Explicit provenance gaps, not implementation gaps:
+
+- missing native locators and metadata for the late simplification and acceptance exchange;
+- no human selection of constructor spellings, bridge transport, or other private mechanisms;
+- no independently stated rationale for every mechanism.
+
+Accidental implementation or lineage gaps found: **none**.
+
+Verdict: **PASS**.
+
+## Per-question verdicts
+
+| Question | Verdict | Blocker |
+|---|---|---|
+| 1 | PASS | None |
+| 2 | PASS | None |
+| 3 | PASS | None |
+| 4 | PASS | None |
+| 5 | PASS | None |
+| 6 | PASS | None |
+
+## Blockers
+
+None.
+
+## Overall result
+
+**PASS**
+
+All six answers were discoverable from the candidate repository with consistent status, supersession lineage, branch authority, assumptions, normative ownership, and source-event attribution. Maintainer acceptance remains required; this reviewer result is not self-certifying.
+
+## Maintainer acceptance
+
+After this passing result was recorded, Tapan Chugh replied exactly:
+
+> accept
+
+The live continuation available on 2026-08-03 supplies no native message
+locator or exact timestamp, so neither is invented. This accepts the passing
+blind-review result for candidate commit
+`2749adbd99eaffd16f063a45de7be01c253f7ef1`; it does not change the reviewed
+ADR, add rationale, or authorize mechanics outside that accepted decision.
diff --git a/docs/decision-evidence/20260806-main-kubernetes-society-execution-third-cold-review.md b/docs/decision-evidence/20260806-main-kubernetes-society-execution-third-cold-review.md
new file mode 100644
index 000000000..749c4e739
--- /dev/null
+++ b/docs/decision-evidence/20260806-main-kubernetes-society-execution-third-cold-review.md
@@ -0,0 +1,193 @@
+# Blind teammate review — Kubernetes society execution, candidate `78ff2f94`
+
+Non-normative evidence. This record is a quarantined input for later blind
+reviews: a future reviewer must not open it during a run.
+
+## Candidate identity
+
+- Repository root: `/home/tapanc/moltzap-pr-917-main`
+- Branch: `impl/917-main-local-society`
+- Commit: `78ff2f9469040d81eae022d72eca5c869995e878`
+- Tree: `b381119471443baaf94922be256046b8527c27d7`
+- Working tree clean at freeze.
+
+## Why a new candidate was frozen
+
+The accepted blind review covers candidate `2749adbd`. Commit `089829c7`
+amended the admitted record after that review, changing binding text in
+`Decision Outcome`:
+
+- `ten agents` to `four agents`, in the acceptance gate and in the non-goals.
+- `infrastructure` to `cluster`, the `RunSpec` field name, in two passages.
+
+The agent law requires a new candidate and a different fresh reviewer after any
+semantic change to an admitted decision. A changed acceptance criterion and a
+renamed contract field are semantic.
+
+## Reviewer identity and isolation attestation
+
+A fresh agent session with no inherited conversation, compaction, memory, or
+private state, and no earlier blind-review output. It received only the
+candidate repository root and the six fixed questions. It was given no design
+summary, no diff tour, no ADR or file pointer, no search term, and no expected
+answer. No question was answered and no hint was given during the run.
+
+The reviewer attests that it did not open, read, or grep the contents of any
+`*-cold-review.md` or invalid-review record, and that those paths appeared only
+in directory listings and `git log --name-status` output.
+
+The reviewer disclosed one porousness in the quarantine: the permitted
+trajectory restates prior blind-review verdicts. It reports that this supplied
+none of its findings, all of which post-date both prior reviews.
+
+## Duration and interventions
+
+One uninterrupted fresh-agent context, roughly 25 minutes. No author
+intervention. No file was modified. `Not discoverable` was not needed for any
+question.
+
+## Exact prompt
+
+The reviewer received the candidate repository root, the quarantine constraint
+above, and the six questions verbatim from the agent law's blind review gate,
+followed by instructions to give a per-question PASS or FAIL verdict, to record
+independently discovered paths and its discovery trail, and to close with an
+overall result.
+
+## Per-question verdicts
+
+| Question | Verdict |
+| --- | --- |
+| 1 — what decision is current, what is binding | PASS |
+| 2 — what it replaces, retains, where the contract lives | PASS |
+| 3 — what an implementer must do, under which assumptions | PASS |
+| 4 — decision-makers and cited source events | FAIL |
+| 5 — strongest contradiction elsewhere | FAIL |
+| 6 — implementable without chat or guessing | FAIL |
+
+## Overall result
+
+**FAIL.** The gate blocks landing.
+
+## Blockers
+
+### The amended text has no receipt and contradicts its own ledger
+
+Two statements binding at this candidate cite no source event, and the retained
+events say the opposite:
+
+- The trajectory's own source-gap paragraph states that the example's
+ `infrastructure` value "remains the binding shape". The record now names the
+ field `cluster`.
+- The only retained human statement on cohort size is `lets get to 10 agents
+ first and then scale`, and the accepted final-shape prompt says
+ `Two-agent, ten-agent, and all 32 OpenClaw/NanoClaw evaluation runs`. The
+ record now requires a four-agent run.
+
+Both edits landed in `089829c7` with no `Record changelog` row, no dated
+trajectory correction, and no supersession. The commit message states the
+amendment "still owes its blind teammate review gate" and that it was committed
+with `--no-verify`. `checkChangelogRow` runs only in `--staged` mode, so nothing
+caught the missing receipt afterwards.
+
+### The acceptance cohort size is stated three ways
+
+| Source | Says |
+| --- | --- |
+| The admitted record, binding | four-agent |
+| The trajectory, evidence | ten agents |
+| `packages/simulator/local/README.md` | ten-agent, never four |
+| `packages/simulator/package.json`, `local/profile.test.mjs` | ten-agent, asserted as exactly ten roster entries |
+| `packages/simulator/local/four-agent-smoke.mjs` | exists, referenced by nothing |
+
+Authority order does not repair this. The record outranks the profile
+documentation and tooling, but the source above the record forbids the way the
+four-agent text arrived, so the higher authority does not bless the newer text
+while the lower artifacts still implement the older one.
+
+## Accidental gaps the reviewer records
+
+1. Which cohort-size gate binds. Blocking.
+2. No `Record changelog` receipt for either in-place amendment.
+3. The record's illustrative snippet spells `export default RunSpec.define`,
+ while the controller admits only one named `runSpec` export and the
+ orientation docs say the same. An implementer copying the snippet fails at
+ module load.
+4. `autoscaling` sits unscoped in the non-goals beside fairness, borrowing, and
+ preemption, while the GKE profile ships a node-pool autoscaler and the
+ changelog describes agents that scale on demand. Resolvable only by reading
+ the non-goal as run scheduling rather than node pools, a distinction the
+ record never draws.
+5. `packages/simulator/local/hundred-agent-soak.mjs` is referenced by no
+ record, document, or target.
+6. Acceptance evidence has no stated location. Removal of the transitional path
+ is conditioned on replacement evidence existing, and that removal has already
+ happened at this candidate, but the record never says where the evidence must
+ live.
+
+## Deliberate deferrals the reviewer confirms
+
+Production Temporal hosting and high availability; generations, restart, rebind,
+rejoin, and recovery APIs; replay, resume, and exactly-once external effects;
+artifact authority, start-or-attach database, execution-id namespace, and
+name-hashing algorithm; new serialization grammars; a public Kubernetes object
+model and per-agent workflows; Nomad, Slurm, managed batch, and GKE Autopilot;
+scale beyond the small gates; secret protocols, persistent-state recovery,
+NetworkPolicy, and multi-tenancy; the bridge transport and wire schema; Effect
+Layer constructor names; anything under `v2/*`.
+
+## Independently discovered paths and headings
+
+`docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md`
+(Scope and authority; Decision Outcome and its six subsections; Non-goals;
+Current owners and earlier outcomes; Consequences);
+`docs/decisions/20260727-code-first-simulator-kernel.md` (Supersession);
+`docs/decisions/20260729-principal-io-uses-runtime-gateways.md` (Supersession);
+`docs/decisions/README.md` (Canonical reading guidance; Records);
+`docs/decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md`
+and its source-gap list; `docs/decision-evidence/README.md`;
+`.claude/skills/decisions/SKILL.md` (Shape; Point corrections versus
+supersession; Landing; Blind review gate); `AGENTS.md` (Decisions; Docs);
+`v2/AGENTS.md` (Authority and reading order);
+`scripts/docs/adr/check-shape.ts` and its `checkChangelogRow`;
+`packages/simulator/src/definition.ts`;
+`packages/simulator/src/cluster/controller/main.ts`;
+`packages/simulator/local/README.md`, `local/profile.test.mjs`, and the
+two-, four-, ten-agent and hundred-agent modules;
+`packages/simulator/gke/README.md`, `cluster.sh`, `terraform/`, `helm/`;
+`docs/simulator/running.mdx`; `CHANGELOG.md`.
+
+## Discovery trail
+
+`git log` and `git status` at HEAD; `ls docs/`, `ls docs/decisions/`,
+`ls docs/decision-evidence/`; `git diff --stat origin/main...HEAD -- docs/` to
+isolate the candidate; the new record read in full; the diff of the two amended
+records and the index; the trajectory read in full; `AGENTS.md`; the decisions
+skill and the evidence README for the governing procedure;
+`scripts/docs/adr/check-shape.ts`, observing that `checkChangelogRow` is
+`--staged`-only; the shape checker run, reporting fifty well-formed records;
+`git log --follow` on the record surfacing `089829c7`; `git show 089829c7`
+exposing both amendments and the unpaid-gate admission; a quarantine-filtered
+repository-wide search for the cohort-size strings; the simulator's definition,
+index, and controller entry for the implemented contract; the local and GKE
+profile listings, READMEs, package manifest, and profile test; the Terraform
+main and the changelog for the autoscaling and hundred-agent conflicts;
+`git cat-file` and `git ls-tree` against `a2b55f32` to verify the cross-branch
+evidence locators; and the v2 authority record and `v2/AGENTS.md` for the
+authority order.
+
+## Acceptance
+
+Superseded by a later review and then overridden.
+
+A fourth review of candidate `335d8cac` passed questions one, two, three, five,
+and six, and failed question four: a dated correction carried a binding change
+on an unattributed human acceptance. That was corrected at `79e4af96` by
+retaining the literal reply, stating what was searched and when, and restoring
+the scale-claim non-goals no source event addressed.
+
+Tapan Chugh then overrode the remaining gate and directed that landing proceed
+without a further passing review. The blockers this record names are resolved in
+the candidate; the override covers the requirement for a fresh reviewer to
+confirm it, not the findings themselves. Recorded here because an override is a
+maintainer decision the log should carry, not an absence.
diff --git a/docs/decisions/20260727-code-first-simulator-kernel.md b/docs/decisions/20260727-code-first-simulator-kernel.md
index c717b8afc..a26543aed 100644
--- a/docs/decisions/20260727-code-first-simulator-kernel.md
+++ b/docs/decisions/20260727-code-first-simulator-kernel.md
@@ -2,7 +2,7 @@
status: partially-superseded
date: 2026-07-27
decision-makers: Tapan Chugh
-superseded-by: 20260729-principal-io-uses-runtime-gateways.md
+superseded-by: 20260801-main-simulator-runs-container-societies-on-kubernetes.md
---
# The simulator is code-first with a closed event catalog
@@ -11,41 +11,34 @@ Decision provenance: [stored code-first simulator trajectory](../decision-eviden
## Supersession
-The following scope remains current: the code-first TypeScript/Effect
-approach; `Simulator.define`; an immutable closed typed EventCatalog;
-the typed run-evidence RunLedger; a scoped runtime roster and lifecycle
-kernel; Effect programs and services; customer-owned
-scenario languages, sweeps, completion policy, and graders; and the
-requirement that OpenClaw, NanoClaw, Effect, and custom runtimes use one
-public stack without callback shortcuts.
-
-For the v1 implementation, `20260729-principal-io-uses-runtime-gateways.md`
-replaces the private-gateway and router-authentication readiness claims.
-Successful acquisition exposes each runtime's exact principal gateway and
-termination through the keyed roster alongside the router-issued agent handle.
-Network identity remains distinct from runtime lifetime. A behavioral runtime
-is ready only when its principal gateway and configured MoltZap capabilities
-are usable. Experiment-controlled endpoints remain network participants for
-probes and workloads, but do not represent a principal instructing an
-autonomous agent. Synthetic-endpoint OpenClaw and NanoClaw runs are network
-diagnostics rather than behavioral acceptance.
-The earlier three-entry-point v1 package list is also replaced: the root
-remains the society definition, execution, and evidence surface, while runtime
-contracts and shipped implementations are grouped at
-`@moltzap/simulator/runtime` inside the same package. The current v1 boundary
-lives in
-[`20260729-principal-io-uses-runtime-gateways.md`](./20260729-principal-io-uses-runtime-gateways.md).
+The following scope remains current for main: the code-first TypeScript/Effect
+approach; an immutable closed typed EventCatalog; the typed run-evidence
+RunLedger and producer-bound writers; exact keyed runtime gateways;
+customer-owned scenario languages, sweeps, completion policy, and graders; one
+`@moltzap/simulator` package; the production v1 router and protocol; and one
+public stack without social callback shortcuts.
+
+[`20260801-main-simulator-runs-container-societies-on-kubernetes.md`](./20260801-main-simulator-runs-container-societies-on-kubernetes.md)
+replaces the main/v1 `simulator.define(...).run(...)` public naming and its
+host-only concrete execution path, including host-local `AgentRuntime.acquire`
+and `effectRuntime({ build })` acquisition, with one `RunSpec`, one
+`Run.execute`, and one Kubernetes path supplied by either a local-cluster or
+GKE Effect Layer. The existing event, ledger, network, exact-gateway acquired
+shape, termination-policy, and customer-program concepts are reused rather
+than replaced.
+
+[`20260729-principal-io-uses-runtime-gateways.md`](./20260729-principal-io-uses-runtime-gateways.md),
+as partially superseded, continues to govern the distinction between
+principal-native gateway control and MoltZap social traffic, the absence of a
+universal gateway union or correlation id, and the classification of
+controlled-endpoint traffic as diagnostic rather than behavioral acceptance.
`20260728-simulator-is-the-system-driver.md` replaces the historical
-single-package ownership and source-layout plan with the V2 simulator
-as a system driver over public production capabilities and a separate
-testbed package. `20260729-router-order-is-opaque.md` replaces
-simulator-owned production Router state, public RouterSequence, and
-legacy transport-facing types with the `router` package's opaque,
-volatile L2 capability. `20260728-six-deep-packages-one-version.md`,
-as partially superseded, and `docs/spec/layer-interfaces.md` own the
-current package boundary. The accepted
-`20260728-simulator-is-the-system-driver.md` record remains unchanged.
+single-package ownership and source-layout plan only for v2. The accepted v2
+record, the Gate 1 manifest, and the v2 package/specification boundary remain
+unchanged. `20260729-router-order-is-opaque.md` continues to replace
+simulator-owned production Router state and public RouterSequence in its v2
+scope.
## Context and Problem Statement
diff --git a/docs/decisions/20260729-principal-io-uses-runtime-gateways.md b/docs/decisions/20260729-principal-io-uses-runtime-gateways.md
index 5db88e31d..9b4ad2e87 100644
--- a/docs/decisions/20260729-principal-io-uses-runtime-gateways.md
+++ b/docs/decisions/20260729-principal-io-uses-runtime-gateways.md
@@ -1,7 +1,8 @@
---
-status: accepted
+status: partially-superseded
date: 2026-07-29
decision-makers: Tapan Chugh
+superseded-by: 20260801-main-simulator-runs-container-societies-on-kubernetes.md
---
# Principal I/O uses runtime-native gateways
@@ -9,6 +10,33 @@ decision-makers: Tapan Chugh
Decision provenance: [stored principal-gateway
trajectory](../decision-evidence/20260729-principal-runtime-gateway-trajectory.md#principal-io-uses-each-runtime-gateway).
+## Supersession
+
+The following scope remains current: principal control uses each runtime's
+exact native gateway; MoltZap carries agent-produced social traffic; code and
+process agents receive no social shortcut; the simulator defines no universal
+gateway union, command language, correlation model, or gateway semantics;
+gateway and router evidence remain distinct; runtime termination remains
+evidence interpreted by customer policy; and the behavioral-evaluation
+contract below remains current.
+
+[`20260801-main-simulator-runs-container-societies-on-kubernetes.md`](./20260801-main-simulator-runs-container-societies-on-kubernetes.md)
+replaces only the host-bound acquisition and code-peer realization on the
+current main simulator path. `AgentRuntime.acquire` and
+`effectRuntime({ build })` closures with shared in-process gateway/behavior
+state are transitional host implementations, not the Kubernetes runtime
+boundary. Each current runtime instead owns a container entrypoint and a
+runtime-specific controller bridge that returns the same exact gateway and
+termination shape after readiness. Code-peer policy runs inside its own agent
+container. Arbitrary Effect values are not serialized, and the replacement
+does not introduce a generic cross-runtime proxy protocol.
+
+Historical statements below that require an in-process Effect API or shared
+scoped state describe the replaced host implementation. The current
+distributed runtime contract lives in the replacement record; all other
+gateway, evidence, evaluation, and v2 boundaries in this record remain
+current.
+
Scope: this record governs the Phase 1 source baseline in
`packages/simulator`, the private `packages/evals` application, and the
mechanical `replyToId` removal across the v1 protocol, server, client, and
diff --git a/docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md b/docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md
new file mode 100644
index 000000000..18dae1919
--- /dev/null
+++ b/docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md
@@ -0,0 +1,294 @@
+---
+status: accepted
+date: 2026-08-01
+decision-makers: Tapan Chugh
+---
+
+# The main simulator runs container societies on Kubernetes
+
+Decision provenance: [stored main-track trajectory](../decision-evidence/20260801-main-kubernetes-society-execution-trajectory.md#main-simulator-runs-container-societies-on-kubernetes), with the retained [code-first simulator](../decision-evidence/20260727-code-first-simulator-trajectory.md#code-first-simulator-closed-event-catalog) and [principal-gateway](../decision-evidence/20260729-principal-runtime-gateway-trajectory.md#principal-io-uses-each-runtime-gateway) trajectories.
+
+## Scope and authority
+
+This decision governs the production v1 simulator on `main`, implemented in
+`packages/simulator`, and the way `packages/evals` executes experiments through
+that simulator. It does not change `v2/*`, the v2 package map, or any v2
+normative contract.
+
+The checked-in source-event trajectories are the requirements boundary for
+this slice. The outcome below contains only choices made in those conversations
+or the minimum mechanics required to connect them. Anything else is a
+non-goal, listed explicitly below.
+
+## Context and Problem Statement
+
+The v1 simulator already provides code-first Effect programs, a closed typed
+event catalog, mixed runtime rosters, runtime-native principal gateways, one
+production router, and a durable run ledger. Its concrete host Layer starts
+local processes and Docker containers. A separate example proved that two
+OpenClaw containers can join the original simulator, but an example-only
+Docker path is not the core simulator and cannot exercise the requested
+Kubernetes cohort.
+
+Experiments need one core path that can run the same society on a local
+Kubernetes cluster or GKE. The selected stack is Kubernetes, Kueue, Agent
+Sandbox, and Temporal. The first useful proof is a small complete society,
+then a larger cohort and real evaluations; the earlier 1,000–10,000-agent goal
+is deferred until that path works.
+
+## Decision Outcome
+
+### The public model is `RunSpec` and `Run.execute`
+
+An experiment exports one code-first `RunSpec`. It declares the versioned
+definition id, closed customer event catalogs, exact keyed runtime roster, and
+the customer `execute` Effect. `Run.execute(spec)` is the only new execution
+entry point.
+
+```ts
+export const runSpec = RunSpec.define({
+ id: "acme.echo/v1",
+ events: [echoEvents],
+ agents: { alice, bob },
+ cluster: localKubernetes,
+ execute: ({ agents, events, network, ledger }) =>
+ Effect.gen(function* () {
+ // Instruct agents through their native gateways, observe the society,
+ // and return when this experiment is complete.
+ }),
+});
+```
+
+The example receives already-constructed runtime descriptors and an Effect
+Layer. It does not select new constructor names for either one.
+
+The `cluster` field contains either the local-Kubernetes or GKE Effect Layer.
+It selects the host without exposing Kubernetes, Kueue, Agent Sandbox, or
+Temporal objects to the roster or customer Effect. Moving a society between
+profiles changes that Layer, not its agents, events, or `execute` program.
+
+This is a small facade over the existing simulator concepts, not a second
+simulation model. The existing closed event catalog, typed ledger, exact keyed
+gateway roster, network capabilities, Effect failure model, and customer-owned
+completion policy remain current. Runtime-specific gateway types remain exact;
+the simulator does not add a universal gateway union.
+
+The old `simulator.define(...).run(...)` host entry point is transitional. It
+is removed after `packages/evals` and the local/GKE acceptance runs use
+`RunSpec` and `Run.execute`. There is no supported Docker execution backend or
+compatibility facade after cutover. Docker may still build images and support a
+local Kubernetes cluster.
+
+### Container runtimes preserve exact native gateways
+
+On the Kubernetes path, every roster value is a container runtime descriptor.
+It preserves the runtime's exact `Gateway` type while privately owning two
+runtime-specific pieces: the portable application-container entrypoint and a
+controller-side bridge. After the Sandbox application is ready, that bridge
+attaches to the runtime and returns the existing `RunningAgent` shape:
+the exact gateway plus termination observation. Only then may the slot satisfy
+the cohort gate and become a `StartedAgent` for the customer Effect.
+
+Arbitrary JavaScript gateway values, Effect closures, and shared in-process
+state do not cross the container boundary. Each runtime implementation owns
+both ends of its bridge and may use its own fixed internal transport. The
+simulator defines no universal command, request, response, correlation,
+session, or model-configuration protocol and does not normalize gateway types.
+The kernel knows only the generic acquired shape it already consumes.
+
+For evaluation code peers, this replaces the host-only
+`effectRuntime({ build })` realization on the Kubernetes path. The peer policy
+runs as the application entrypoint in that peer's Sandbox container, and
+`packages/evals` owns the peer-specific observation bridge and its exact
+gateway adapter. Peer social behavior still uses the production MoltZap
+client and router. The in-process Effect runtime remains transitional host
+code until cutover; no public `scriptedRuntime` constructor or generic
+scripted-agent protocol is introduced.
+
+### One execution is one experiment society
+
+Each call creates one society for one customer Effect and then tears it down:
+
+1. Temporal starts one coarse workflow for the run.
+2. Kueue admits capacity for the complete roster.
+3. The controller creates one Agent Sandbox with one application container for
+ each roster entry.
+4. Each runtime-specific controller bridge attaches, and the controller waits
+ until the exact roster is ready at the same cohort gate.
+5. The in-cluster controller invokes the `execute` Effect once.
+6. The existing simulator ledger and run outcome retain the experiment and
+ infrastructure evidence.
+7. Temporal drives cleanup of the run-owned Kubernetes resources.
+
+The society is not a warm pool and is not reused by another experiment.
+Kueue owns capacity admission; it does not decide simulator readiness.
+Kubernetes and Agent Sandbox own container placement and lifecycle; they do
+not run customer policy. The controller owns the exact readiness gate,
+customer Effect, and simulator evidence. Temporal owns the coarse operational
+lifecycle and cleanup; it does not run agent logic, append simulator evidence,
+or replay the customer Effect.
+
+One roster entry means one logical agent in one Agent Sandbox application
+container. Infrastructure containers are not agents. Real agents and
+code/scripted agents may share one society, but every agent's social traffic
+uses the production MoltZap router. The experiment controls an agent through
+that runtime's native principal gateway and does not impersonate an agent with
+a synthetic MoltZap participant.
+
+The controller uses a stable simulator image and loads the experiment module
+late, so changing an experiment does not require building a new agent image.
+The stock digest-pinned OpenClaw image is the compatibility baseline; a
+prebuilt MoltZap image may only be an optimization. The exact bundle transport
+and cache are private profile details, not a public artifact protocol.
+
+### Failure and evidence retain the existing simulator semantics
+
+Dispatch requires the complete roster to be ready together. A backing Pod
+restart before dispatch simply keeps that slot outside the gate until its
+current application and controller bridge are usable; no generation API is
+exposed. An unrecoverable or never-ready agent or bridge fails acquisition and
+starts cleanup. After dispatch, runtime termination remains typed ledger
+evidence and the customer Effect's existing policy decides whether to finish,
+fail, or keep observing the run.
+
+The controller invokes `execute` once for a run and never automatically
+replays it. Controller loss or infrastructure failure fails the run and starts
+cleanup. This is not an exactly-once guarantee for external side effects;
+customer code owns any application-level retry or idempotency it needs.
+
+The run returns the same kind of program `Exit` and completed-ledger receipt
+already owned by the simulator. Infrastructure failure uses the existing
+infrastructure-outcome model. Temporal history and Kubernetes status are
+operational observations, not replacements for the simulator ledger.
+
+### Local and GKE are two profiles of one path
+
+The repository owns one local Kubernetes profile for development and CI and
+one GKE profile for cloud qualification. Both install or connect to the same
+required components and invoke the same `Run.execute` path. A small
+repository-local CLI accepts a RunSpec entrypoint and calls that same library
+path; it does not define a separate execution protocol.
+
+The local profile uses a repository-owned local cluster and a development
+Temporal deployment. The GKE reference is regional GKE Standard and uses
+Agent Sandbox. Terraform and Helm own reproducible GKE and add-on setup.
+Production Temporal hosting and high availability remain deliberately
+unselected; GKE qualification may use a test deployment or a configured
+Temporal endpoint.
+
+The Kubernetes implementation stays behind the existing Effect Layer
+boundary. That boundary is sufficient for a possible future scheduler; this
+slice does not implement Nomad, Slurm, or another backend.
+
+### Acceptance is experiment evidence, not platform completeness
+
+The slice is complete only when all of the following use the core
+`packages/simulator` path:
+
+- unit tests with a private fake platform prove cohort-gate ordering, one
+ customer-Effect invocation, post-dispatch termination policy, outcomes, and
+ cleanup;
+- a local-cluster two-agent smoke proves Kueue admission, one Sandbox/container
+ per agent, native gateway readiness, execution, ledger evidence, and zero
+ run-owned residue;
+- one end-to-end experiment, sized by its run rather than by its source, proves
+ the same complete-roster path at larger cohorts before any scale claim;
+- all 32 OpenClaw/NanoClaw evaluation cells invoke `Run.execute` through
+ Kubernetes and record their real outcomes, including honest operational or
+ behavioral failures rather than forced passes;
+- the same small smoke and at least one OpenClaw evaluation run on GKE through
+ the same authoring contract; and
+- the transitional Docker example and host execution path are removed only
+ after the replacement evidence exists.
+
+### Non-goals
+
+The following are not part of this decision or its first implementation:
+
+- generation identifiers or streams, a customer-visible restart/recovery API,
+ or post-dispatch replacement, rebinding, rejoin, and recovery of in-flight
+ work;
+- replay or resume of the customer Effect, exactly-once external effects, or a
+ customer-visible distributed transaction protocol;
+- a durable artifact authority, start-or-attach binding database, global
+ execution-id namespace, synthetic UUID scheme, or normative Kubernetes-name
+ hashing algorithm;
+- a new immutable-data grammar, JCS contract, universal input/result/failure
+ schema, or serialization rules beyond the simulator's existing schemas and
+ the fixed runtime-specific bridge schemas and checksums needed to move an
+ experiment module or pinned image;
+- a public Kubernetes object model, arbitrary Pod templates, per-agent
+ Temporal workflows, or simulator APIs for Kueue, Sandbox, or Temporal
+ internals;
+- a universal gateway proxy, command language, actor mailbox, cross-runtime
+ correlation model, or serialization of arbitrary JavaScript/Effect values;
+- warm societies, multi-run scheduling policy, fairness, borrowing, preemption,
+ simulator-owned autoscaling of a run's cohort, router high availability, or
+ production Temporal high availability. A profile may let its node pool
+ autoscale, which is the cluster's own capacity mechanism and the simpler one
+ to operate;
+- a 100-, 1,000-, 5,000-, or 10,000-agent qualification claim before the
+ two-agent and larger-cohort gates pass;
+- a Nomad, Slurm, managed-batch, or GKE Autopilot implementation;
+- exact Secret-provider protocols, persistent-agent-state recovery, exhaustive
+ NetworkPolicy design, or a general multi-tenant security platform; and
+- any implementation or contract change under `v2/*`.
+
+### Current owners and earlier outcomes
+
+`packages/simulator` owns `RunSpec`, `Run.execute`, the private Kubernetes
+implementation, profile assets, controller, and its use of Kueue, Agent
+Sandbox, and Temporal. `packages/evals` continues to own cases, runtime
+conditions, grading, reports, resume policy, and Phoenix publication. It is a
+consumer, not a second execution platform.
+
+[`20260727-code-first-simulator-kernel.md`](./20260727-code-first-simulator-kernel.md)
+remains current for its code-first Effect model, closed typed event catalog,
+typed ledger, runtime roster, customer-owned scenario/sweep/completion/grading
+policy, and single-package boundary. This decision replaces only the v1
+`simulator.define(...).run(...)` public naming and the host-only concrete
+execution path.
+
+[`20260729-principal-io-uses-runtime-gateways.md`](./20260729-principal-io-uses-runtime-gateways.md)
+remains current for exact runtime-native gateway types, agent social traffic,
+termination policy, mixed societies, and behavioral-evaluation evidence. This
+decision replaces only its host-bound realization of code agents as
+`effectRuntime({ build })` closures sharing in-process state with their
+gateway. Container runtime implementations now own runtime-specific bridges;
+the ban on a simulator-wide gateway union or generic command protocol remains.
+
+[`20260729-effect-native-evaluation-results.md`](./20260729-effect-native-evaluation-results.md)
+remains current for cases, grading, report resume, SQLite, and Phoenix. This
+decision changes where an evaluation run executes, not how evaluation truth is
+defined or published.
+
+The distributed-execution ADRs on the v2 branch remain v2 authority. Their
+checked-in source trajectories inform this main-track decision, but their v2
+process map, package ownership, generation model, and trust contracts are not
+copied into v1.
+
+## Consequences
+
+Experiment authors get one small code-first contract and one execution path
+from laptop-scale Kubernetes to GKE. The core simulator, rather than an
+example, owns container-society execution. The strict cohort gate and
+one-container-per-agent boundary match the experiment requirements without
+turning the simulator into a general execution platform.
+
+The design accepts startup latency and a stable controller/bundle mechanism in
+exchange for avoiding per-experiment agent images. It also accepts that a
+controller or agent failure may end a run; automatic recovery is intentionally
+outside the first experiment-infrastructure slice.
+
+## Record changelog
+
+Point corrections that leave the Decision Outcome intact. A change that alters
+the outcome is a supersession, not a row here.
+
+| Date | Change |
+|---|---|
+| 2026-08-06 | Renamed the `RunSpec` field `infrastructure` to `cluster`, matching the implementation and the orientation docs. |
+| 2026-08-06 | Replaced the fixed four-agent acceptance gate with one end-to-end experiment sized by its run. Removes the earlier ten- and four-agent wording, which the record, the ledger, and the profile tooling had never agreed on. The scale-claim non-goals are unchanged: no source event addresses them. |
+| 2026-08-06 | Corrected the stale subpath in the simulator overview from `/runtime` to `/agents`, the export the package actually publishes. |
+| 2026-08-06 | Corrected the illustrative snippet from `export default` to the named `runSpec` export the controller admits. |
+| 2026-08-06 | Scoped the `autoscaling` non-goal to a run's cohort. A profile's node pool may autoscale; it was selected because it is the simpler thing to operate. |
diff --git a/docs/decisions/README.md b/docs/decisions/README.md
index 30ff3b6b7..71faded14 100644
--- a/docs/decisions/README.md
+++ b/docs/decisions/README.md
@@ -52,7 +52,8 @@ planning database as continuing authority.
| Decision | Date | Status | Superseded by |
|---|---|---|---|
-| [Principal I/O uses runtime-native gateways](20260729-principal-io-uses-runtime-gateways.md) | 2026-07-29 | accepted | — |
+| [The main simulator runs container societies on Kubernetes](20260801-main-simulator-runs-container-societies-on-kubernetes.md) | 2026-08-01 | accepted | — |
+| [Principal I/O uses runtime-native gateways](20260729-principal-io-uses-runtime-gateways.md) | 2026-07-29 | partially-superseded | [Main Kubernetes society execution](20260801-main-simulator-runs-container-societies-on-kubernetes.md) |
| [Evaluation runs produce typed reports published to Phoenix](20260729-effect-native-evaluation-results.md) | 2026-07-29 | partially-superseded | [Principal runtime gateways](20260729-principal-io-uses-runtime-gateways.md) |
| [Representation limits are fixed or derived](20260729-representation-limits-are-fixed-or-derived.md) | 2026-07-29 | accepted | — |
| [Identity and Router expose deep Effect capabilities](20260729-identity-and-router-expose-deep-effect-capabilities.md) | 2026-07-29 | accepted | — |
@@ -73,7 +74,7 @@ planning database as continuing authority.
| [The model surface is start_conversation, reply, and listen](20260728-model-surface-is-start-reply-listen.md) | 2026-07-28 | accepted | — |
| [V2 has six deep packages and one Moltzap version](20260728-six-deep-packages-one-version.md) | 2026-07-28 | partially-superseded | [Opaque Router order](20260729-router-order-is-opaque.md) |
| [V2 owns one simulator as the system driver](20260728-simulator-is-the-system-driver.md) | 2026-07-28 | accepted | — |
-| [The simulator is code-first with a closed event catalog](20260727-code-first-simulator-kernel.md) | 2026-07-27 | partially-superseded | [Principal runtime gateways](20260729-principal-io-uses-runtime-gateways.md), [Simulator system driver](20260728-simulator-is-the-system-driver.md), [six packages and one version](20260728-six-deep-packages-one-version.md), [opaque Router order](20260729-router-order-is-opaque.md) |
+| [The simulator is code-first with a closed event catalog](20260727-code-first-simulator-kernel.md) | 2026-07-27 | partially-superseded | [Main Kubernetes society execution](20260801-main-simulator-runs-container-societies-on-kubernetes.md), [principal runtime gateways](20260729-principal-io-uses-runtime-gateways.md), [Simulator system driver](20260728-simulator-is-the-system-driver.md), [six packages and one version](20260728-six-deep-packages-one-version.md), [opaque Router order](20260729-router-order-is-opaque.md) |
| [Registration is out of band; the plane knows one caller](20260727-registration-is-out-of-band.md) | 2026-07-27 | superseded | [Registry bootstrap admission](20260729-registration-is-registry-bootstrap-admission.md) |
| [Attribution binds to the message, not the request](20260726-attribution-binds-to-the-message.md) | 2026-07-26 | partially-superseded | [JCS, JOSE, and AuthenticatedHttp](20260729-identity-uses-jcs-jose-authenticated-http.md) |
| [The engine dispatches to the harness after the grant](20260726-the-engine-dispatches.md) | 2026-07-26 | partially-superseded | [Endpoint daemon](20260728-endpoint-daemon-speaks-modern-mcp.md), [model surface](20260728-model-surface-is-start-reply-listen.md) |
diff --git a/docs/development/eval-add-evaluation.mdx b/docs/development/eval-add-evaluation.mdx
index 9693db5e6..193f1b46f 100644
--- a/docs/development/eval-add-evaluation.mdx
+++ b/docs/development/eval-add-evaluation.mdx
@@ -5,7 +5,9 @@ description: "Add a typed case, exact peer roster, executable policy, criterion,
`packages/evals` is a private, code-first customer of
`@moltzap/simulator`. A bundled case is an immutable TypeScript value with
-the exact autonomous peers and policy it needs.
+the exact autonomous peer definitions and policy it needs. At execution time,
+each definition becomes one Agent Sandbox application container in the cell's
+`RunSpec` roster.
Most additions change `cases.ts`, `grading.ts`, and their tests. Change
`peer.ts` only when the required autonomous network behavior is genuinely new.
@@ -35,19 +37,19 @@ const HONEST_REFUSAL = decodeCriterionId(
Malformed values then fail when the code catalog is loaded, before a simulator
resource or result bundle is allocated.
-## 2. Declare the exact peer runtimes
+## 2. Declare the exact peer definitions
The target runtime belongs to the OpenClaw or NanoClaw condition. The case owns
only the autonomous code peers it needs:
```ts
-type ReviewPeerRuntimes = Readonly<{
- [PEER_AGENT_NAME]: EvaluationPeerRuntime;
+type ReviewPeerDefinitions = Readonly<{
+ [PEER_AGENT_NAME]: EvaluationPeerDefinition;
}>;
function reviewPeers(
caseId: EvaluationCaseId,
-): ReviewPeerRuntimes {
+): ReviewPeerDefinitions {
return {
[PEER_AGENT_NAME]: selectedResponsePeerRuntime(
caseId,
@@ -62,11 +64,16 @@ The keys become the exact keys of `context.peers`. A case with no social peers
uses an empty record. Do not add idle peers to a shared roster; only the
runtimes in this record are started.
-Bundled peer implementations are autonomous `effectRuntime` policies. They
-send and receive through `EffectRuntimeContext.client`, so their social
-traffic traverses the production protocol and router. Their
-`EvaluationPeerGateway` reports a completed exchange to the evaluation
-controller; it is not a command surface.
+Each peer factory returns an image-independent `EvaluationPeerDefinition` with
+a closed application plan. Evaluation execution binds that definition to the
+configured digest-pinned peer image, mounts its bootstrap data, and runs the
+plan through `peer-application.ts → runEvaluationPeerApplication` inside the
+peer's application container.
+
+The application uses its production MoltZap client, so every social send and
+receive traverses the protocol and router. Its peer-specific bridge exposes an
+`EvaluationPeerGateway` that reports a completed exchange to the evaluation
+controller. It is observation-only and cannot command a social action.
## 3. Write a policy that returns one selection
@@ -76,10 +83,10 @@ observation capabilities:
```ts
function reviewProgram(
instruction: string,
-): EvaluationCaseProgram {
+): EvaluationCaseProgram {
return (
context: EvaluationCaseProgramContext<
- ReviewPeerRuntimes,
+ ReviewPeerDefinitions,
Failure
>,
) =>
@@ -174,11 +181,11 @@ Do not turn provider errors, invalid evidence, runtime failure, or model
abstention into a behavioral failure. The report types preserve those states
separately.
-## 5. Add new peer behavior only at the network boundary
+## 5. Add new container peer behavior only at the network boundary
Reuse the focused policies in `peer.ts` when they match:
-| Runtime factory | Autonomous network behavior |
+| Peer factory | Autonomous network behavior |
|---|---|
| `selectedResponsePeerRuntime` | Wait for a target-created conversation, send ordered messages, and observe each target response |
| `contextPeerRuntime` | Perform the same exchange for context that is not selected |
@@ -187,10 +194,12 @@ Reuse the focused policies in `peer.ts` when they match:
| `observerPeerRuntime` | Observe the target's first group message |
| `orderedGroupPeerRuntime` | Wait for a source contribution, ask the target, and observe its response |
-If none fits, add one autonomous policy that uses the production client. Its
-gateway should expose only the smallest observation needed by case execution.
-Do not add a generic queue of commands, a second request protocol, or a direct
-social callback.
+If none fits, add one closed autonomous application plan interpreted inside
+the peer container through the production client. Its bridge gateway should
+expose only the smallest observation needed by case execution. Do not add a
+generic queue of commands, a second request protocol, or a direct social
+callback. Arbitrary Effect closures and gateway objects do not cross the
+container boundary.
The peer's `PeerExchange.observations` are in protocol order. For a selected
exchange, the final observation is the one returned to case policy; test that
@@ -207,7 +216,7 @@ contracts instead of normalizing them:
plus terminal output. Its factory owns the per-attempt native idempotency
sequence and returns `Some(outputEvidenceId)`.
- NanoClaw submits to its owner-local socket, records
- `NanoclawPrincipalInputSent`, and returns `None`. Its output is an
+ `NanoClawPrincipalInputSent`, and returns `None`. Its output is an
uncorrelated multi-frame stream, so the adapter never consumes the next frame
or attributes it to the input.
@@ -280,6 +289,11 @@ ignored local artifacts. Preserve real OpenClaw or NanoClaw failures in the
report; file a reproducible product defect separately instead of changing a
channel to make a case pass.
+The live matrix also requires digest-pinned controller/support, peer, and
+NanoClaw application images plus the selected local or GKE profile. Supplying
+those inputs is not a qualification claim; retain actual startup, execution,
+and grading failures as typed attempt states.
+
## Related
- [Code-first evaluations](/development/evals) — execution, resume, and
diff --git a/docs/development/eval-grading-reference.mdx b/docs/development/eval-grading-reference.mdx
index 0b03af7e3..f699f430a 100644
--- a/docs/development/eval-grading-reference.mdx
+++ b/docs/development/eval-grading-reference.mdx
@@ -4,8 +4,9 @@ description: "How the private evaluation application validates gateway and socia
---
Evaluation grading starts from a completed, definition-validated simulator
-ledger. It never grades a runtime callback return value, a copied response
-string, or an in-process social shortcut.
+ledger retrieved after a local-Kubernetes or GKE cell completes. It never
+grades a runtime callback return value, a copied response string, or a social
+shortcut around the production router.
The ledger is canonical physical evidence. The transcript is an
evaluation-owned normalized projection. A grade is an auditable
diff --git a/docs/development/evals.mdx b/docs/development/evals.mdx
index a9f1e3cee..6d5177c34 100644
--- a/docs/development/evals.mdx
+++ b/docs/development/evals.mdx
@@ -9,14 +9,20 @@ conditions, criteria, and sweeps are ordinary TypeScript and Effect values.
Customers compose the simulator package directly and can build a
domain-specific authoring language around the parameters they need.
+Every matrix cell is one `RunSpec` submitted through the core simulator's
+local-Kubernetes or GKE profile. Each target and each autonomous code peer is a
+separate Agent Sandbox application container. The controller invokes the case
+Effect only after the complete roster and every runtime-specific bridge are
+ready.
+
## One attempt, two interaction boundaries
A successful case path keeps principal control separate from social traffic:
-1. The case contributes an exact keyed record of autonomous Effect peer
- runtimes.
-2. The condition adds one OpenClaw or NanoClaw target to that record and starts
- the mixed roster against the production router.
+1. The case contributes an exact keyed record of autonomous peer definitions.
+2. The condition adds one OpenClaw or NanoClaw target, and execution
+ materializes the peer definitions with the configured digest-pinned
+ application image.
3. Case policy instructs the target through its runtime-native principal
gateway.
4. The target and code peers create and use MoltZap conversations
@@ -67,12 +73,17 @@ needed by that case. A direct exchange starts one peer; a group case starts its
question, source, and observer peers; a principal-only case starts none.
Unused peers are not acquired.
-Each peer is an `effectRuntime({ build })` implementation. Its behavior uses
-`EffectRuntimeContext.client` to resolve agents, open conversations, receive
-messages, and send messages through the production protocol. Its
-`EvaluationPeerGateway` contains only an `exchange` observation. The
-evaluation controller cannot use that gateway to make the peer perform a
-social action.
+Each `peer.ts → EvaluationPeerDefinition` owns a closed application plan and a
+factory that binds it to the configured digest-pinned peer image. The plan is
+mounted into that peer's Sandbox and interpreted by
+`peer-application.ts → runEvaluationPeerApplication`. Its production MoltZap
+client resolves agents, opens conversations, receives messages, and sends
+messages through the router.
+
+The peer-specific controller bridge exposes only the observation Effect on
+`EvaluationPeerGateway`. It cannot command the peer or bypass the production
+network. Arbitrary closures, gateway objects, and shared state do not cross the
+container boundary.
Case programs receive five capabilities:
@@ -159,8 +170,9 @@ The SQLite bundle under `.moltzap/evals/results/` stores:
- typed run, evidence, judge, and ledger-allocation failures.
`results.ts → resumeStoredEvaluationReport` validates every immutable plan
-component before executing only the missing suffix. The report cannot skip or
-reorder a matrix cell.
+component, including the selected profile, images, Temporal address, and ledger
+artifact location, before executing only the missing suffix. The report cannot
+skip, reorder, or silently move a matrix cell.
Live failures remain results. OpenClaw or NanoClaw may fail to start, terminate,
omit required social behavior, time out, produce evidence that grading
@@ -194,20 +206,41 @@ Start or resume a live report:
```bash
OPENAI_API_KEY=... \
+MOLTZAP_CONTROLLER_IMAGE=CONTROLLER_IMAGE_AT_SHA256 \
+MOLTZAP_SUPPORT_IMAGE=PEER_APPLICATION_IMAGE_AT_SHA256 \
+MOLTZAP_NANOCLAW_IMAGE=NANOCLAW_APPLICATION_IMAGE_AT_SHA256 \
+MOLTZAP_LOCAL_ARTIFACTS="$PWD/.moltzap/local-artifacts" \
+MOLTZAP_TEMPORAL_ADDRESS=127.0.0.1:7233 \
mise x node@24.18.0 -- pnpm nx run @moltzap/evals:eval -- \
+ --profile local \
--report-id baseline-2026-07-29 \
--openclaw-model "$OPENCLAW_MODEL" \
--nanoclaw-model "$NANOCLAW_MODEL"
OPENAI_API_KEY=... \
+MOLTZAP_CONTROLLER_IMAGE=CONTROLLER_IMAGE_AT_SHA256 \
+MOLTZAP_SUPPORT_IMAGE=PEER_APPLICATION_IMAGE_AT_SHA256 \
+MOLTZAP_NANOCLAW_IMAGE=NANOCLAW_APPLICATION_IMAGE_AT_SHA256 \
+MOLTZAP_LOCAL_ARTIFACTS="$PWD/.moltzap/local-artifacts" \
+MOLTZAP_TEMPORAL_ADDRESS=127.0.0.1:7233 \
mise x node@24.18.0 -- pnpm nx run @moltzap/evals:resume -- \
+ --profile local \
--report-id baseline-2026-07-29 \
--openclaw-model "$OPENCLAW_MODEL" \
--nanoclaw-model "$NANOCLAW_MODEL"
```
The source worktree must be clean. Both model IDs are required and become part
-of the immutable native runtime configuration.
+of the immutable native runtime configuration. The controller/support, peer,
+and NanoClaw application images must be immutable digest references. Their
+presence is an execution prerequisite, not evidence that the NanoClaw image or
+a live cluster has passed qualification.
+
+For GKE, select `--profile gke`, replace the local artifact root with the
+Terraform-owned `MOLTZAP_GKE_ARTIFACT_BUCKET`, and provide the explicit
+`MOLTZAP_KUBE_CONTEXT` and configured Temporal endpoint. Each profile submits
+the same generated RunSpec module and reads the same relative completed-ledger
+path.
Publish a completed report:
diff --git a/docs/modules/simulator/src.mdx b/docs/modules/simulator/src.mdx
index 77819a785..25c03e176 100644
--- a/docs/modules/simulator/src.mdx
+++ b/docs/modules/simulator/src.mdx
@@ -13,7 +13,7 @@ Code-first simulator API.
## Public surface
-### [`AgentConnection`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/router.ts#L121)
+### [`AgentConnection`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/router.ts#L80)
_Interface_
@@ -159,7 +159,48 @@ export class AgentRuntimeStartFailed extends Schema.TaggedClass {
+ override get message(): string {
+ return this.detail;
+ }
+}
+```
+
+Cluster loss that ends a run without exposing its backend.
+
+### [`ClusterLost`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/execute.ts#L97)
+
+_Class_
+
+```ts
+export class ClusterLost<
+ Definitions extends Readonly>,
+> extends Data.TaggedClass("ClusterLost")<{
+ readonly cause: Cause.Cause>;
+ readonly receipt: LedgerReceipt;
+}> {}
+```
+
+Post-allocation cluster error plus all durable evidence retained.
+
+### [`ClusterServices`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L76)
+
+_TypeAlias_
+
+```ts
+export type ClusterServices = LedgerStorage | RouterProvider | Cluster;
+```
+
+Opaque service set supplied by a local-Kubernetes or GKE Layer.
+
+### [`CompletedLedgerReceipt`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/execute.ts#L64)
_Class_
@@ -175,7 +216,7 @@ export class CompletedLedgerReceipt extends Schema.TaggedClass()
A participant allocated a conversation address for a nonempty group.
-### [`ConversationParticipants`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/conversation.ts#L29)
+### [`ConversationParticipants`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/conversation.ts#L25)
_TypeAlias_
@@ -236,7 +277,7 @@ export type ConversationParticipants = readonly [
Every conversation has at least one participant of any network role.
-### [`ConversationSocket`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/conversation.ts#L99)
+### [`ConversationSocket`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/conversation.ts#L95)
_Class_
@@ -248,21 +289,21 @@ export class ConversationSocket {
* The ordered receive cursor for this endpoint and conversation. Repeated
* consumption advances the cursor instead of replaying old delivery.
*/
- readonly messages: Stream.Stream;
+ readonly messages: Stream.Stream;
readonly endpoint: ParticipantHandle;
readonly address: ConversationAddress;
private readonly sendMessage: (
content: MessageParts,
- ) => Effect.Effect;
+ ) => Effect.Effect;
private constructor(
endpoint: ParticipantHandle,
address: ConversationAddress,
- messages: Stream.Stream,
+ messages: Stream.Stream,
sendMessage: (
content: MessageParts,
- ) => Effect.Effect,
+ ) => Effect.Effect,
) {
this.endpoint = endpoint;
this.address = address;
@@ -273,10 +314,10 @@ export class ConversationSocket {
static [conversationSocketConstruction](
endpoint: ParticipantHandle,
address: ConversationAddress,
- messages: Stream.Stream,
+ messages: Stream.Stream,
sendMessage: (
content: MessageParts,
- ) => Effect.Effect,
+ ) => Effect.Effect,
): ConversationSocket {
return new ConversationSocket(endpoint, address, messages, sendMessage);
}
@@ -286,7 +327,7 @@ export class ConversationSocket {
* @param content Value supplied to the operation.
* @returns The created conversation socket.
*/
- send(content: string | MessageParts): Effect.Effect {
+ send(content: string | MessageParts): Effect.Effect {
return validateParts(parts(content)).pipe(Effect.flatMap(this.sendMessage));
}
@@ -295,14 +336,14 @@ export class ConversationSocket {
* consuming Effect, so the socket never skips an earlier message.
* @returns The created conversation socket.
*/
- receive(): Effect.Effect {
+ receive(): Effect.Effect {
return this.messages.pipe(
Stream.runHead,
Effect.flatMap(
Option.match({
onNone: () =>
Effect.fail(
- networkFailure(
+ networkError(
"receive",
`conversation ${this.address.conversationId} ended before another message arrived`,
),
@@ -333,7 +374,7 @@ export const coreEvents = EventCatalog.merge(
The exact event classes readable from every simulator run ledger.
-### [`CustomerEvents`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/event-services.ts#L38)
+### [`CustomerEvents`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/events.ts#L42)
_Interface_
@@ -360,7 +401,7 @@ export type EncodedEventOf = Schema.Schema.Encoded<
The closed encoded union persisted for a catalog.
-### [`Endpoint`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/endpoint.ts#L54)
+### [`Endpoint`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/endpoint.ts#L53)
_Class_
@@ -394,7 +435,7 @@ export class Endpoint {
* sockets retain their own ordered delivery queues independently.
* @returns Live endpoint delivery stream.
*/
- messages(): Stream.Stream {
+ messages(): Stream.Stream {
return this.inbox.messages;
}
@@ -406,7 +447,7 @@ export class Endpoint {
*/
open(
...participants: ConversationParticipants
- ): Effect.Effect {
+ ): Effect.Effect {
const [first, ...rest] = participants;
const ids: ParticipantIds = [
first.id,
@@ -444,7 +485,7 @@ export class Endpoint {
*/
socket(
address: ConversationAddress,
- ): Effect.Effect {
+ ): Effect.Effect {
const isParticipant = address.participants.some(
(participant) => participant.id === this.participant.id,
);
@@ -463,7 +504,7 @@ export class Endpoint {
),
)
: Effect.fail(
- networkFailure(
+ networkError(
"socket",
`participant ${this.participant.name} is not addressed by the conversation`,
),
@@ -511,7 +552,7 @@ export class EndpointMessageSent extends Schema.TaggedClass
A controlled endpoint committed a message through the data plane.
-### [`EventCatalog`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/events/catalog.ts#L152)
+### [`EventCatalog`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/events/catalog.ts#L130)
_Class_
@@ -606,7 +647,7 @@ The exact immutable event universe for one definition.
The private type identifier makes catalog arguments nominal: a structural
object cannot claim a schema, constructor list, and tag list that disagree.
-### [`EventCatalogDefinitionError`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/events/catalog.ts#L54)
+### [`EventCatalogDefinitionError`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/events/catalog.ts#L59)
_Class_
@@ -614,25 +655,12 @@ _Class_
export class EventCatalogDefinitionError extends Schema.TaggedError()(
"EventCatalogDefinitionError",
{
- failure: Schema.Literal(
- "duplicate-tag",
- "invalid-event-class",
- "invalid-tag",
- ),
+ failure: Schema.Literal("duplicate-tag", "invalid-tag"),
tag: Schema.String,
},
) {
override get message(): string {
- switch (this.failure) {
- case "duplicate-tag":
- return `Duplicate event tag "${this.tag}"`;
- case "invalid-event-class":
- return `Event catalog member "${this.tag}" is not a schema-backed class`;
- case "invalid-tag":
- return `Event tag "${this.tag}" must be namespaced and versioned, for example "acme.consensus-reached/v1"`;
- default:
- return `Unknown event catalog failure "${this.failure}" for "${this.tag}"`;
- }
+ return definitionFailureMessage[this.failure](this.tag);
}
}
```
@@ -644,10 +672,7 @@ Invalid catalogs fail during definition construction, before a run starts.
_TypeAlias_
```ts
-export type EventCatalogDefinitionFailure =
- | "duplicate-tag"
- | "invalid-event-class"
- | "invalid-tag";
+export type EventCatalogDefinitionFailure = "duplicate-tag" | "invalid-tag";
```
Represents event catalog definition failure conditions.
@@ -675,7 +700,7 @@ export type EventClassOf = CatalogClassesOf;
The closed constructor union declared by a catalog.
-### [`EventMetadata`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/event-services.ts#L22)
+### [`EventMetadata`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/events.ts#L26)
_Interface_
@@ -698,7 +723,7 @@ export type EventOf = Schema.Schema.Type>;
The closed instance union declared by a catalog.
-### [`IncompleteLedgerReceipt`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/run.ts#L76)
+### [`IncompleteLedgerReceipt`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/execute.ts#L73)
_Class_
@@ -713,7 +738,7 @@ export class IncompleteLedgerReceipt extends Schema.TaggedClass Effect.Effect;
+ ) => Effect.Effect;
/** Delay every delivery on one directed link for the current Scope. */
readonly delay: (
from: ParticipantHandle,
to: ParticipantHandle,
duration: Duration.DurationInput,
- ) => Effect.Effect;
+ ) => Effect.Effect;
/** Park every delivery on one directed link for the current Scope. */
readonly hold: (
from: ParticipantHandle,
to: ParticipantHandle,
- ) => Effect.Effect;
+ ) => Effect.Effect;
/** Install one custom policy on a directed link for the current Scope. */
readonly shape: (
from: ParticipantHandle,
to: ParticipantHandle,
policy: LinkPolicy,
description: string,
- ) => Effect.Effect;
+ ) => Effect.Effect;
}
```
@@ -991,7 +1016,7 @@ export type MessageParts = Schema.Schema.Type;
Nonempty protocol message content.
-### [`Network`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/endpoint.ts#L185)
+### [`Network`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/endpoint.ts#L184)
_Class_
@@ -1004,13 +1029,13 @@ export class Network extends Context.Tag("@moltzap/simulator/Network")<
Network operations available to the customer program.
-### [`NetworkFailure`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/router.ts#L50)
+### [`NetworkError`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/failure.ts#L22)
_Class_
```ts
-export class NetworkFailure extends Schema.TaggedError()(
- "NetworkFailure",
+export class NetworkError extends Schema.TaggedError()(
+ "NetworkError",
{
operation: networkOperation,
detail: Schema.String,
@@ -1024,7 +1049,7 @@ export class NetworkFailure extends Schema.TaggedError()(
An operational failure at a network boundary.
-### [`NetworkService`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/endpoint.ts#L178)
+### [`NetworkService`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/endpoint.ts#L177)
_Interface_
@@ -1032,7 +1057,7 @@ _Interface_
export interface NetworkService {
endpoint(
name: Name,
- ): Effect.Effect, NetworkFailure>;
+ ): Effect.Effect, NetworkError>;
}
```
@@ -1081,7 +1106,7 @@ export class ProgramFailed extends Schema.TaggedClass()(
The customer program failed with a typed failure or defect.
-### [`ProgramFinished`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/run.ts#L94)
+### [`ProgramFinished`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/execute.ts#L91)
_Class_
@@ -1122,7 +1147,7 @@ export class ProgramSucceeded extends Schema.TaggedClass()(
The customer program returned successfully.
-### [`ReadableRunLedger`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/event-services.ts#L28)
+### [`ReadableRunLedger`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/events.ts#L32)
_Interface_
@@ -1139,7 +1164,7 @@ export interface ReadableRunLedger {
Definition-bound read access to every committed core and customer event.
-### [`ReceivedMessage`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/router.ts#L76)
+### [`ReceivedMessage`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/network/router.ts#L35)
_Interface_
@@ -1212,84 +1237,98 @@ export class RouterStopFailed extends Schema.TaggedClass()(
Router release or stopped-router evidence collection failed.
-### [`RunInfrastructureFailed`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/run.ts#L100)
+### [`Run`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L310)
-_Class_
+_Variable_
```ts
-export class RunInfrastructureFailed<
- Definitions extends Readonly>,
-> extends Data.TaggedClass("RunInfrastructureFailed")<{
- readonly cause: Cause.Cause>;
- readonly receipt: LedgerReceipt;
-}> {}
+export const Run: Readonly<{ execute: typeof executeRunSpec }> = Object.freeze({
+ execute: executeRunSpec,
+})
```
-Post-allocation infrastructure failure plus all durable evidence retained.
+Discoverable execution entry point for one experiment society.
-### [`RunStarted`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/events/core.ts#L12)
+### [`RunSpec`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L150)
-_Class_
+_Interface_
```ts
-export class RunStarted extends Schema.TaggedClass()(
- "moltzap.run-started/v1",
- {
- definitionId: Schema.NonEmptyString,
- },
-) {}
+export interface RunSpec<
+ Id extends SimulatorDefinitionId = SimulatorDefinitionId,
+ CustomerCatalogs extends
+ readonly AnyEventCatalog[] = readonly AnyEventCatalog[],
+ Definitions extends Readonly> = Readonly<
+ Record
+ >,
+ A = unknown,
+ E = unknown,
+ R = never,
+ ClusterLayer extends Layer.Layer<
+ never,
+ unknown,
+ unknown
+ > = Layer.Layer,
+> {
+ /**
+ * Present only on the exact values RunSpec.define produced, and carrying
+ * their runner. This is the one identity gate: nothing structural
+ * distinguishes a definition from a lookalike, and a lookalike has no
+ * runner to invoke.
+ */
+ readonly [runSpecTypeId]?: () => RunSpecExecution<
+ Id,
+ CustomerCatalogs,
+ Definitions,
+ A,
+ E,
+ R,
+ ClusterLayer
+ >;
+ readonly id: Id;
+ readonly events: CustomerCatalogs;
+ readonly agents: Definitions;
+ readonly cluster: ClusterLayer &
+ Layer.Layer<
+ ClusterServices,
+ Layer.Layer.Error,
+ Layer.Layer.Context
+ >;
+ readonly execute: (
+ context: RunExecutionContext,
+ ) => Effect.Effect;
+}
```
-The run ledger is allocated and run-scoped acquisition has begun.
+Immutable code-first definition of one experiment society.
-### [`simulator`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L234)
+### [`RunSpec`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L305)
_Variable_
```ts
-export const simulator: Readonly<{ define: typeof defineSimulator }> =
- Object.freeze({
- define: defineSimulator,
- })
+export const RunSpec: Readonly<{ define: typeof defineRunSpec }> =
+ Object.freeze({ define: defineRunSpec })
```
-Discoverable entry point for code-first society definitions.
+Discoverable constructor for immutable experiment definitions.
-### [`SimulatorDefinition`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L169)
+### [`RunStarted`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/events/core.ts#L12)
-_Interface_
+_Class_
```ts
-export interface SimulatorDefinition<
- Id extends SimulatorDefinitionId,
- CustomerCatalogs extends readonly AnyEventCatalog[],
-> {
- readonly id: Id;
- readonly catalog: DefinitionEventServices["catalog"];
- readonly customerCatalog: CustomerEventCatalog;
- readonly ledger: DefinitionEventServices["ledger"];
- readonly events: DefinitionEventServices["events"];
- readonly agents: ReturnType>;
- readonly run: ReturnType<
- typeof makeRunner<
- Id,
- CatalogSchemaOf>,
- CatalogClassesOf>
- >
- >;
- readonly openLedger: ReturnType<
- typeof makeLedgerReader<
- Id,
- CatalogSchemaOf>,
- CatalogClassesOf>
- >
- >;
-}
+export class RunStarted extends Schema.TaggedClass()(
+ "moltzap.run-started/v1",
+ {
+ definitionId: Schema.NonEmptyString,
+ },
+) {}
```
-Definition-bound capabilities for one versioned family of simulator runs.
+The run ledger is allocated and run-scoped acquisition has begun.
-### [`SimulatorDefinitionError`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L27)
+### [`SimulatorDefinitionError`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/definition.ts#L28)
_Class_
@@ -1309,7 +1348,7 @@ export class SimulatorDefinitionError extends Schema.TaggedError>,
-> = AgentRosterAcquisitionError | LedgerFailure | NetworkFailure;
+> =
```
Represents simulator run failure conditions.
-### [`SimulatorRunOptions`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/run.ts#L61)
-
-_Interface_
-
-```ts
-export interface SimulatorRunOptions {
- readonly provenance?: JsonObject;
- readonly metadata?: JsonObject;
-}
-```
-
-Optional run metadata; platform and runtime policy belong in Layers.
-
-### [`SimulatorRunOutcome`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/kernel/run.ts#L108)
+### [`SimulatorRunOutcome`](https://github.com/chughtapan/moltzap/blob/main/packages/simulator/src/run/execute.ts#L105)
_TypeAlias_
@@ -1379,7 +1379,7 @@ export type SimulatorRunOutcome<
A,
E,
Definitions extends Readonly>,
-> = ProgramFinished | RunInfrastructureFailed;
+> = ProgramFinished | ClusterLost;
```
Closed result of every run whose ledger allocation succeeded.
@@ -1396,15 +1396,16 @@ Stable persisted identity for an event class.
## Files
+- `cluster.ts`
- `definition.ts`
- `catalog.ts`
- `core.ts`
-- `event-services.ts`
-- `run.ts`
-- `layer.ts`
-- `live.ts`
+- `append.ts`
- `conversation.ts`
- `endpoint.ts`
+- `failure.ts`
- `link.ts`
- `participant.ts`
- `router.ts`
+- `events.ts`
+- `execute.ts`
diff --git a/docs/simulator/grading.mdx b/docs/simulator/grading.mdx
index e6ffcd7cb..dec5ce8b8 100644
--- a/docs/simulator/grading.mdx
+++ b/docs/simulator/grading.mdx
@@ -11,27 +11,37 @@ The simulator ledger records facts without interpreting them. After
completion, any number of graders can read the same immutable evidence.
Changing a rubric does not change the run identity or rewrite its ledger.
-## Open evidence through its definition
+## Open evidence with the exact run contract
-Use the same `simulator.define` value that produced the run:
+Retrieve the completed artifacts from the selected local or GKE profile, then
+use the same definition id and complete catalog that produced the run:
```ts
import {
+ EventCatalog,
ProgramSucceeded,
RouterMessageCommitted,
- simulator,
+ coreEvents,
} from "@moltzap/simulator";
import type {
CompletedRunLedger,
} from "@moltzap/simulator/ledger";
+import {
+ openLedgerArtifacts,
+} from "@moltzap/simulator/ledger";
import { Chunk, Effect, Stream } from "effect";
+import {
+ deliveryEvents,
+ runSpec,
+} from "./delivery-run.mjs";
-const DeliveryEvaluation = simulator.define(
- "acme.delivery-evaluation/v1",
+const deliveryCatalog = EventCatalog.merge(
+ coreEvents,
+ deliveryEvents,
);
const gradeLedger = (
- ledger: CompletedRunLedger,
+ ledger: CompletedRunLedger,
) =>
Effect.gen(function* () {
const collected = yield* Effect.all({
@@ -49,15 +59,20 @@ const gradeLedger = (
};
});
-const report = yield* DeliveryEvaluation.openLedger(ledgerRef).pipe(
+const report = yield* openLedgerArtifacts(
+ deliveryCatalog,
+ receipt.ledger,
+ artifacts,
+ runSpec.id,
+).pipe(
Effect.flatMap(gradeLedger),
);
```
-`DeliveryEvaluation.openLedger(ledgerRef)` returns a ledger validated against
-that definition's exact catalog. `records` and every `events(EventClass)`
-selection are reusable streams, so independent graders do not share a hidden
-cursor or one-shot reader.
+`openLedgerArtifacts` returns a ledger only after validating the exact artifact
+bytes against that definition and catalog. `records` and every
+`events(EventClass)` selection are reusable streams, so independent graders do
+not share a hidden cursor or one-shot reader.
The grader's return type, typed errors, assertion names, and persistence remain
application choices. A boolean verdict is rarely enough. Text evidence is
@@ -113,7 +128,7 @@ class LedgerNotGradeable extends Schema.TaggedError()(
) {}
const requireProgramSuccess = (
- ledger: CompletedRunLedger,
+ ledger: CompletedRunLedger,
) =>
ledger.events(ProgramSucceeded).pipe(
Stream.runCollect,
@@ -176,7 +191,7 @@ The private `packages/evals` application demonstrates the distinction:
- `OpenClawPrincipalInstructionAttempted` and
`OpenClawPrincipalFinalOutput` describe OpenClaw's native gateway RPC;
-- `NanoclawPrincipalInputSent` describes input submitted through NanoClaw's
+- `NanoClawPrincipalInputSent` describes input submitted through NanoClaw's
owner-local socket;
- `CodePeerMessageSent` and `CodePeerMessageReceived` are testimony from
autonomous Effect peers using the production protocol;
@@ -196,8 +211,8 @@ cases proceed by selecting router-bound peer evidence. Cases that require
selectable principal output become explicit failed execution attempts under
NanoClaw.
-This arrangement lets real process agents and in-process Effect agents share
-one router without giving code agents a callback path around the network.
+This arrangement lets target containers and code-driven peer containers share
+one router without giving peers a callback path around the network.
## Code graders compose
diff --git a/docs/simulator/overview.mdx b/docs/simulator/overview.mdx
index 9a4d0ef8c..998441000 100644
--- a/docs/simulator/overview.mdx
+++ b/docs/simulator/overview.mdx
@@ -1,29 +1,19 @@
---
title: "Society simulator"
-description: "Run mixed agent societies as Effect programs and analyze exact typed ledgers."
+description: "Run containerized agent societies as code-first Effect programs and analyze exact typed ledgers."
---
-`@moltzap/simulator` is the code-first library for agentic-society experiments.
-One run owns one router, one ledger, and one keyed roster. Programs use the
-Effect `Clock` in their environment. The roster can freely mix external
-processes, in-process `effectRuntime` agents, and customer-defined
-`defineRuntime` agents. Deterministic mocks are ordinary instances of those
-code runtimes.
-
-Every autonomous runtime exposes its own owner-local principal gateway and
-uses the same MoltZap protocol and run-scoped router for social traffic.
-OpenClaw keeps its native gateway RPC, NanoClaw keeps its native CLI socket,
-and an in-process runtime exposes exactly the customer gateway returned by
-its builder. None of those gateways replaces the network. In-process agents
-do not receive a callback shortcut around the router, so mixed-agent results
-exercise the same addressing, delivery, and durable router path.
-
-The package also supplies the filesystem ledger, isolated production router,
-and shipped OpenClaw, NanoClaw, and Effect runtime implementations. The
-production router requires a reachable Docker daemon. It
-builds and caches a local content-addressed router image from the exact
-`@moltzap/server-core` and `@moltzap/protocol` packages installed with the
-simulator.
+`@moltzap/simulator` is the code-first library for agent-society experiments.
+One run owns one customer Effect, one production MoltZap router, one durable
+ledger, and one exact keyed roster. Kubernetes is the execution backend. The
+repository provides local kind and GKE profiles for the same path.
+
+Each roster entry becomes one Agent Sandbox application container. Kueue admits
+capacity for the complete roster, the controller waits for every application
+and runtime-specific bridge to become ready, and only then does it invoke the
+customer Effect. Temporal coordinates the coarse run lifecycle and cleanup.
+Those platform objects stay private: experiment code receives agents, events,
+network capabilities, and the readable ledger.
## One package, four public entry points
@@ -31,26 +21,32 @@ The package keeps capability boundaries inside one install:
| Import | Owner |
|---|---|
-| `@moltzap/simulator` | Society definitions, the run kernel, customer services, and `simulatorLayer` |
-| `@moltzap/simulator/runtime` | Runtime contracts and the Effect, OpenClaw, and NanoClaw implementations |
-| `@moltzap/simulator/network` | Router, transport, participant, endpoint, conversation, and link contracts for network implementations |
-| `@moltzap/simulator/ledger` | Ledger schemas, storage contracts, completed-ledger opening, and offline inspection |
+| `@moltzap/simulator` | `RunSpec`, `Run.execute`, event catalogs, customer services, and run outcomes |
+| `@moltzap/simulator/agents` | Container runtime descriptors and the shipped OpenClaw and NanoClaw implementations |
+| `@moltzap/simulator/network` | Router, transport, participant, endpoint, conversation, and link contracts |
+| `@moltzap/simulator/ledger` | Ledger schemas, completed-artifact validation, and offline inspection |
-Experiment code uses the root entry point together with `/runtime`. Router and
-link implementations use `/network`; storage implementations and independent
-analysis tools use `/ledger`. Internally, the kernel coordinates these
-capabilities through Effect services, while `simulatorLayer` provides the
-production router, filesystem ledger, and host services once at the
-application boundary.
+Experiment code normally imports the root entry point and `/agents`.
+Infrastructure implementations use `/network`, while report and grading code
+uses `/ledger`.
-## Define the event universe
+## Define one `RunSpec`
-A definition has a versioned identity and an exact set of schema-backed event
-classes:
+A controller-loadable experiment module exports exactly one named `runSpec`.
+The definition contains a versioned identity, its complete customer event
+catalog, its exact roster, the cluster Layer supplied by the selected profile,
+and the customer Effect:
```ts
-import { EventCatalog, simulator } from "@moltzap/simulator";
-import { Schema } from "effect";
+import {
+ EventCatalog,
+ RunSpec,
+} from "@moltzap/simulator";
+import {
+ openClawRuntime,
+} from "@moltzap/simulator/agents";
+import { Effect, Schema } from "effect";
+import { controllerServicesFromEnvironment } from "/opt/moltzap/dist/cluster/controller/services.js";
class ConsensusReached extends Schema.TaggedClass()(
"acme.consensus-reached/v1",
@@ -60,253 +56,121 @@ class ConsensusReached extends Schema.TaggedClass()(
},
) {}
-const Society = simulator.define(
- "acme.negotiation/v1",
- EventCatalog.make(ConsensusReached),
+export const negotiationEvents = EventCatalog.make(
+ ConsensusReached,
);
-```
-
-The definition automatically adds `CoreEvents`: run, router, runtime,
-endpoint, link, and program evidence emitted by the kernel. Callers declare
-only customer-owned classes.
-
-The resulting catalog is closed. Undeclared classes cannot be emitted,
-selected from a typed event stream, or decoded by `Society.openLedger`.
-Duplicate, unversioned, and malformed event tags fail during definition
-construction. Changing a persisted event shape requires a new tag, such as
-`acme.consensus-reached/v2`. Typed opening always uses one of the exact classes
-declared by the matching definition.
-
-## Mix runtimes in one roster
-`Society.agents` preserves every roster key and runtime gateway in the type of
-`roster.startedAgents`:
-
-```ts
-import { messagesSend } from "@moltzap/protocol/message";
-import {
- effectRuntime,
- nanoclawRuntime,
- openClawRuntime,
-} from "@moltzap/simulator/runtime";
-import { Effect, Ref, Stream } from "effect";
-
-const roster = Society.agents({
- alice: openClawRuntime(),
- bob: nanoclawRuntime({
- autoRegisterConversations: true,
- }),
- carol: effectRuntime({
- build: (context) =>
- Effect.gen(function* () {
- const prefix = yield* Ref.make("Reply from ");
- return {
- gateway: Object.freeze({
- setPrefix: (value: string) => Ref.set(prefix, value),
- }),
- behavior: context.messages.pipe(
- Stream.runForEach((notification) =>
- Ref.get(prefix).pipe(
- Effect.flatMap((value) =>
- context.client.callDefinition(messagesSend, {
- conversationId:
- notification.message.conversationId,
- parts: [
- {
- type: "text",
- text: `${value}${context.agent.name}`,
- },
- ],
- }),
- ),
- Effect.asVoid,
- ),
- ),
- ),
- };
- }),
- }),
+const runtime = (identity: string) =>
+ openClawRuntime({
+ tools: {
+ deny: ["*"],
+ elevated: { enabled: false },
+ exec: { mode: "deny" },
+ },
+ sandbox: { mode: "off" },
+ workspaceFiles: [
+ { relativePath: "IDENTITY.md", content: identity },
+ ],
+ });
+
+export const runSpec = RunSpec.define({
+ id: "acme.negotiation/v1",
+ events: [negotiationEvents],
+ agents: {
+ alice: runtime("You are Alice."),
+ bob: runtime("You are Bob."),
+ },
+ cluster: controllerServicesFromEnvironment(),
+ execute: ({ agents, events, network, ledger }) =>
+ Effect.gen(function* () {
+ const workload = yield* network.endpoint("workload");
+ const conversation = yield* workload.open(
+ agents.alice.agent,
+ agents.bob.agent,
+ );
+ yield* conversation.send(
+ "Propose a plan and explain the tradeoffs.",
+ );
+
+ yield* events.emit(
+ ConsensusReached.make({
+ proposal: "initial-proposal",
+ supporters: [agents.alice.agent.name],
+ }),
+ );
+
+ yield* Effect.logDebug("ledger allocated", ledger.ref);
+ }),
});
```
-Each runtime constructor owns its installation, startup deadline, readiness,
-and scoped teardown policy. A custom runtime uses `defineRuntime` from
-`@moltzap/simulator/runtime` and receives the same identity, credentials,
-router address, readiness connection, and Scope as the shipped
-implementations.
-Deterministic mocks are ordinary code runtimes in the same roster.
-
-Every runtime also owns a Schema describing its definition-time policy,
-overrides, and defaults. Construction captures a deeply immutable encoded JSON
-snapshot; each read returns a fresh value in the runtime schema's native shape,
-so mutating a native built-in cannot alter later reads or ledger provenance.
-This configuration does not claim acquisition-resolved host facts. The kernel
-records the canonical snapshot under the kernel-owned `agents` provenance key.
-Customer provenance is composed around that key and cannot replace agent,
-runtime, or configuration evidence. Runtime families do not normalize model
-or provider fields into a simulator-wide union, and credentials never enter
-this configuration.
-
-Runtime acquisition returns only after readiness. Once every runtime is
-ready, `roster.startedAgents` contains exact values such as `agents.alice` and
-`agents.carol`. Keyed access carries the declared roster and its exact gateway
-types into the experiment.
-
-Each value is a `StartedAgent` with three deliberately separate capabilities:
-
-| Field | Meaning |
-|---|---|
-| `agent` | Router-issued identity used to address the autonomous participant |
-| `gateway` | Runtime-native, owner-local principal API |
-| `termination` | Effect that observes completion, failure, exit, or signal |
-
-For example, `agents.alice.gateway.agent(...)` invokes OpenClaw's native
-`agent` RPC, while `agents.bob.gateway.submit(...)` writes to NanoClaw's
-native CLI socket. `agents.carol.gateway.setPrefix(...)` is exactly the API
-returned by the `effectRuntime` builder above. These calls control each
-runtime through the interface it already owns. Any agent-to-agent message
-caused by that control is still an autonomous action sent through the
-runtime's production client and router.
+The absolute cluster-services import is private to the repository-built
+controller image. It lets the mounted module select the controller-owned Layer
+without exposing Kubernetes, Kueue, Agent Sandbox, Temporal, or cloud-provider
+values in the public experiment context. The controller loads the module late
+and calls `Run.execute(runSpec)` once.
-## Write the experiment as an Effect
+The definition's event universe is closed. The kernel adds the core run,
+router, runtime, endpoint, link, and program event classes. Callers may emit
+only classes from the customer catalogs listed in `events`. Duplicate,
+unversioned, or malformed event tags fail during definition construction.
+Changing a persisted event shape requires a new versioned tag.
-The experiment obtains run-scoped capabilities as Effect services:
+## Runtime-native gateways stay exact
-```ts
-import { Network } from "@moltzap/simulator";
-import { Effect } from "effect";
+Every started roster value exposes three separate capabilities:
-const experiment = Effect.gen(function* () {
- const agents = yield* roster.startedAgents;
- const network = yield* Network;
- const events = yield* Society.Events;
- const ledger = yield* Society.Ledger;
+| Field | Meaning |
+|---|---|
+| `agent` | Router-issued social identity for the autonomous participant |
+| `gateway` | That runtime's exact owner-local principal API |
+| `termination` | Observation of autonomous completion, failure, exit, or signal |
- const workload = yield* network.endpoint("workload");
- const conversation = yield* workload.open(
- agents.alice.agent,
- agents.bob.agent,
- );
+OpenClaw keeps its gateway RPC and NanoClaw keeps its CLI-socket contract. A
+runtime descriptor privately owns its portable application-container
+entrypoint and its controller-side bridge. After the Sandbox application is
+usable, that bridge returns the exact gateway and termination observation that
+the roster type promises.
- yield* conversation.send(
- "Propose a plan and explain the tradeoffs.",
- );
+Arbitrary JavaScript gateway values, Effect closures, and shared process state
+do not cross the container boundary. Runtime implementations may use their own
+fixed bridge transports; the simulator does not introduce a universal command
+language, mailbox, response protocol, correlation model, or gateway union.
- const reply = yield* conversation.receive();
+Code-driven evaluation peers follow the same boundary. Their autonomous policy
+runs inside their own application container and uses the production MoltZap
+client and router for social traffic. Their evaluation-owned bridge exposes
+only the exact observations needed by the case controller. It cannot command a
+peer to send a social message.
- yield* events.emit(
- ConsensusReached.make({
- proposal: reply.message.id,
- supporters: [agents.alice.agent.name],
- }),
- { correlationId: reply.message.id },
- );
-
- yield* Effect.logDebug(
- "ledger allocated",
- ledger.ref,
- );
- return reply.message;
-});
-```
+## The customer Effect owns experiment policy
-The four services have distinct jobs:
+`execute` receives four run-scoped capabilities:
-| Service | Capability |
+| Capability | Purpose |
|---|---|
-| `roster.startedAgents` | Exact identities, principal gateways, and lifecycle observations for autonomous participants |
-| `Network` | Experiment-controlled diagnostic, workload, and observer endpoints |
-| `Society.Events` | Emit only this definition's customer event classes |
-| `Society.Ledger` | Read all core and customer evidence committed so far |
-
-`Society.Ledger.records` is a catch-up-then-tail Stream of full envelopes.
-`Society.Ledger.events(ConsensusReached)` is a catch-up-then-tail Stream of that
-exact class. A late or racing consumer receives committed history and then
-live commits without a gap. Customer code owns Stream consumption and fiber
+| `agents` | Exact roster keys, identities, native gateways, and termination observations |
+| `network` | Experiment-controlled diagnostic, workload, and observer endpoints |
+| `events` | Emit only the definition's declared customer event classes |
+| `ledger` | Read all core and customer evidence committed so far |
+
+The readable ledger's `records` stream catches up over committed history and
+then follows live commits. `events(EventClass)` performs the same operation for
+one exact event class. Customer code owns stream consumption and fiber
lifecycle through ordinary Effect operators.
-The customer event producer is fixed by the kernel. `Events.emit` accepts
-only an event plus optional causation and correlation ids; callers cannot
-claim to be the router, runtime supervisor, endpoint observer, or link
-controller.
-
-## Endpoints and conversations
-
-`Network.endpoint(name)` binds an experiment-controlled participant to the
-router. The same name returns the same endpoint for the run, and each name has
-one binding. Endpoints are diagnostics, workload generators, or observers
-controlled by the experiment. They are not principal APIs for OpenClaw,
-NanoClaw, or code agents; autonomous participants and their native gateways
-belong in the roster.
-
-`endpoint.open(...participants)` creates a participant-independent
-`ConversationAddress` and returns a `ConversationSocket` bound to the opening
-endpoint. The socket exposes:
-
-- `send(content)` for protocol text or parts;
-- `messages`, one ordered receive cursor for that endpoint and address;
-- `receive()` for the next ordered delivery. Selection and discard policy
- stays in the customer Effect.
+Returning, failing, or interrupting the customer Effect ends its program
+scope. Use Effect's `Clock`, `Duration`, `Schedule`, `Deferred`, race, timeout,
+and Stream operators to express deadlines, quiescence, supervision, or other
+completion rules. Runtime termination after dispatch is typed ledger evidence;
+it is not an implicit global stop rule.
-Another addressed endpoint binds the same address with
-`endpoint.socket(conversation.address)`. Conversation identity never implies
-a sender; the bound socket does. A socket cursor advances as it is consumed,
-so later receives do not return old messages. `endpoint.messages()` is a live
-fan-out stream for endpoint observers; start consuming it before the traffic
-of interest. Use `Society.Ledger` for durable evidence.
+`Network.endpoint(name)` creates an experiment-controlled participant. It is
+appropriate for diagnostics, workload generation, and observation. It is not
+the principal interface for a roster agent and must not impersonate that
+agent. Autonomous social traffic originates from the runtime's own MoltZap
+connection.
-## Customer policy ends the run
-
-Pass the roster and the already-built Effect to `Society.run`:
-
-```ts
-import {
- simulatorLayer,
-} from "@moltzap/simulator";
-import { Duration, Effect } from "effect";
-
-const Platform = simulatorLayer({
- ledgerDirectory: "./simulator-ledgers",
- router: {
- startupTimeout: Duration.minutes(2),
- },
-});
-
-const run = Society.run(
- roster,
- experiment,
- {
- provenance: { suite: "negotiation" },
- metadata: { case: "baseline" },
- },
-).pipe(Effect.provide(Platform));
-
-const outcome = yield* run;
-```
-
-Returning, failing, or interrupting the experiment ends its program scope.
-Use `Effect.timeout`, `Effect.race`, `Schedule`, `Clock`, `Deferred`, and
-Stream operators directly to express completion. Runtime termination is
-ledger evidence, not an implicit global stop rule; customer policy decides
-whether an agent exit should fail, finish, or leave the experiment running.
-
-When the outer Effect completes after ledger allocation, the run returns a
-closed outcome.
-`ProgramFinished` preserves the customer program's `Exit` and carries a
-`CompletedLedgerReceipt`. `RunInfrastructureFailed` preserves the exact
-infrastructure `Cause` and carries either a completed or incomplete receipt.
-Both receipts retain the storage-owned ledger reference. Only allocation
-failure before an active ledger capability reaches kernel ownership remains a
-typed failure of the outer Effect. A `LedgerStorageError` may still identify a
-reference minted during that unsuccessful allocation.
-
-Caller interruption remains interruption after the kernel's finalization
-attempt and therefore does not return either outcome.
-
-Customer modules own scenario formats, operator commands, completion policy,
-sweep execution, and graders.
-
-## Directed links are scoped
The run kernel owns the link fabric and installs both `LinkController` and the
`LinkDriver` behind it, so shaping traffic needs no additional layers:
@@ -405,28 +269,39 @@ pair of events, never either one alone.
## One run-owned lifecycle
-`Society.run` owns the resource order:
+Each invocation creates one society and then tears it down:
+
+1. Temporal starts one coarse workflow for the run.
+2. Kueue admits capacity for the complete roster.
+3. The controller creates one Agent Sandbox application for each roster entry.
+4. Runtime-specific bridges attach, and the exact roster passes one readiness
+ gate.
+5. The controller invokes the customer Effect once.
+6. The simulator finalizes the ledger and run outcome.
+7. Temporal drives cleanup of run-owned Kubernetes resources.
-1. Allocate `manifest.json` and `records.ndjson`.
-2. Acquire one isolated router.
-3. Create the link fabric that owns policy state and per-message link evidence.
-4. Bind the roster and wait for every runtime's readiness contract. An
- in-process runtime registers its agent with the fabric while it acquires
- its inbound stream.
-5. Install `roster.startedAgents`, `Network`, `Society.Ledger`,
- `Society.Events`, `LinkController`, and its `LinkDriver`, then run the
- customer Effect.
-6. Close experiment endpoints, runtime scopes, and the router.
-7. Append durable router-commit evidence available after router shutdown.
-8. Publish `completion.json`.
+The society is not a reusable warm pool. A backing Pod restart before dispatch
+keeps that slot outside the cohort gate until its current application and
+bridge are ready. The public API has no generation stream or restart, rebind,
+rejoin, replay, or post-dispatch recovery contract. Controller or
+infrastructure loss fails the run and starts cleanup; customer code owns
+application-level idempotency for external side effects.
-The v0 lifecycle has one binding per participant. Restart, replacement,
-rebinding, fencing, and offline delivery are outside the current contract.
-Teardown-induced process exit is not reported as autonomous termination.
+When execution reaches ledger ownership, the run produces one of two closed
+outcomes:
-## Durable, then visible
+- `ProgramFinished` preserves the customer program's `Exit` and carries a
+ `CompletedLedgerReceipt`.
+- `ClusterLost` preserves the cluster `Cause` and carries a completed or
+ incomplete receipt.
-The filesystem ledger has three artifacts:
+Ledger allocation failure before ownership remains a typed failure of the
+outer Effect. Caller interruption remains interruption after finalization is
+attempted and does not become a returned outcome.
+
+## Durable evidence and offline grading
+
+A completed run owns three artifacts:
| File | Holds |
|---|---|
@@ -434,37 +309,61 @@ The filesystem ledger has three artifacts:
| `records.ndjson` | Schema-validated event envelopes in one logical sequence |
| `completion.json` | Record count and SHA-256 digests for the manifest and records |
-A commit is acknowledged only after the corresponding record bytes are
-durable. Live readers then observe the value decoded from those exact bytes.
-A failed append is never published to readers, and the failure ends the run.
+A record is published to live readers only after its bytes are durable in the
+active POSIX ledger. Local runs write that ledger beneath their retained
+artifact root. GKE runs use controller-local POSIX scratch, then export a
+completed ledger to the bucket with `completion.json` last. Both profiles use
+the same retained relative shape:
+
+```text
+{namespace}/ledger/{ledgerRef}/manifest.json
+{namespace}/ledger/{ledgerRef}/records.ndjson
+{namespace}/ledger/{ledgerRef}/completion.json
+```
+
+GKE export happens only after the simulator produces a completed receipt. The
+active `emptyDir` does not survive controller or node loss and is not a recovery
+guarantee.
-`Society.openLedger(outcome.receipt.ledger)` verifies completed artifacts before
-exposing evidence:
+After retrieving those exact files, construct the same complete catalog and
+open them without starting a router or any agents:
```ts
import {
- ProgramFinished,
+ EventCatalog,
+ coreEvents,
} from "@moltzap/simulator";
-import { Effect, Stream } from "effect";
-
-if (!(outcome instanceof ProgramFinished)) {
- return yield* Effect.failCause(outcome.cause);
-}
-
-const ledger = yield* Society.openLedger(outcome.receipt.ledger);
+import {
+ openLedgerArtifacts,
+} from "@moltzap/simulator/ledger";
-const consensus = yield* ledger
- .events(ConsensusReached)
- .pipe(Stream.runCollect);
+const catalog = EventCatalog.merge(
+ coreEvents,
+ negotiationEvents,
+);
-const report = yield* Society.openLedger(outcome.receipt.ledger).pipe(
- Effect.flatMap(gradeLedger),
+const ledger = yield* openLedgerArtifacts(
+ catalog,
+ receipt.ledger,
+ artifacts,
+ runSpec.id,
);
```
-Opening checks strict artifact schemas, definition identity, exact catalog
-tags, SHA-256 digests, run identities, record count, unique event ids,
-contiguous logical sequence, and every event schema. The resulting
-`CompletedRunLedger` streams are immutable, reusable, exact-class streams.
-Opening a ledger does not start agents or a router. Compose any number of
-ordinary Effect graders over the returned value.
+Opening verifies strict artifact schemas, the expected definition id, exact
+catalog tags, completion digests, run identities, record count, unique event
+ids, contiguous logical sequence, and every event schema. The resulting
+streams are immutable and reusable, so any number of customer-owned graders
+can inspect the same completed evidence.
+
+## Local and GKE are profiles of the same path
+
+The local profile creates a repository-owned kind cluster with the pinned
+Kueue, Agent Sandbox, and development Temporal components. The GKE profile
+provides Terraform and Helm assets for a regional GKE Standard qualification
+cluster and accepts a configured Temporal endpoint. Both submit the same `.mjs`
+`runSpec` module and reach the same controller and `Run.execute` path.
+
+See [Running simulator programs](/simulator/running) for commands. Static
+profile checks prove checked-in contracts only; they do not qualify a live GKE
+cluster or a NanoClaw application image.
diff --git a/docs/simulator/running.mdx b/docs/simulator/running.mdx
index 0fa0323e5..b2c411477 100644
--- a/docs/simulator/running.mdx
+++ b/docs/simulator/running.mdx
@@ -1,247 +1,229 @@
---
title: "Running simulator programs"
-description: "Run code-first society experiments through your existing TypeScript and job tooling."
+description: "Submit one RunSpec through the shared local-Kubernetes or GKE execution path."
---
-The simulator runs through ordinary TypeScript entrypoints and task runners.
-Experiment owners expose the command or operator surface that fits their
-domain.
-
-The code-first API keeps the network, lifecycle, and ledger contracts stable
-while each experiment owner chooses the operator surface appropriate to its
-domain. TypeScript entrypoints and task runners are the simulator's normal
-execution path.
+Simulator programs are ordinary `.mjs` modules loaded by the in-cluster
+controller. The repository-local submitters accept one module path and run it
+through the same Temporal, Kubernetes, Kueue, Agent Sandbox, controller, and
+`Run.execute` path.
## Package entry points
| Import | Purpose |
|---|---|
-| `@moltzap/simulator` | Definitions, event catalogs, services, and the default host Layer |
-| `@moltzap/simulator/runtime` | Runtime contracts and the Effect, OpenClaw, and NanoClaw implementations |
+| `@moltzap/simulator` | `RunSpec`, `Run.execute`, event catalogs, customer services, and run outcomes |
+| `@moltzap/simulator/agents` | Container runtime descriptors and exact OpenClaw and NanoClaw gateway contracts |
| `@moltzap/simulator/network` | Router, transport, link, endpoint, and nominal capability contracts |
-| `@moltzap/simulator/ledger` | Completed-ledger types, the storage port, and manifest inspection |
+| `@moltzap/simulator/ledger` | Completed-ledger types, validation, and artifact inspection |
-`simulator.define` binds `run` and `openLedger` to one versioned definition
-and its complete event catalog.
+The experiment module owns its agents, customer events, customer Effect, and
+completion policy. The selected profile owns every platform object.
-## Make a TypeScript entrypoint
+## Write a controller-loadable module
-A normal module is an executable experiment:
+Export exactly one named `runSpec`:
```ts
-import { messagesSend } from "@moltzap/protocol/message";
-import {
- Network,
- simulator,
- simulatorLayer,
-} from "@moltzap/simulator";
+import { RunSpec } from "@moltzap/simulator";
import {
- effectRuntime,
-} from "@moltzap/simulator/runtime";
-import {
- Duration,
- Effect,
- Ref,
- Schema,
- Stream,
-} from "effect";
-
-const Society = simulator.define("acme.echo/v1");
-
-const roster = Society.agents({
- echo: effectRuntime({
- build: (context) =>
- Effect.gen(function* () {
- const prefix = yield* Ref.make("echo: ");
- return {
- gateway: Object.freeze({
- setPrefix: (value: string) => Ref.set(prefix, value),
- }),
- behavior: context.messages.pipe(
- Stream.runForEach((notification) =>
- Ref.get(prefix).pipe(
- Effect.flatMap((value) =>
- context.client.callDefinition(messagesSend, {
- conversationId:
- notification.message.conversationId,
- parts: [
- {
- type: "text",
- text: `${value}${context.agent.name}`,
- },
- ],
- }),
- ),
- Effect.asVoid,
- ),
- ),
- ),
- };
- }),
- }),
-});
+ openClawRuntime,
+} from "@moltzap/simulator/agents";
+import { Duration, Effect, Schema } from "effect";
+import { controllerServicesFromEnvironment } from "/opt/moltzap/dist/cluster/controller/services.js";
-const experiment = Effect.gen(function* () {
- const agents = yield* roster.startedAgents;
- const network = yield* Network;
- yield* agents.echo.gateway.setPrefix("diagnostic reply: ");
+class ExperimentTimedOut extends Schema.TaggedError()(
+ "ExperimentTimedOut",
+ {},
+) {}
- const workload = yield* network.endpoint("diagnostics");
- const conversation = yield* workload.open(agents.echo.agent);
- yield* conversation.send("hello");
- return yield* conversation.receive();
+const alice = openClawRuntime({
+ tools: {
+ deny: ["*"],
+ elevated: { enabled: false },
+ exec: { mode: "deny" },
+ },
+ sandbox: { mode: "off" },
+ workspaceFiles: [
+ { relativePath: "IDENTITY.md", content: "You are Alice." },
+ ],
});
-const Platform = simulatorLayer({
- ledgerDirectory: "./simulator-ledgers",
- router: {
- startupTimeout: Duration.minutes(2),
- },
+export const runSpec = RunSpec.define({
+ id: "acme.echo/v1",
+ events: [],
+ agents: { alice },
+ cluster: controllerServicesFromEnvironment(),
+ execute: ({ agents, network }) =>
+ Effect.gen(function* () {
+ const diagnostic = yield* network.endpoint("diagnostic");
+ const conversation = yield* diagnostic.open(
+ agents.alice.agent,
+ );
+ yield* conversation.send("hello");
+ }).pipe(
+ Effect.timeoutFail({
+ duration: Duration.minutes(5),
+ onTimeout: () => ExperimentTimedOut.make({}),
+ }),
+ ),
});
+```
-const main = Society.run(
- roster,
- experiment,
- {
- provenance: { suite: "smoke" },
- metadata: { case: "echo" },
- },
-).pipe(Effect.provide(Platform));
+The absolute cluster-services import is available inside the repository-built
+controller image. It constructs the selected profile's private Layer from the
+validated controller environment. Experiment code does not receive raw
+Kubernetes, Kueue, Sandbox, or Temporal objects.
-void Effect.runPromise(main);
-```
+The controller requires the exact value returned by `RunSpec.define`. It
+loads the mounted module once and invokes `Run.execute(runSpec)` once; there is
+no fallback execution entry point or automatic replay.
-Customer provenance is additive. The kernel always writes the reserved
-`agents` key last with each roster name, runtime name, and sanitized
-definition-time runtime configuration, so a caller-provided `agents` value
-cannot replace execution evidence.
+Every roster runtime must provide a distributed application-container
+realization. Its bridge resolves only after the application is usable and
+returns that runtime's exact `.gateway` plus `.termination` observation. The
+customer Effect starts after all roster entries pass the same readiness gate.
-Run the module with the repository's build target, Node entrypoint, test
-runner, workflow system, or scheduler.
+## Run on the local Kubernetes profile
-Construct `simulatorLayer` once at the application boundary and provide
-it around the complete run or suite. Runtime constructors remain values in
-the roster; they own runtime-specific installation and readiness settings.
+Build the shared controller/support image:
-`roster.startedAgents` becomes available only after every runtime is ready.
-Each value separates its router-issued `.agent`, exact runtime-native
-`.gateway`, and `.termination` observation. OpenClaw and NanoClaw retain their
-existing owner-local gateways. An `effectRuntime` exposes exactly the gateway
-returned by `build`; its autonomous `behavior` uses the production client and
-router for social actions.
+```bash
+pnpm nx run @moltzap/simulator:local-controller-image
+```
-`Network.endpoint` creates only experiment-controlled diagnostics, workloads,
-and observers. It is not a substitute principal interface for a roster
-runtime.
+The command prints an immutable `pinnedImage`. Use it to create the pinned kind
+profile:
-## Express completion policy in the program
+```bash
+pnpm nx run @moltzap/simulator:local-cluster-create -- \
+ --artifacts "$PWD/.moltzap/local-artifacts" \
+ --image CONTROLLER_IMAGE_AT_SHA256
+```
-The customer Effect returns, fails, or is interrupted according to its own
-logic:
+The cluster setup prints its exact kube context, tool paths, queue names,
+Temporal address, and artifact roots. It refuses to replace an existing
+cluster.
-```ts
-class ExperimentTimedOut extends Schema.TaggedError()(
- "ExperimentTimedOut",
- {},
-) {}
+Submit the module through the local profile:
-const boundedExperiment = experiment.pipe(
- Effect.timeoutFail({
- duration: Duration.minutes(5),
- onTimeout: () => ExperimentTimedOut.make({}),
- }),
-);
+```bash
+MOLTZAP_CONTROLLER_IMAGE=CONTROLLER_IMAGE_AT_SHA256 \
+MOLTZAP_SUPPORT_IMAGE=CONTROLLER_IMAGE_AT_SHA256 \
+MOLTZAP_TEMPORAL_ADDRESS=127.0.0.1:7233 \
+pnpm nx run @moltzap/simulator:local-run -- path/to/experiment.mjs
```
-Use Effect's `Clock`, `Schedule`, `race`, `timeout`, `Deferred`, Stream, and
-Scope primitives for deadlines, quiescence, supervised work, or explicit
-stop conditions.
+`MOLTZAP_SUPPORT_IMAGE` defaults to `MOLTZAP_CONTROLLER_IMAGE` for this local
+path. Both values must be digest-pinned. The checked-in
+`packages/simulator/local/README.md` records the component versions and smoke
+modules.
-A runtime exit after readiness is committed as typed ledger evidence. It does
-not implicitly end the customer Effect. This lets one policy fail fast on an
-agent exit while another continues to observe the remaining society.
+## Run on the GKE profile
-## Interpret allocation and run outcomes separately
+Provision the checked-in Terraform profile, install its pinned add-ons, push
+the controller/support image, and acquire the explicit kube context as
+described in `packages/simulator/gke/README.md`. Then submit the same module:
-The outer `Society.run` Effect fails only when ledger allocation fails before
-an active ledger capability reaches kernel ownership. That typed
-`LedgerStorageError` may identify a reference minted during the unsuccessful
-allocation. When allowed to complete after that ownership handoff, the Effect
-returns one of two closed outcomes:
+```bash
+MOLTZAP_KUBE_CONTEXT=EXPLICIT_KUBE_CONTEXT \
+MOLTZAP_GKE_ARTIFACT_BUCKET=PROFILE_ARTIFACT_BUCKET \
+MOLTZAP_TEMPORAL_ADDRESS=TEMPORAL_HOST:7233 \
+MOLTZAP_CONTROLLER_IMAGE=REGISTRY/CONTROLLER@sha256:DIGEST \
+MOLTZAP_SUPPORT_IMAGE=REGISTRY/CONTROLLER@sha256:DIGEST \
+pnpm nx run @moltzap/simulator:gke-run -- path/to/experiment.mjs
+```
-- `ProgramFinished` carries the customer program's `Exit` and a
- `CompletedLedgerReceipt`.
-- `RunInfrastructureFailed` carries the exact infrastructure `Cause` and
- either a completed or incomplete ledger receipt.
+The GKE submitter validates the checked-in profile, uses only the explicit
+context and bucket, and calls the same Temporal submission code as the local
+profile. The repository does not select production Temporal hosting or high
+availability.
-Program failure and interruption are values inside `ProgramFinished.exit`.
-Router, runtime-acquisition, append, teardown, and completion failures are
-`RunInfrastructureFailed` values. In both cases, the caller receives the exact
-physical ledger reference retained after allocation; it never has to scan a
-directory to recover evidence.
+Static GKE validation does not contact Google Cloud or a cluster:
-Interrupting the outer `Society.run` Effect remains caller interruption after
-the kernel's finalization attempt and does not return a receipt.
+```bash
+pnpm nx run @moltzap/simulator:gke-profile-check
+```
-A customer command can map these typed values to its own exit codes,
-structured output, retries, and operator messages. The simulator package does
-not impose a process-wide exit-code table.
+Passing that check is not a live qualification claim. The GKE acceptance gate
+still requires a caller-authorized project, the small smoke, an OpenClaw
+evaluation, readable retained artifacts, and zero run-owned residue.
-## Build a narrow customer language when useful
+## Express completion policy in `execute`
-Products can accept declarative input by placing that grammar next to the
-customer concepts it represents.
+The customer Effect returns, fails, or is interrupted according to its own
+logic. Use Effect's `Clock`, `Schedule`, race, timeout, `Deferred`, Stream, and
+Scope primitives for deadlines, quiescence, supervised work, and explicit
+stop conditions.
-For example, a customer might decode a schema with only a model id, topology
-preset, and prompt family, then compile each case into:
+A runtime exit after readiness is committed as typed ledger evidence. It does
+not implicitly end the customer Effect. One program may fail fast on that
+evidence while another continues observing the remaining society.
-1. customer event classes and an `EventCatalog`;
-2. one versioned `simulator.define` value;
-3. a keyed mixed-runtime roster;
-4. an Effect program using `roster.startedAgents`, `Network`,
- `Society.Events`, and `Society.Ledger`;
-5. code graders composed over `Society.openLedger`.
+The run returns a `ProgramFinished` or `ClusterLost` outcome after ledger
+allocation succeeds. `ProgramFinished.exit` preserves customer success, typed
+failure, defect, or interruption. Infrastructure acquisition, append,
+controller, teardown, or completion failures stay distinct from behavioral
+results.
-That input may come from generated TypeScript, a database row, an HTTP
-request, or a customer-owned file format. The customer module owns the input
-unions, versioning, and migration policy.
+The submitters print one final JSON result containing the run namespace and
+bounded controller result. Applications decide how to map that result into
+their own exit codes, retries, operator messages, and report states.
-## Sweeps are orchestration
+## Sweeps remain application orchestration
-Use Effect and the surrounding job system for matrices and concurrency:
+A single simulator invocation is one definition-bound society and one ledger.
+Schedules, matrices, retries, sharding, naming, resumption, and aggregation
+stay in the calling application. For example, `packages/evals` submits every
+case-condition cell as its own `RunSpec` through the selected local or GKE
+profile, then persists the terminal attempt in its report database.
-```ts
-const results = yield* Effect.forEach(
- cases,
- runCase,
- { concurrency: 8 },
-);
-```
+This keeps suite orchestration failures separate from the evidence produced by
+an individual society.
-Schedules, retries, sharding, naming, resumption, and report aggregation stay
-at this layer. A single simulator run remains one definition-bound Effect and
-one completed ledger, keeping suite orchestration failures distinct from the
-evidence produced by a society.
+## Inspect completed artifacts
-## Inspect ledgers in code
+After a run publishes a completed receipt, both profiles retain exported files
+under the same relative path:
-Use the same definition and storage Layer that produced the run:
+```text
+{namespace}/ledger/{ledgerRef}/{manifest.json,records.ndjson,completion.json}
+```
+
+Local files are written directly below the artifact root selected during
+cluster setup. GKE runs build the active ledger on controller-local POSIX
+storage, then export the three completed artifacts to the Terraform-owned
+Cloud Storage bucket with `completion.json` last. The active GKE ledger is not
+a recovery guarantee for controller or node loss before that export finishes.
+Retrieve the three retained files, then validate them with the same complete
+event catalog:
```ts
import {
- readLedgerManifest,
+ EventCatalog,
+ coreEvents,
+} from "@moltzap/simulator";
+import {
+ openLedgerArtifacts,
} from "@moltzap/simulator/ledger";
+import {
+ runSpec,
+ experimentEvents,
+} from "./experiment.mjs";
-const inspect = Effect.gen(function* () {
- const manifest = yield* readLedgerManifest(ledgerRef);
- const verdict = yield* Society.openLedger(ledgerRef).pipe(
- Effect.flatMap(gradeLedger),
- );
- return { manifest, verdict };
-}).pipe(Effect.provide(Platform));
+const catalog = EventCatalog.merge(
+ coreEvents,
+ experimentEvents,
+);
+
+const ledger = yield* openLedgerArtifacts(
+ catalog,
+ receipt.ledger,
+ artifacts,
+ runSpec.id,
+);
```
-`readLedgerManifest` supports indexing without reading event evidence.
-`Society.openLedger` returns reusable exact-class streams after the definition,
-catalog, artifact digests, identities, count, sequence, and event schemas
-validate.
+Opening validates the definition identity, exact catalog, schemas, digests,
+run identity, count, event identities, and logical sequence before exposing
+reusable typed streams. It does not start a society.
diff --git a/eslint.shared.mjs b/eslint.shared.mjs
index 27486428b..079d6649a 100644
--- a/eslint.shared.mjs
+++ b/eslint.shared.mjs
@@ -269,7 +269,7 @@ export function rootEslintConfig(options = {}) {
},
packageIgnores,
{
- files: ["*.ts"],
+ files: ["*.ts", "examples/**/*.ts"],
languageOptions,
plugins: {
...guard.configs.strict.plugins,
diff --git a/knip.json b/knip.json
index b52066b35..e768729b8 100644
--- a/knip.json
+++ b/knip.json
@@ -46,6 +46,8 @@
"packages/evals": {
"entry": [
"src/cli.ts",
+ "src/execution.ts",
+ "src/peer-application.ts",
"src/**/*.test.ts",
"src/**/*.types-check.ts"
],
@@ -53,8 +55,12 @@
},
"packages/simulator": {
"entry": [
+ "src/cluster/controller/services.ts",
+ "src/cluster/controller/main.ts",
+ "src/cluster/profiles/gke.ts",
+ "src/cluster/profiles/local.ts",
+ "src/cluster/temporal.ts",
"src/**/*.test.ts",
- "src/**/*.integration.test.ts",
"src/**/*.types-check.ts",
"vitest*.config.mjs"
],
diff --git a/package.json b/package.json
index c3cfec2cf..c0de093c3 100644
--- a/package.json
+++ b/package.json
@@ -39,8 +39,6 @@
"docs:check:gates-test": "pnpm exec tsx scripts/__tests__/gates.test.ts",
"test:compute-next-version": "bash scripts/release/compute-next-version.test.sh",
"check:agent-setup": "bash scripts/repo/check-agent-setup.sh",
- "simulator:example": "pnpm simulator:example:check && node --experimental-strip-types examples/simulator/hello.ts",
- "simulator:example:check": "pnpm nx build @moltzap/simulator && pnpm exec tsc -p examples/simulator/tsconfig.json",
"test:pack:simulator": "pnpm nx build @moltzap/simulator && node scripts/test/simulator-packages.mjs",
"prepare": "husky && node scripts/setup/restore-tsgo-exec-bit.mjs",
"effect:source": "./scripts/setup/prepare-effect.sh",
@@ -54,6 +52,7 @@
"@mermaid-js/mermaid-cli": "^11.15.0",
"@types/node": "^25.5.0",
"@typescript/native": "npm:typescript@^7.0.2",
+ "@vitest/coverage-v8": "^3.2.4",
"eslint": "^9",
"eslint-plugin-agent-code-guard": "0.0.20",
"husky": "^9.0.0",
diff --git a/packages/evals/README.md b/packages/evals/README.md
index 67ba6d27a..31a0c7681 100644
--- a/packages/evals/README.md
+++ b/packages/evals/README.md
@@ -1,84 +1,82 @@
# MoltZap evaluations
-This private package is one code-first customer of `@moltzap/simulator`. It
-defines behavioral cases, runs mixed societies through the production router,
-grades durable ledger evidence, stores resumable reports, and publishes
-completed results to Phoenix.
+This private package is a code-first customer of `@moltzap/simulator`. It
+defines behavioral cases, runs mixed OpenClaw and NanoClaw societies through
+the simulator's Kubernetes path, grades durable ledger evidence, stores
+resumable SQLite reports, and publishes completed results to Phoenix.
The bundled baseline pairs sixteen cases with OpenClaw and NanoClaw target
-conditions. Every society also contains autonomous in-process Effect peers.
-The target receives principal instructions through its runtime-native gateway;
-all target-to-peer and peer-to-target traffic uses the same MoltZap protocol
-and router.
+conditions. Each matrix cell constructs one case-specific `RunSpec` and submits
+it through either the repository's local kind profile or its GKE profile. Case
+peers run as autonomous application containers. Target-to-peer and
+peer-to-target traffic uses the production MoltZap protocol and router.
## Execution model
```text
-principal
- │
- ├── OpenClaw RPC ──────── OpenClaw target ─┐
- └── NanoClaw socket ───── NanoClaw target ─┤
- ├── production router
-case-owned Effect peers ──────────────────────┘
- │
- └── observation gateways
-
-closed event catalog ── ledger ── transcript ── criteria / judge
- │
- └── SQLite report ── Phoenix
+evaluation sweep
+ │
+ └── generated per-cell RunSpec
+ │
+ ├── local kind ─┐
+ └── GKE ────────┴── Temporal controller
+ │
+ OpenClaw / NanoClaw target ──┤
+ case-owned peer containers ──┴── router
+ │
+ completed ledger artifacts
+ │
+ transcript ── criteria / judge
+ │
+ SQLite report ── Phoenix
```
-A native gateway output says what a runtime returned to its principal. A
-router commit says what an agent did on the social network. Grading keeps
+A native gateway output says what a target runtime returned to its principal.
+A router commit says what an agent did on the social network. Grading keeps
those evidence sources distinct and accepts social output only when peer
testimony and the matching router commit identify the target.
+Kubernetes, Kueue, Agent Sandbox, and Temporal objects stay outside case
+programs. The generated module injects the controller-owned infrastructure
+layer, while the case owns only its target runtime, peer plans, deadlines, and
+evidence policy.
+
## Source organization
| Module | Responsibility |
|---|---|
| `src/model.ts` | Branded identities and shared evaluation vocabulary |
-| `src/cases.ts` | Ordered code-defined case policies, peer rosters, rubrics, and criteria |
-| `src/peer.ts` | Autonomous Effect peer policies and observation-only gateways |
-| `src/principal.ts` | Evaluation-local adapters over native runtime gateways |
+| `src/cases.ts` | Ordered case programs, peer definitions, rubrics, and criteria |
+| `src/peer.ts` | Closed peer plans, container descriptors, and observation gateways |
+| `src/peer-application.ts` | Peer-container entrypoint and result bridge |
+| `src/principal.ts` | Evaluation-local adapters over native target gateways |
| `src/events.ts` | Complete evaluation event catalog and ledger projection |
-| `src/execution.ts` | Mixed-roster acquisition and bounded case execution |
-| `src/grading.ts` | Curated boundary re-exporting the transcript, judge, assessment, and calibration modules |
-| `src/transcript.ts` | Normalized transcripts, ledger projection, and evidence-ID invariants |
-| `src/judge.ts` | Provider-neutral judge bundle, closed judge failures, and result validation |
-| `src/assessment.ts` | Criterion decisions, assessment provenance, and one-semantic-call grading |
-| `src/calibration.ts` | The fixed calibration corpus and its behavioral run |
-| `src/judge-openai.ts` | Production OpenAI judge layer, prompt, and typed failure mapping |
+| `src/execution.ts` | Cell `RunSpec` construction, case execution, and result projection |
+| `src/submission.ts` | Generated module and local/GKE submission boundary |
+| `src/artifacts.ts` | Exact local or Cloud Storage ledger-artifact retrieval |
+| `src/grading.ts` | Transcript, judge, assessment, and calibration boundary |
| `src/sweep.ts` | Immutable plans, terminal attempts, reports, and state transitions |
-| `src/results.ts` | Report-local Effect SQL persistence and transactional resume |
+| `src/results.ts` | Report-local SQLite persistence and transactional resume |
| `src/phoenix.ts` | Completed-report publication boundary composed by the CLI |
-| `src/phoenix-client.ts` | The one Phoenix SDK boundary: typed request failures and Promise adaptation |
-| `src/phoenix-publication.ts` | Publication failure vocabulary and canonical JSON comparison |
-| `src/phoenix-dataset.ts` | The stable dataset catalog and its remote reconciliation |
-| `src/phoenix-experiment.ts` | Per-condition experiment identity, provenance, and reconciliation |
-| `src/phoenix-run.ts` | One idempotent experiment run per terminal local attempt |
-| `src/phoenix-evaluation.ts` | Per-criterion assessment rows materialized on each run |
| `src/cli.ts` | Operator configuration and commands at the application edge |
-This package is a private executable application rather than a customer
-library. Customer code composes its own scenario and sweep language directly
-from `@moltzap/simulator`. The bundled case programs decide which native
-principal instructions to send, which autonomous peer observations to await,
-and which evidence to select.
+This package is an executable application, not a customer library. Other
+customers compose their own scenario and sweep language directly from
+`@moltzap/simulator`.
## Adding a behavioral case
-1. Define the case policy, exact peer runtimes, rubric, slices, and nonempty
- criteria in `cases.ts`.
-2. Reuse a peer policy or add an autonomous policy in `peer.ts`. Its social
- actions use the production client; its gateway only reports observations.
-3. Add any new evidence class to `events.ts` before the simulator definition
- is constructed.
-4. Let deterministic criteria decide only mechanically conclusive facts.
- Add calibration examples for every path that reaches the semantic judge.
-5. Test both accepted evidence and the relevant rejection boundary.
+1. Define the case program, exact peer definitions, rubric, slices, and
+ nonempty criteria in `cases.ts`.
+2. Reuse a closed peer plan or add one in `peer.ts`. Its container uses the
+ production protocol client; its controller gateway reports observations
+ only.
+3. Add new evidence classes to `events.ts` before constructing the `RunSpec`.
+4. Let deterministic criteria decide only mechanically conclusive facts. Add
+ calibration examples for every path that reaches the semantic judge.
+5. Test accepted evidence and the relevant rejection boundaries.
-## Verification
+## Static verification
Run package tasks through Nx with the repository Node version:
@@ -89,57 +87,126 @@ mise x node@24.18.0 -- pnpm nx run @moltzap/evals:test
mise x node@24.18.0 -- pnpm nx run @moltzap/evals:lint
```
-Calibrate the full semantic-judge path before a live sweep:
+These checks validate the generated modules, peer bridge, artifact identities,
+ledger projection, grading, SQLite resume, and Phoenix behavior. They do not
+run or qualify a live local or GKE society.
+
+Calibrate the semantic judge separately:
```bash
OPENAI_API_KEY=... \
mise x node@24.18.0 -- pnpm nx run @moltzap/evals:calibrate
```
-Start the ordered 32-cell OpenClaw/NanoClaw report:
+## Running a report
+
+Run and resume require a clean, committed worktree. The report plan records the
+exact source revision, model IDs, runtime configuration, profile, controller
+and application images, Temporal address, ledger-artifact location, and one
+attempt per case-condition cell. Both images below must be immutable lowercase
+`@sha256:<64 hex>` references:
+
+- `MOLTZAP_SUPPORT_IMAGE` contains the evaluation peer application and is used
+ for every case-owned peer container. The repository-built controller image
+ satisfies this contract and may be used for both controller and support.
+- `MOLTZAP_NANOCLAW_IMAGE` is the distinct NanoClaw application image that
+ implements the shipped NanoClaw container entrypoint and gateway contract.
+
+Create the local cluster with an absolute artifact directory as described in
+the [local simulator profile](../simulator/local/README.md), then pass that same
+directory to the evaluation process:
```bash
OPENAI_API_KEY=... \
+ANTHROPIC_API_KEY=... \
+MOLTZAP_CONTROLLER_IMAGE=CONTROLLER_AT_SHA256 \
+MOLTZAP_SUPPORT_IMAGE=CONTROLLER_AT_SHA256 \
+MOLTZAP_NANOCLAW_IMAGE=NANOCLAW_AT_SHA256 \
+MOLTZAP_LOCAL_ARTIFACTS="$PWD/.moltzap/local-artifacts" \
+MOLTZAP_TEMPORAL_ADDRESS=127.0.0.1:7233 \
mise x node@24.18.0 -- pnpm nx run @moltzap/evals:eval -- \
- --report-id baseline-2026-07-29 \
+ --profile local \
+ --report-id baseline-2026-08-04 \
--openclaw-model "$OPENCLAW_MODEL" \
--nanoclaw-model "$NANOCLAW_MODEL"
```
-The command requires a clean worktree and records the exact source revision.
-Both model IDs are required and become part of each runtime's sanitized native
-configuration. Omit `--report-id` to derive one from the current UTC time.
+For GKE, use the [GKE simulator profile](../simulator/gke/README.md), push the
+controller/support image to its registry, authenticate `gcloud` for artifact
+readback, and provide the selected cluster and retained bucket:
-Result bundles live at
-`.moltzap/evals/results/.sqlite`; run ledgers live under
-`.moltzap/evals/ledgers/`. SQLite is the mutable report authority. Each matrix
-cell is committed atomically, and resume executes only cells missing from an
-exactly matching plan:
+```bash
+OPENAI_API_KEY=... \
+ANTHROPIC_API_KEY=... \
+MOLTZAP_KUBE_CONTEXT=EXPLICIT_KUBE_CONTEXT \
+MOLTZAP_GKE_ARTIFACT_BUCKET=ARTIFACT_BUCKET \
+MOLTZAP_TEMPORAL_ADDRESS=TEMPORAL_HOST:7233 \
+MOLTZAP_CONTROLLER_IMAGE=REGISTRY/CONTROLLER@sha256:DIGEST \
+MOLTZAP_SUPPORT_IMAGE=REGISTRY/CONTROLLER@sha256:DIGEST \
+MOLTZAP_NANOCLAW_IMAGE=REGISTRY/NANOCLAW@sha256:DIGEST \
+ mise x node@24.18.0 -- pnpm nx run @moltzap/evals:eval -- \
+ --profile gke \
+ --report-id baseline-2026-08-04 \
+ --openclaw-model "$OPENCLAW_MODEL" \
+ --nanoclaw-model "$NANOCLAW_MODEL"
+```
+
+Omit `--report-id` on `eval` to derive one from the current UTC time. Result
+bundles live at `.moltzap/evals/results/.sqlite`. Completed ledger
+artifacts remain owned by the selected simulator profile:
+
+```text
+local: {MOLTZAP_LOCAL_ARTIFACTS}/{namespace}/ledger/{ledgerRef}/{artifact}
+GKE: gs://{MOLTZAP_GKE_ARTIFACT_BUCKET}/{namespace}/ledger/{ledgerRef}/{artifact}
+```
+
+Each completed ledger contains `manifest.json`, `records.ndjson`, and
+`completion.json`. The evaluation process retrieves those exact artifacts and
+validates them against the case catalog, definition, receipt, record sequence,
+and digests before grading.
+
+Resume uses the same profile, images, models, and artifact authority:
```bash
OPENAI_API_KEY=... \
+ANTHROPIC_API_KEY=... \
+MOLTZAP_CONTROLLER_IMAGE=CONTROLLER_AT_SHA256 \
+MOLTZAP_SUPPORT_IMAGE=CONTROLLER_AT_SHA256 \
+MOLTZAP_NANOCLAW_IMAGE=NANOCLAW_AT_SHA256 \
+MOLTZAP_LOCAL_ARTIFACTS="$PWD/.moltzap/local-artifacts" \
+MOLTZAP_TEMPORAL_ADDRESS=127.0.0.1:7233 \
mise x node@24.18.0 -- pnpm nx run @moltzap/evals:resume -- \
- --report-id baseline-2026-07-29 \
+ --profile local \
+ --report-id baseline-2026-08-04 \
--openclaw-model "$OPENCLAW_MODEL" \
--nanoclaw-model "$NANOCLAW_MODEL"
```
-Behavioral `passed`, `failed`, and `undecided` verdicts are report data.
-Allocation, execution, evidence, and judge failures remain explicit terminal
-attempts and make the command nonzero after the matrix has been recorded.
+SQLite is the mutable report authority. Each truthful terminal cell commits
+atomically, and resume executes only cells missing from an exactly matching
+plan. Allocation and controller failures become explicit terminal attempts.
+After a completed receipt exists, unavailable or invalid artifacts become an
+`EvidenceRejectedAttempt` so the receipt is retained and the society is not
+silently rerun. A submission failure before any truthful receipt rolls back the
+cell for a later retry. Judge unavailability is also recorded explicitly.
+
+Behavioral `passed`, `failed`, and `undecided` verdicts remain report data.
+Operationally incomplete reports return nonzero only after every terminal
+attempt that can be recorded has been committed.
+
+## Publishing
Publish a completed report to a self-hosted or managed Phoenix instance:
```bash
PHOENIX_HOST=http://localhost:6006 \
mise x node@24.18.0 -- pnpm nx run @moltzap/evals:publish -- \
- --report-id baseline-2026-07-29
+ --report-id baseline-2026-08-04
```
Set `PHOENIX_API_KEY` when required. Repeated publication reconciles the stable
case dataset, one experiment per condition, and every report attempt before
returning the Phoenix experiment URLs.
-Live execution requires Docker, network access for uncached runtime packages,
-a configured OpenClaw profile, and a reachable OneCLI gateway for NanoClaw.
-Runtime failures stay visible in the report.
+This repository has static coverage for both profiles. It does not claim that
+a live local or GKE evaluation has completed successfully.
diff --git a/packages/evals/package.json b/packages/evals/package.json
index 6989b166c..2b101d762 100644
--- a/packages/evals/package.json
+++ b/packages/evals/package.json
@@ -5,6 +5,9 @@
"private": true,
"license": "MIT",
"type": "module",
+ "files": [
+ "dist"
+ ],
"scripts": {
"build": "nx run @moltzap/evals:build",
"lint": "nx run @moltzap/evals:lint",
diff --git a/packages/evals/src/README.md b/packages/evals/src/README.md
index 9f7a91a59..7e0e00d23 100644
--- a/packages/evals/src/README.md
+++ b/packages/evals/src/README.md
@@ -1,5 +1,12 @@
# Evaluation application boundary
+> **Implementation transition:** The [main-track Kubernetes
+> contract](../../../docs/decisions/20260801-main-simulator-runs-container-societies-on-kubernetes.md)
+> moves enabled attempts to the core `Run.execute` Kubernetes path. Host
+> acquisition below describes the implementation being replaced. Peer policy
+> remains evaluation-owned but runs in one peer application container and
+> reports through its exact evaluation-owned observation bridge.
+
This directory is a private application above `@moltzap/simulator`.
`cli.ts` is its executable entry point. Customer society and scenario
languages compose the simulator package directly instead of depending on an
diff --git a/packages/evals/src/artifacts.test.ts b/packages/evals/src/artifacts.test.ts
new file mode 100644
index 000000000..1a2877523
--- /dev/null
+++ b/packages/evals/src/artifacts.test.ts
@@ -0,0 +1,176 @@
+import { Path } from "@effect/platform";
+import { NodeContext } from "@effect/platform-node";
+import { assert, it } from "@effect/vitest";
+import { ledgerRef } from "@moltzap/simulator/ledger";
+import { Effect, Option, Schema } from "effect";
+import {
+ EvaluationArtifactReadFailed,
+ evaluationArtifactBucket,
+ evaluationArtifactLocation,
+ localArtifactRoot,
+ readEvaluationLedgerArtifactsWith,
+ type EvaluationArtifactLocation,
+ type EvaluationArtifactOperations,
+ type EvaluationArtifactStorage,
+} from "./artifacts.js";
+
+/* eslint-disable agent-code-guard/no-hardcoded-assertion-literals -- These tests pin the external artifact identities and immutable file set. */
+
+const test = it.effect;
+const REF = Schema.decodeSync(ledgerRef)(
+ "00000000-0000-4000-8000-000000000917",
+);
+const ARTIFACTS = {
+ manifest: "manifest contents",
+ records: "record contents",
+ completion: "completion contents",
+} as const;
+
+function content(identity: string): string {
+ if (identity.endsWith("/manifest.json")) {
+ return ARTIFACTS.manifest;
+ }
+ if (identity.endsWith("/records.ndjson")) {
+ return ARTIFACTS.records;
+ }
+ if (identity.endsWith("/completion.json")) {
+ return ARTIFACTS.completion;
+ }
+ throw new Error(`unexpected artifact identity ${identity}`);
+}
+
+function operations(
+ fileIdentities: string[],
+ objectIdentities: string[],
+): EvaluationArtifactOperations {
+ return Object.freeze({
+ readFile: (identity: string) =>
+ Effect.sync(() => {
+ fileIdentities.push(identity);
+ return content(identity);
+ }),
+ readObject: (identity: string) =>
+ Effect.sync(() => {
+ objectIdentities.push(identity);
+ return content(identity);
+ }),
+ });
+}
+
+const localArtifactStorage = Effect.gen(function* () {
+ const path = yield* Path.Path;
+ return {
+ profile: "local",
+ root: Option.getOrThrow(
+ localArtifactRoot(path, "/var/lib/moltzap/artifacts"),
+ ),
+ } as const satisfies EvaluationArtifactStorage;
+});
+
+const gkeStorage = {
+ profile: "gke",
+ bucket: Option.getOrThrow(evaluationArtifactBucket("moltzap-eval-artifacts")),
+} as const satisfies EvaluationArtifactStorage;
+
+function locate(storage: EvaluationArtifactStorage) {
+ return Option.getOrThrow(
+ evaluationArtifactLocation(storage, "mz-run-917", REF),
+ );
+}
+
+test("reads the exact local namespace ledger artifact set", () => {
+ const files: string[] = [];
+ const objects: string[] = [];
+ return localArtifactStorage.pipe(
+ Effect.flatMap((storage) =>
+ readEvaluationLedgerArtifactsWith(
+ locate(storage),
+ operations(files, objects),
+ ),
+ ),
+ Effect.tap((artifacts) => {
+ assert.deepStrictEqual(artifacts, ARTIFACTS);
+ assert.deepStrictEqual(objects, []);
+ assert.deepStrictEqual(
+ [...files].sort((left, right) => left.localeCompare(right)),
+ [
+ `/var/lib/moltzap/artifacts/mz-run-917/ledger/${REF}/completion.json`,
+ `/var/lib/moltzap/artifacts/mz-run-917/ledger/${REF}/manifest.json`,
+ `/var/lib/moltzap/artifacts/mz-run-917/ledger/${REF}/records.ndjson`,
+ ],
+ );
+ }),
+ Effect.provide(NodeContext.layer),
+ );
+});
+
+test("reads the exact GCS namespace ledger artifact set", () => {
+ const files: string[] = [];
+ const objects: string[] = [];
+ return readEvaluationLedgerArtifactsWith(
+ locate(gkeStorage),
+ operations(files, objects),
+ ).pipe(
+ Effect.tap((artifacts) => {
+ assert.deepStrictEqual(artifacts, ARTIFACTS);
+ assert.deepStrictEqual(files, []);
+ assert.deepStrictEqual(
+ [...objects].sort((left, right) => left.localeCompare(right)),
+ [
+ `gs://moltzap-eval-artifacts/mz-run-917/ledger/${REF}/completion.json`,
+ `gs://moltzap-eval-artifacts/mz-run-917/ledger/${REF}/manifest.json`,
+ `gs://moltzap-eval-artifacts/mz-run-917/ledger/${REF}/records.ndjson`,
+ ],
+ );
+ }),
+ Effect.provide(NodeContext.layer),
+ );
+});
+
+test("surfaces an unavailable artifact as an operational read failure", () =>
+ localArtifactStorage.pipe(
+ Effect.flatMap((storage) =>
+ readEvaluationLedgerArtifactsWith(locate(storage), {
+ readFile: (identity) =>
+ identity.endsWith("/records.ndjson")
+ ? Effect.fail("records are unavailable")
+ : Effect.succeed(content(identity)),
+ readObject: () => Effect.dieMessage("unexpected object read"),
+ }),
+ ),
+ Effect.flip,
+ Effect.tap((failure) => {
+ assert.instanceOf(failure, EvaluationArtifactReadFailed);
+ assert.strictEqual(failure.artifact, "records");
+ assert.strictEqual(failure.profile, "local");
+ }),
+ Effect.provide(NodeContext.layer),
+ ));
+
+test("refuses a relative artifact root before any run is addressed", () =>
+ Path.Path.pipe(
+ Effect.tap((path) => {
+ assert.isTrue(Option.isNone(localArtifactRoot(path, "artifacts")));
+ assert.isTrue(Option.isSome(localArtifactRoot(path, "/artifacts")));
+ }),
+ Effect.provide(NodeContext.layer),
+ ));
+
+test("refuses an artifact bucket Cloud Storage would not name", () =>
+ Effect.sync(() => {
+ assert.isTrue(Option.isNone(evaluationArtifactBucket("Moltzap-Artifacts")));
+ assert.isTrue(Option.isNone(evaluationArtifactBucket("moltzap/artifacts")));
+ assert.isTrue(Option.isSome(evaluationArtifactBucket("moltzap-artifacts")));
+ }));
+
+test("refuses a ledger ref that is not one storage path segment", () =>
+ Effect.sync(() => {
+ const forged = Schema.decodeSync(ledgerRef)("../outside");
+ const located: Option.Option =
+ evaluationArtifactLocation(gkeStorage, "mz-run-917", forged);
+ assert.isTrue(Option.isNone(located));
+ }));
+
+/* eslint-enable agent-code-guard/no-hardcoded-assertion-literals -- External artifact identity assertions end here. */
+
+// @agent-code-guard/regression-only: the identities are fixed external contracts and each rejection example pins one candidate the constructors must refuse before a run is addressed
diff --git a/packages/evals/src/artifacts.ts b/packages/evals/src/artifacts.ts
new file mode 100644
index 000000000..875c6fede
--- /dev/null
+++ b/packages/evals/src/artifacts.ts
@@ -0,0 +1,223 @@
+/** @file Exact local/GCS retrieval of completed evaluation ledger artifacts. */
+
+import { Command, FileSystem, Path } from "@effect/platform";
+import type { CommandExecutor } from "@effect/platform/CommandExecutor";
+import {
+ ledgerArtifactFiles,
+ type CompletedLedgerArtifacts,
+ type LedgerArtifact,
+ type LedgerRef,
+} from "@moltzap/simulator/ledger";
+import { Brand, Effect, Option, Schema } from "effect";
+
+/**
+ * Absolute host directory a local run writes its completed artifacts under.
+ * Only `localArtifactRoot` produces one, so no read re-checks absoluteness.
+ */
+export type LocalArtifactRoot = string & Brand.Brand<"LocalArtifactRoot">;
+
+const asLocalArtifactRoot = Brand.nominal();
+
+const artifactBucket = Schema.String.pipe(
+ Schema.pattern(/^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$/u),
+ Schema.brand("ArtifactBucket"),
+);
+/** Cloud Storage bucket a GKE run writes its completed artifacts into. */
+export type ArtifactBucket = typeof artifactBucket.Type;
+
+/**
+ * A ledger ref is only a storage identity; the profiles that happen to store a
+ * ledger under its own directory need one path segment, and a ref carrying a
+ * separator or a parent reference would address a neighbouring run instead.
+ */
+const ledgerDirectory = Schema.UUID.pipe(Schema.brand("LedgerDirectory"));
+/** One completed ledger addressed as exactly one storage path segment. */
+type LedgerDirectory = typeof ledgerDirectory.Type;
+
+const decodeLedgerDirectory = Schema.decodeUnknownOption(ledgerDirectory);
+const decodeArtifactBucket = Schema.decodeUnknownOption(artifactBucket);
+
+/** Artifact retrieval failed before canonical ledger validation. */
+export class EvaluationArtifactReadFailed extends Schema.TaggedError()(
+ "EvaluationArtifactReadFailed",
+ {
+ profile: Schema.Literal("local", "gke"),
+ artifact: Schema.Literal("manifest", "records", "completion"),
+ detail: Schema.NonEmptyString,
+ },
+) {}
+
+/** Replaceable read boundaries used by deterministic retrieval tests. */
+export interface EvaluationArtifactOperations {
+ readonly readFile: (
+ path: string,
+ ) => Effect.Effect;
+ readonly readObject: (
+ url: string,
+ ) => Effect.Effect;
+}
+
+// The target belongs to the profile, not to a run: a location carrying both an
+// optional directory and an optional bucket can be built for a profile whose
+// own target was never resolved, and every read then has to re-decide that.
+/** Validated artifact target owned by the profile a sweep runs on. */
+export type EvaluationArtifactStorage =
+ | Readonly<{ profile: "local"; root: LocalArtifactRoot }>
+ | Readonly<{ profile: "gke"; bucket: ArtifactBucket }>;
+
+/** Host storage identity for one completed simulator run. */
+export interface EvaluationArtifactLocation {
+ readonly storage: EvaluationArtifactStorage;
+ readonly namespace: string;
+ readonly ledger: LedgerDirectory;
+}
+
+/**
+ * Accept an artifact root only where the host path service calls it absolute.
+ * @param path Platform path service that decides absoluteness.
+ * @param value Candidate root read from the host environment.
+ * @returns The branded root, absent when the candidate is relative.
+ */
+export function localArtifactRoot(
+ path: Path.Path,
+ value: string,
+): Option.Option {
+ return path.isAbsolute(value)
+ ? Option.some(asLocalArtifactRoot(value))
+ : Option.none();
+}
+
+/**
+ * Accept a Cloud Storage bucket named the way Cloud Storage names buckets.
+ * @param value Candidate bucket read from the host environment.
+ * @returns The branded bucket, absent when the name is not one.
+ */
+export function evaluationArtifactBucket(
+ value: string,
+): Option.Option {
+ return decodeArtifactBucket(value);
+}
+
+/**
+ * Address one completed run inside the artifact storage its profile owns.
+ * @param storage Validated target owned by the profile the run executed on.
+ * @param namespace Run namespace the simulator submitter reported.
+ * @param ref Ledger identity the controller committed for the run.
+ * @returns The addressed location, absent when the ref is not one segment.
+ */
+export function evaluationArtifactLocation(
+ storage: EvaluationArtifactStorage,
+ namespace: string,
+ ref: LedgerRef,
+): Option.Option {
+ return decodeLedgerDirectory(ref).pipe(
+ Option.map((ledger) => Object.freeze({ storage, namespace, ledger })),
+ );
+}
+
+const liveOperations: EvaluationArtifactOperations<
+ FileSystem.FileSystem | CommandExecutor
+> = Object.freeze({
+ readFile: (path: string) =>
+ FileSystem.FileSystem.pipe(
+ Effect.flatMap((fileSystem) => fileSystem.readFileString(path)),
+ ),
+ readObject: (url: string) =>
+ Command.string(
+ Command.make("gcloud", "storage", "cat", url).pipe(
+ Command.stderr("inherit"),
+ ),
+ ),
+});
+
+function readFailure(
+ location: EvaluationArtifactLocation,
+ artifact: LedgerArtifact,
+ cause: unknown,
+): EvaluationArtifactReadFailed {
+ return EvaluationArtifactReadFailed.make({
+ profile: location.storage.profile,
+ artifact,
+ detail: String(cause).trim() || "artifact read failed",
+ });
+}
+
+function localIdentity(
+ root: LocalArtifactRoot,
+ location: EvaluationArtifactLocation,
+ artifact: LedgerArtifact,
+ path: Path.Path,
+): string {
+ return path.join(
+ root,
+ location.namespace,
+ "ledger",
+ location.ledger,
+ ledgerArtifactFiles[artifact],
+ );
+}
+
+function gcsIdentity(
+ bucket: ArtifactBucket,
+ location: EvaluationArtifactLocation,
+ artifact: LedgerArtifact,
+): string {
+ return `gs://${bucket}/${encodeURIComponent(location.namespace)}/ledger/${location.ledger}/${ledgerArtifactFiles[artifact]}`;
+}
+
+function readArtifact(
+ location: EvaluationArtifactLocation,
+ artifact: LedgerArtifact,
+ operations: EvaluationArtifactOperations,
+ path: Path.Path,
+) {
+ const storage = location.storage;
+ const read =
+ storage.profile === "local"
+ ? operations.readFile(
+ localIdentity(storage.root, location, artifact, path),
+ )
+ : operations.readObject(gcsIdentity(storage.bucket, location, artifact));
+ return read.pipe(
+ Effect.mapError((cause) => readFailure(location, artifact, cause)),
+ );
+}
+
+/**
+ * Retrieve the three exact immutable artifacts through injected operations.
+ * @param location Profile-owned namespace and ledger identity.
+ * @param operations Replaceable local-file and Cloud Storage readers.
+ * @returns The three retrieved artifact texts without interpreting them.
+ */
+export function readEvaluationLedgerArtifactsWith(
+ location: EvaluationArtifactLocation,
+ operations: EvaluationArtifactOperations,
+): Effect.Effect<
+ CompletedLedgerArtifacts,
+ EvaluationArtifactReadFailed,
+ Path.Path | Requirements
+> {
+ return Effect.gen(function* () {
+ const path = yield* Path.Path;
+ const [manifest, records, completion] = yield* Effect.all(
+ [
+ readArtifact(location, "manifest", operations, path),
+ readArtifact(location, "records", operations, path),
+ readArtifact(location, "completion", operations, path),
+ ] as const,
+ { concurrency: 3 },
+ );
+ return { manifest, records, completion };
+ }).pipe(Effect.withSpan("readEvaluationLedgerArtifactsWith"));
+}
+
+/**
+ * Retrieve completed artifacts from the selected repository-owned profile.
+ * @param location Profile-owned namespace and ledger identity.
+ * @returns The three artifact texts read through live host operations.
+ */
+export function readEvaluationLedgerArtifacts(
+ location: EvaluationArtifactLocation,
+) {
+ return readEvaluationLedgerArtifactsWith(location, liveOperations);
+}
diff --git a/packages/evals/src/cases.test.ts b/packages/evals/src/cases.test.ts
index 15a0be1c7..10a085112 100644
--- a/packages/evals/src/cases.test.ts
+++ b/packages/evals/src/cases.test.ts
@@ -23,7 +23,10 @@ import {
decodeEvaluationCaseId,
decodeEvaluationEvidenceId,
} from "./model.js";
-import type { EvaluationPeerGateway, EvaluationPeerRuntime } from "./peer.js";
+import type {
+ EvaluationPeerDefinition,
+ EvaluationPeerGateway,
+} from "./peer.js";
const test = it.effect;
const OBSERVE_PEER_OPERATION = "observe:peer";
@@ -35,8 +38,8 @@ const PRINCIPAL_OUTPUT_ID = decodeEvaluationEvidenceId(
const PEER_OUTPUT_ID = decodeEvaluationEvidenceId("case-test:peer-output");
const PASSED_VERDICT = "passed";
-type DirectTestPeerRuntimes = Readonly<{
- [PEER_AGENT_NAME]: EvaluationPeerRuntime;
+type DirectTestPeerDefinitions = Readonly<{
+ [PEER_AGENT_NAME]: EvaluationPeerDefinition;
}>;
function evidence(text: string): CriterionEvidence {
@@ -66,7 +69,7 @@ function peer(
};
}
-function peers(): EvaluationCasePeers {
+function peers(): EvaluationCasePeers {
return {
[PEER_AGENT_NAME]: peer(
PEER_AGENT_NAME,
@@ -76,7 +79,10 @@ function peers(): EvaluationCasePeers {
}
interface ProgramRecorder {
- readonly context: EvaluationCaseProgramContext;
+ readonly context: EvaluationCaseProgramContext<
+ DirectTestPeerDefinitions,
+ never
+ >;
readonly operations: readonly string[];
}
@@ -86,7 +92,10 @@ function programRecorder(): ProgramRecorder {
[roster[PEER_AGENT_NAME], "peer"],
]);
const operations: string[] = [];
- const context: EvaluationCaseProgramContext = {
+ const context: EvaluationCaseProgramContext<
+ DirectTestPeerDefinitions,
+ never
+ > = {
peers: roster,
instruct: (message) =>
Effect.sync(() => {
diff --git a/packages/evals/src/cases.ts b/packages/evals/src/cases.ts
index 3cdd31c1c..f77e90aed 100644
--- a/packages/evals/src/cases.ts
+++ b/packages/evals/src/cases.ts
@@ -2,7 +2,7 @@
import type { Part } from "@moltzap/protocol/message";
import type { SimulatorDefinitionId } from "@moltzap/simulator";
-import type { StartedAgent } from "@moltzap/simulator/runtime";
+import type { StartedAgent } from "@moltzap/simulator/agents";
import { Array as Arr, Effect, type Option } from "effect";
import type { NonEmptyReadonlyArray } from "effect/Array";
import {
@@ -13,8 +13,8 @@ import {
openingPeerRuntime,
orderedGroupPeerRuntime,
selectedResponsePeerRuntime,
+ type EvaluationPeerDefinition,
type EvaluationPeerGateway,
- type EvaluationPeerRuntime,
} from "./peer.js";
import {
CriterionDecided,
@@ -62,9 +62,9 @@ export interface CriterionDefinition {
readonly decide: (evidence: CriterionEvidence) => CriterionDecision;
}
-/** Code-peer runtimes keyed only by the autonomous roles one case needs. */
-export type EvaluationCasePeerRuntimes = Readonly<
- Record
+/** Image-independent peers keyed only by the autonomous roles one case needs. */
+export type EvaluationCasePeerDefinitions = Readonly<
+ Record
>;
/** One acquired autonomous peer and its observation-only gateway. */
@@ -75,10 +75,10 @@ export type EvaluationCasePeer = StartedAgent<
/** Exact acquired peers corresponding to one case's keyed runtime record. */
export type EvaluationCasePeers<
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
> = Readonly<{
[Name in Exclude<
- typeof TARGET_AGENT_NAME | Extract,
+ typeof TARGET_AGENT_NAME | Extract,
typeof TARGET_AGENT_NAME
>]: EvaluationCasePeer;
}>;
@@ -90,10 +90,10 @@ export type EvaluationCasePeers<
* gateways expose autonomous observations only; they do not accept commands.
*/
export interface EvaluationCaseProgramContext<
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
Failure,
> {
- readonly peers: EvaluationCasePeers;
+ readonly peers: EvaluationCasePeers;
readonly instruct: (
message: string,
) => Effect.Effect, Failure>;
@@ -109,10 +109,10 @@ export interface EvaluationCaseProgramContext<
}
/** Runtime-independent case policy interpreted by one concrete condition. */
-type EvaluationCaseProgram = <
- Failure,
->(
- context: EvaluationCaseProgramContext,
+type EvaluationCaseProgram<
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
+> = (
+ context: EvaluationCaseProgramContext,
) => Effect.Effect;
/** Immutable case information consumed by plans, grading, and reports. */
@@ -128,17 +128,17 @@ export interface EvaluationCaseMetadata {
/** Rank-2 consumer that preserves an otherwise hidden exact peer roster. */
interface EvaluationCaseDefinitionConsumer {
- readonly execute: (
- definition: EvaluationCaseDefinition,
+ readonly execute: (
+ definition: EvaluationCaseDefinition,
) => Result;
}
/** Metadata plus the exact autonomous peer roster and executable policy. */
export interface EvaluationCaseDefinition<
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
> extends EvaluationCaseMetadata {
- readonly peers: PeerRuntimes;
- readonly program: EvaluationCaseProgram;
+ readonly peers: PeerDefinitions;
+ readonly program: EvaluationCaseProgram;
readonly withDefinition: (
consumer: EvaluationCaseDefinitionConsumer,
) => Result;
@@ -257,9 +257,11 @@ function freezeCriterion(definition: CriterionDefinition): CriterionDefinition {
});
}
-function defineCase(
- definition: Omit, "withDefinition">,
-): EvaluationCaseDefinition {
+function defineCase<
+ const PeerDefinitions extends EvaluationCasePeerDefinitions,
+>(
+ definition: Omit, "withDefinition">,
+): EvaluationCaseDefinition {
const [firstCriterion, ...remainingCriteria] = definition.criteria;
return Object.freeze({
...definition,
@@ -271,7 +273,7 @@ function defineCase(
...remainingCriteria.map(freezeCriterion),
]),
withDefinition(
- this: EvaluationCaseDefinition,
+ this: EvaluationCaseDefinition,
consumer: EvaluationCaseDefinitionConsumer,
): Result {
return consumer.execute(this);
@@ -285,34 +287,36 @@ function freezeCatalog(
return Object.freeze(definitions);
}
-type DirectPeerRuntimes = Readonly<{
- [PEER_AGENT_NAME]: EvaluationPeerRuntime;
+type DirectPeerDefinitions = Readonly<{
+ [PEER_AGENT_NAME]: EvaluationPeerDefinition;
}>;
-type SpeakingGroupPeerRuntimes = Readonly<{
- [PEER_AGENT_NAME]: EvaluationPeerRuntime;
- [SOURCE_AGENT_NAME]: EvaluationPeerRuntime;
- [OBSERVER_1_AGENT_NAME]: EvaluationPeerRuntime;
+type SpeakingGroupPeerDefinitions = Readonly<{
+ [PEER_AGENT_NAME]: EvaluationPeerDefinition;
+ [SOURCE_AGENT_NAME]: EvaluationPeerDefinition;
+ [OBSERVER_1_AGENT_NAME]: EvaluationPeerDefinition;
}>;
-type SilentGroupPeerRuntimes = Readonly<{
- [PEER_AGENT_NAME]: EvaluationPeerRuntime;
- [OBSERVER_1_AGENT_NAME]: EvaluationPeerRuntime;
- [OBSERVER_2_AGENT_NAME]: EvaluationPeerRuntime;
+type SilentGroupPeerDefinitions = Readonly<{
+ [PEER_AGENT_NAME]: EvaluationPeerDefinition;
+ [OBSERVER_1_AGENT_NAME]: EvaluationPeerDefinition;
+ [OBSERVER_2_AGENT_NAME]: EvaluationPeerDefinition;
}>;
-type CrossConversationPeerRuntimes = Readonly<{
- [SOURCE_AGENT_NAME]: EvaluationPeerRuntime;
- [PROBE_AGENT_NAME]: EvaluationPeerRuntime;
+type CrossConversationPeerDefinitions = Readonly<{
+ [SOURCE_AGENT_NAME]: EvaluationPeerDefinition;
+ [PROBE_AGENT_NAME]: EvaluationPeerDefinition;
}>;
-type PrincipalPeerRuntimes = Readonly>;
+type PrincipalPeerDefinitions = Readonly<
+ Record
+>;
function directProgram(
instruction: string,
-): EvaluationCaseProgram {
+): EvaluationCaseProgram {
return (
- context: EvaluationCaseProgramContext,
+ context: EvaluationCaseProgramContext,
) =>
Effect.gen(function* () {
yield* context.instruct(instruction);
@@ -322,9 +326,12 @@ function directProgram(
function speakingGroupProgram(
instruction: string,
-): EvaluationCaseProgram {
+): EvaluationCaseProgram {
return (
- context: EvaluationCaseProgramContext,
+ context: EvaluationCaseProgramContext<
+ SpeakingGroupPeerDefinitions,
+ Failure
+ >,
) =>
Effect.gen(function* () {
yield* context.instruct(instruction);
@@ -336,9 +343,9 @@ function speakingGroupProgram(
function silentGroupProgram(
instruction: string,
-): EvaluationCaseProgram {
+): EvaluationCaseProgram {
return (
- context: EvaluationCaseProgramContext,
+ context: EvaluationCaseProgramContext,
) =>
Effect.gen(function* () {
yield* context.instruct(instruction);
@@ -351,10 +358,10 @@ function silentGroupProgram(
function crossConversationProgram(
sourceInstruction: string,
probeInstruction: string,
-): EvaluationCaseProgram {
+): EvaluationCaseProgram {
return (
context: EvaluationCaseProgramContext<
- CrossConversationPeerRuntimes,
+ CrossConversationPeerDefinitions,
Failure
>,
) =>
@@ -368,9 +375,9 @@ function crossConversationProgram(
function principalProgram(
instruction: string,
-): EvaluationCaseProgram {
+): EvaluationCaseProgram {
return (
- context: EvaluationCaseProgramContext,
+ context: EvaluationCaseProgramContext,
) =>
Effect.gen(function* () {
const output = yield* context.instruct(instruction);
@@ -380,9 +387,9 @@ function principalProgram(
function identityProgram(
instruction: string,
-): EvaluationCaseProgram {
+): EvaluationCaseProgram {
return (
- context: EvaluationCaseProgramContext,
+ context: EvaluationCaseProgramContext,
) =>
Effect.gen(function* () {
yield* context.observeContext(context.peers[PEER_AGENT_NAME]);
@@ -415,7 +422,7 @@ function groupInstruction(name: string): string {
function directPeers(
caseId: EvaluationCaseId,
messages: NonEmptyReadonlyArray,
-): DirectPeerRuntimes {
+): DirectPeerDefinitions {
return {
[PEER_AGENT_NAME]: selectedResponsePeerRuntime(
caseId,
@@ -429,7 +436,7 @@ function groupPeers(
caseId: EvaluationCaseId,
announcement: string,
question: string,
-): SpeakingGroupPeerRuntimes {
+): SpeakingGroupPeerDefinitions {
return {
[PEER_AGENT_NAME]: orderedGroupPeerRuntime({
caseId,
@@ -451,7 +458,7 @@ function groupPeers(
function silentGroupPeers(
caseId: EvaluationCaseId,
question: string,
-): SilentGroupPeerRuntimes {
+): SilentGroupPeerDefinitions {
return {
[PEER_AGENT_NAME]: groupResponsePeerRuntime({
caseId,
@@ -469,7 +476,7 @@ function crossConversationPeers(
caseId: EvaluationCaseId,
setupMessages: NonEmptyReadonlyArray,
probe: string,
-): CrossConversationPeerRuntimes {
+): CrossConversationPeerDefinitions {
return {
[SOURCE_AGENT_NAME]: contextPeerRuntime(
caseId,
diff --git a/packages/evals/src/cli.ts b/packages/evals/src/cli.ts
index d29633c72..5361d1626 100644
--- a/packages/evals/src/cli.ts
+++ b/packages/evals/src/cli.ts
@@ -4,11 +4,13 @@
import { Command as CliCommand, Options } from "@effect/cli";
import { Command, Path } from "@effect/platform";
import { NodeContext, NodeRuntime } from "@effect/platform-node";
+import type { CompletedLedgerReceipt } from "@moltzap/simulator";
import {
- simulatorLayer,
- type CompletedLedgerReceipt,
-} from "@moltzap/simulator";
-import { DateTime, Duration, Either, Effect, Option, Schema } from "effect";
+ LedgerStorageError,
+ type CompletedLedgerArtifacts,
+} from "@moltzap/simulator/ledger";
+import { image, type Image } from "@moltzap/simulator/agents";
+import { Config, DateTime, Duration, Effect, Option, Schema } from "effect";
import type { NonEmptyReadonlyArray } from "effect/Array";
import {
evaluationCase,
@@ -17,13 +19,23 @@ import {
type EvaluationCaseMetadata,
} from "./cases.js";
import {
- behavioralEvaluation,
EvaluationExecutionFailed,
nanoclawEvaluationCondition,
+ openEvaluationLedger,
openClawEvaluationCondition,
+ projectEvaluationControllerResult,
type EvaluationCondition,
type EvaluationExecutionResult,
} from "./execution.js";
+import {
+ evaluationArtifactBucket,
+ evaluationArtifactLocation,
+ localArtifactRoot,
+ readEvaluationLedgerArtifacts,
+ type ArtifactBucket,
+ type EvaluationArtifactStorage,
+ type LocalArtifactRoot,
+} from "./artifacts.js";
import {
GradeCompleted,
GradingRefused,
@@ -35,7 +47,12 @@ import {
transcriptFromLedger,
type EvaluationTranscript,
} from "./grading.js";
-import { decodeJudgePolicyId, type JudgePolicyId } from "./model.js";
+import {
+ decodeJudgePolicyId,
+ type EvaluationConditionId,
+ type EvaluationConditionName,
+ type JudgePolicyId,
+} from "./model.js";
import { PhoenixPublisher, phoenixPublisherLive } from "./phoenix.js";
import {
createStoredEvaluationReport,
@@ -50,26 +67,31 @@ import {
EvaluationCasePlan,
EvaluationConditionPlan,
EvaluationReportPlan,
+ GkeEvaluationInfrastructure,
EvidenceRejectedAttempt,
JudgePolicySnapshot,
LedgerAllocationFailedAttempt,
+ LocalEvaluationInfrastructure,
RunFailedAttempt,
decodeEvaluationReportId,
ensureSweepOperationallyComplete,
evaluationReportId,
makeAssessedAttempt,
makeJudgingUnavailableAttempt,
+ type EvaluationInfrastructure,
type EvaluationReportId,
type EvaluationSweepCell,
- type TerminalAttempt,
} from "./sweep.js";
+import {
+ submitEvaluationCell,
+ type EvaluationSubmissionResult,
+ type SimulatorProfile,
+} from "./submission.js";
const CLI_VERSION = "0.0.0";
const RUNTIME_STARTUP_TIMEOUT = Duration.minutes(5);
-const ROUTER_STARTUP_TIMEOUT = Duration.minutes(10);
const PEER_OBSERVATION_TIMEOUT = Duration.minutes(5);
const CASE_TIMEOUT = Duration.minutes(20);
-const LEDGER_DIRECTORY = [".moltzap", "evals", "ledgers"] as const;
const JUDGE_POLICY: JudgePolicyId = decodeJudgePolicyId(
"openai-gpt-5.6-sol/v1",
);
@@ -101,6 +123,43 @@ class SemanticJudgeCalibrationFailed extends Schema.TaggedError;
+}
+
+interface LocalExecutionEnvironment extends CommonExecutionEnvironment {
+ readonly profile: "local";
+ readonly localArtifacts: LocalArtifactRoot;
+}
+
+interface GkeExecutionEnvironment extends CommonExecutionEnvironment {
+ readonly profile: "gke";
+ readonly kubeContext: string;
+ readonly gkeArtifactBucket: ArtifactBucket;
+}
+
+// Each profile carries exactly the target it needs. One flat record with
+// optional fields would let a plan be built for a profile whose artifact target
+// was never resolved, and the only place to catch that is a runtime throw.
+type EvaluationExecutionEnvironment =
+ | LocalExecutionEnvironment
+ | GkeExecutionEnvironment;
+
+interface EvaluationExecutionImages {
+ readonly controllerImage: Image;
+ readonly peerApplicationImage: Image;
+ readonly nanoclawApplicationImage: Image;
}
interface AttemptContext {
@@ -184,6 +243,7 @@ const exactSourceRevision = Effect.fn("evals.exactSourceRevision")(
function evaluationConditions(
options: RuntimeOptions,
+ nanoclawApplicationImage: Image,
): readonly [EvaluationCondition, EvaluationCondition] {
const execution = {
peerObservationTimeout: PEER_OBSERVATION_TIMEOUT,
@@ -192,7 +252,6 @@ function evaluationConditions(
return [
openClawEvaluationCondition({
runtime: {
- installMode: "workspace",
startupTimeout: RUNTIME_STARTUP_TIMEOUT,
modelId: options.openclawModel,
},
@@ -200,7 +259,7 @@ function evaluationConditions(
}),
nanoclawEvaluationCondition({
runtime: {
- installMode: "workspace",
+ applicationImage: nanoclawApplicationImage,
autoRegisterConversations: true,
startupTimeout: RUNTIME_STARTUP_TIMEOUT,
modelId: options.nanoclawModel,
@@ -249,9 +308,33 @@ function conditionPlan(
});
}
+function planInfrastructure(
+ environment: EvaluationExecutionEnvironment,
+): EvaluationInfrastructure {
+ const shared = {
+ controllerImage: environment.controllerImage,
+ peerApplicationImage: environment.peerApplicationImage,
+ nanoclawApplicationImage: environment.nanoclawApplicationImage,
+ temporalAddress: environment.temporalAddress,
+ };
+ return environment.profile === "local"
+ ? LocalEvaluationInfrastructure.make({
+ ...shared,
+ profile: environment.profile,
+ artifactDirectory: environment.localArtifacts,
+ })
+ : GkeEvaluationInfrastructure.make({
+ ...shared,
+ profile: environment.profile,
+ kubeContext: environment.kubeContext,
+ artifactBucket: environment.gkeArtifactBucket,
+ });
+}
+
function reportPlan(
sourceRevision: string,
conditions: NonEmptyReadonlyArray,
+ environment: EvaluationExecutionEnvironment,
): EvaluationReportPlan {
const [firstCase, ...remainingCases] = evaluationCases;
const [firstCondition, ...remainingConditions] = conditions;
@@ -263,6 +346,7 @@ function reportPlan(
...remainingConditions.map(conditionPlan),
],
judgePolicy: judgePolicySnapshot(),
+ infrastructure: planInfrastructure(environment),
samplesPerCell: 1,
});
}
@@ -358,8 +442,13 @@ function persistGrade(
function assessExecution(
context: AttemptContext,
receipt: CompletedLedgerReceipt,
+ artifacts: CompletedLedgerArtifacts,
) {
- return behavioralEvaluation.openLedger(receipt.ledger).pipe(
+ return openEvaluationLedger(
+ context.definition,
+ receipt.ledger,
+ artifacts,
+ ).pipe(
Effect.flatMap((ledger) =>
transcriptFromLedger(ledger, context.definition),
),
@@ -380,6 +469,7 @@ function assessExecution(
function completeExecution(
context: AttemptContext,
outcome: EvaluationExecutionResult,
+ artifacts: CompletedLedgerArtifacts,
) {
return Effect.gen(function* () {
if (outcome instanceof EvaluationExecutionFailed) {
@@ -389,11 +479,151 @@ function completeExecution(
detail: outcome.detail,
});
}
- return yield* assessExecution(context, outcome.receipt);
+ return yield* assessExecution(context, outcome.receipt, artifacts);
});
}
+function ledgerAllocationFailed(context: AttemptContext) {
+ return DateTime.now.pipe(
+ Effect.map((completedAt) =>
+ LedgerAllocationFailedAttempt.make({
+ ...terminalFields(context, completedAt),
+ failure: LedgerStorageError.make({
+ operation: "allocate",
+ detail:
+ "the simulator controller could not allocate its durable ledger",
+ }),
+ }),
+ ),
+ );
+}
+
+function runInfrastructureFailed(
+ context: AttemptContext,
+ receipt: EvaluationSubmissionResult["result"]["summary"] & {
+ readonly _tag: "ClusterLost";
+ },
+) {
+ return DateTime.now.pipe(
+ Effect.map((completedAt) =>
+ RunFailedAttempt.make({
+ ...terminalFields(context, completedAt),
+ receipt: receipt.receipt,
+ detail: "the simulator controller reported an infrastructure failure",
+ }),
+ ),
+ );
+}
+
+function artifactStorage(
+ environment: EvaluationExecutionEnvironment,
+): EvaluationArtifactStorage {
+ return environment.profile === "local"
+ ? { profile: environment.profile, root: environment.localArtifacts }
+ : { profile: environment.profile, bucket: environment.gkeArtifactBucket };
+}
+
+function readCompletedArtifacts(
+ environment: EvaluationExecutionEnvironment,
+ context: AttemptContext,
+ namespace: string,
+ receipt: CompletedLedgerReceipt,
+) {
+ return Option.match(
+ evaluationArtifactLocation(
+ artifactStorage(environment),
+ namespace,
+ receipt.ledger,
+ ),
+ {
+ onNone: () =>
+ rejectEvidence(
+ context,
+ receipt,
+ "the controller ledger ref is not one artifact path segment",
+ ),
+ onSome: (location) =>
+ readEvaluationLedgerArtifacts(location).pipe(
+ Effect.matchEffect({
+ onFailure: (failure) =>
+ rejectEvidence(context, receipt, describeUnknown(failure)),
+ onSuccess: (artifacts) =>
+ projectEvaluationControllerResult(
+ context.definition,
+ receipt,
+ artifacts,
+ ).pipe(
+ Effect.matchEffect({
+ onFailure: (failure) =>
+ rejectEvidence(context, receipt, describeUnknown(failure)),
+ onSuccess: (outcome) =>
+ completeExecution(context, outcome, artifacts),
+ }),
+ ),
+ }),
+ ),
+ },
+ );
+}
+
+function completeSubmission(
+ environment: EvaluationExecutionEnvironment,
+ context: AttemptContext,
+ submission: EvaluationSubmissionResult,
+) {
+ const summary = submission.result.summary;
+ if (summary._tag === "LedgerAllocationFailed") {
+ return ledgerAllocationFailed(context);
+ }
+ if (summary._tag === "ClusterLost") {
+ return runInfrastructureFailed(context, summary);
+ }
+ return readCompletedArtifacts(
+ environment,
+ context,
+ submission.namespace,
+ summary.receipt,
+ );
+}
+
+function conditionModelId(
+ models: CommonExecutionEnvironment["models"],
+ condition: EvaluationConditionId,
+): string {
+ const byCondition: Readonly> = {
+ "openclaw/v2": models.openclaw,
+ "nanoclaw/v2": models.nanoclaw,
+ };
+ // Indexing needs the plain spelling; the brand is not part of the key set.
+ const name: EvaluationConditionName = condition;
+ return byCondition[name];
+}
+
+function submissionInput(
+ environment: EvaluationExecutionEnvironment,
+ context: AttemptContext,
+ condition: EvaluationCondition,
+) {
+ return {
+ workspaceRoot: environment.workspaceRoot,
+ profile: environment.profile,
+ caseId: context.definition.id,
+ definitionId: context.definition.definitionId,
+ attemptId: context.cell.attemptId,
+ condition: {
+ id: condition.id,
+ modelId: conditionModelId(environment.models, condition.id),
+ },
+ peerApplicationImage: environment.peerApplicationImage,
+ nanoclawApplicationImage: environment.nanoclawApplicationImage,
+ runtimeStartupTimeoutMillis: Duration.toMillis(RUNTIME_STARTUP_TIMEOUT),
+ peerObservationTimeoutMillis: Duration.toMillis(PEER_OBSERVATION_TIMEOUT),
+ caseTimeoutMillis: Duration.toMillis(CASE_TIMEOUT),
+ } as const;
+}
+
function executeCell(
+ environment: EvaluationExecutionEnvironment,
conditions: readonly EvaluationCondition[],
cell: EvaluationSweepCell,
) {
@@ -405,28 +635,10 @@ function executeCell(
definition,
startedAt: yield* DateTime.now,
};
- const execution = yield* definition
- .withDefinition({
- execute: (exact) =>
- condition.execute(exact, { attemptId: cell.attemptId }),
- })
- .pipe(Effect.either);
- return yield* Either.match(execution, {
- onLeft: (failure) =>
- DateTime.now.pipe(
- Effect.map(
- (completedAt): TerminalAttempt =>
- LedgerAllocationFailedAttempt.make({
- ...terminalFields(context, completedAt),
- failure,
- }),
- ),
- ),
- onRight: (outcome) =>
- completeExecution(context, outcome).pipe(
- Effect.map((attempt): TerminalAttempt => attempt),
- ),
- });
+ const submission = yield* submitEvaluationCell(
+ submissionInput(environment, context, condition),
+ );
+ return yield* completeSubmission(environment, context, submission);
}).pipe(Effect.withSpan("evals.executeCell"));
}
@@ -448,21 +660,13 @@ function reportIdNow() {
);
}
-function simulatorPlatform(ledgerDirectory: string) {
- return simulatorLayer({
- ledgerDirectory,
- router: { startupTimeout: ROUTER_STARTUP_TIMEOUT },
- });
-}
-
function executeReport(
- ledgerDirectory: string,
+ environment: EvaluationExecutionEnvironment,
conditions: readonly EvaluationCondition[],
) {
- return runEvaluationSweep((cell) => executeCell(conditions, cell)).pipe(
- Effect.provide(SemanticJudgeOpenAi),
- Effect.provide(simulatorPlatform(ledgerDirectory)),
- );
+ return runEvaluationSweep((cell) =>
+ executeCell(environment, conditions, cell),
+ ).pipe(Effect.provide(SemanticJudgeOpenAi));
}
function logReport(report: CompletedEvaluationReport, path: string) {
@@ -487,11 +691,146 @@ const nanoclawModelOption = Options.text("nanoclaw-model").pipe(
Options.withSchema(Schema.NonEmptyString),
Options.withDescription("Exact NanoClaw model ID."),
);
+const profileOption = Options.text("profile").pipe(
+ Options.withSchema(Schema.Literal("local", "gke")),
+ Options.withDefault("local"),
+ Options.withDescription("Repository-owned Kubernetes execution profile."),
+);
const runtimeOptions = {
openclawModel: openclawModelOption,
nanoclawModel: nanoclawModelOption,
+ profile: profileOption,
} as const;
+function requiredEnvironment(key: string) {
+ return Config.string(key).pipe(
+ Effect.mapError(() =>
+ EvaluationSourceStateError.make({
+ detail: `${key} is required for evaluation execution`,
+ }),
+ ),
+ );
+}
+
+function distributedApplicationImage(
+ key:
+ | "MOLTZAP_CONTROLLER_IMAGE"
+ | "MOLTZAP_SUPPORT_IMAGE"
+ | "MOLTZAP_NANOCLAW_IMAGE",
+ value: string,
+): Effect.Effect {
+ return Schema.decodeUnknown(image)(value).pipe(
+ Effect.mapError(() =>
+ EvaluationSourceStateError.make({
+ detail: `${key} must be a lowercase SHA-256 digest-pinned image`,
+ }),
+ ),
+ );
+}
+
+function executionImages() {
+ return Effect.all({
+ controllerImage: requiredEnvironment("MOLTZAP_CONTROLLER_IMAGE").pipe(
+ Effect.flatMap((value) =>
+ distributedApplicationImage("MOLTZAP_CONTROLLER_IMAGE", value),
+ ),
+ ),
+ peerApplicationImage: requiredEnvironment("MOLTZAP_SUPPORT_IMAGE").pipe(
+ Effect.flatMap((value) =>
+ distributedApplicationImage("MOLTZAP_SUPPORT_IMAGE", value),
+ ),
+ ),
+ nanoclawApplicationImage: requiredEnvironment(
+ "MOLTZAP_NANOCLAW_IMAGE",
+ ).pipe(
+ Effect.flatMap((value) =>
+ distributedApplicationImage("MOLTZAP_NANOCLAW_IMAGE", value),
+ ),
+ ),
+ });
+}
+
+function requiredArtifactTarget(
+ key: "MOLTZAP_LOCAL_ARTIFACTS" | "MOLTZAP_GKE_ARTIFACT_BUCKET",
+ requirement: string,
+ accept: (value: string) => Option.Option,
+) {
+ return requiredEnvironment(key).pipe(
+ Effect.flatMap((value) =>
+ Option.match(accept(value), {
+ onNone: () =>
+ Effect.fail(
+ EvaluationSourceStateError.make({
+ detail: `${key} must be ${requirement}`,
+ }),
+ ),
+ onSome: Effect.succeed,
+ }),
+ ),
+ );
+}
+
+function localArtifactDirectory(path: Path.Path) {
+ return requiredArtifactTarget(
+ "MOLTZAP_LOCAL_ARTIFACTS",
+ "an absolute path",
+ (value) => localArtifactRoot(path, value),
+ );
+}
+
+function gkeArtifactBucket() {
+ return requiredArtifactTarget(
+ "MOLTZAP_GKE_ARTIFACT_BUCKET",
+ "a valid Cloud Storage bucket name",
+ evaluationArtifactBucket,
+ );
+}
+
+function commonEnvironment(
+ root: string,
+ options: RuntimeOptions,
+ images: EvaluationExecutionImages,
+) {
+ return {
+ workspaceRoot: root,
+ ...images,
+ models: {
+ openclaw: options.openclawModel,
+ nanoclaw: options.nanoclawModel,
+ },
+ } as const;
+}
+
+function executionEnvironment(
+ root: string,
+ options: RuntimeOptions,
+): Effect.Effect<
+ EvaluationExecutionEnvironment,
+ EvaluationSourceStateError,
+ Path.Path
+> {
+ return Effect.gen(function* () {
+ const path = yield* Path.Path;
+ const common = {
+ ...commonEnvironment(root, options, yield* executionImages()),
+ temporalAddress: yield* requiredEnvironment("MOLTZAP_TEMPORAL_ADDRESS"),
+ };
+ if (options.profile === "local") {
+ return {
+ ...common,
+ profile: options.profile,
+ localArtifacts: yield* localArtifactDirectory(path),
+ };
+ }
+ return {
+ ...common,
+ profile: options.profile,
+ kubeContext: yield* requiredEnvironment("MOLTZAP_KUBE_CONTEXT"),
+ gkeArtifactBucket: yield* gkeArtifactBucket(),
+ };
+ });
+}
+
function runOrResume(
mode: "run" | "resume",
reportId: Option.Option,
@@ -500,21 +839,23 @@ function runOrResume(
return Effect.gen(function* () {
const root = yield* workspaceRoot();
const sourceRevision = yield* exactSourceRevision();
- const conditions = evaluationConditions(options);
- const plan = reportPlan(sourceRevision, conditions);
+ const environment = yield* executionEnvironment(root, options);
+ const conditions = evaluationConditions(
+ options,
+ environment.nanoclawApplicationImage,
+ );
+ const plan = reportPlan(sourceRevision, conditions, environment);
const resolvedId = Option.isSome(reportId)
? reportId.value
: yield* reportIdNow();
const databasePath = yield* reportLocation(root, resolvedId);
- const path = yield* Path.Path;
- const ledgerDirectory = path.join(root, ...LEDGER_DIRECTORY);
return yield* Effect.gen(function* () {
if (mode === "run") {
yield* createStoredEvaluationReport(resolvedId, plan);
} else {
yield* resumeStoredEvaluationReport(plan);
}
- const completed = yield* executeReport(ledgerDirectory, conditions);
+ const completed = yield* executeReport(environment, conditions);
yield* logReport(completed, databasePath);
return yield* ensureSweepOperationallyComplete(completed);
}).pipe(Effect.provide(evaluationResultStoreLayer(databasePath)));
diff --git a/packages/evals/src/events.test.ts b/packages/evals/src/events.test.ts
index a26541d80..7823cd2f2 100644
--- a/packages/evals/src/events.test.ts
+++ b/packages/evals/src/events.test.ts
@@ -3,12 +3,12 @@ import { agentName } from "@moltzap/protocol/identity";
import { agentId, conversationId, messageId } from "@moltzap/protocol/testing";
import { ProgramSucceeded, RouterMessageCommitted } from "@moltzap/simulator";
import {
- NanoclawGatewayInput,
- NanoclawGatewayOutput,
+ NanoClawGatewayInput,
+ NanoClawGatewayOutput,
OpenClawGatewayRequest,
OpenClawGatewaySucceeded,
OpenClawGatewayTimedOut,
-} from "@moltzap/simulator/runtime";
+} from "@moltzap/simulator/agents";
import { routerSequence } from "@moltzap/simulator/network";
import { Effect, Schema, Stream } from "effect";
import {
@@ -16,8 +16,8 @@ import {
CodePeerMessageSent,
EvaluationEvidenceProjectionError,
EvaluationEvidenceSelected,
- NanoclawPrincipalInputSent,
- NanoclawPrincipalOutputReceived,
+ NanoClawPrincipalInputSent,
+ NanoClawPrincipalOutputReceived,
OpenClawPrincipalFinalOutput,
OpenClawPrincipalInstructionAttempted,
PeerExchangeNotObserved,
@@ -82,18 +82,18 @@ const OPENCLAW_OUTPUT = OpenClawPrincipalFinalOutput.make({
}),
});
-const NANOCLAW_INPUT = NanoclawPrincipalInputSent.make({
+const NANOCLAW_INPUT = NanoClawPrincipalInputSent.make({
caseId: CASE_ID,
agentName: BOB_NAME,
agentId: BOB_ID,
- input: NanoclawGatewayInput.make({ text: NANOCLAW_INPUT_TEXT }),
+ input: NanoClawGatewayInput.make({ text: NANOCLAW_INPUT_TEXT }),
});
-const NANOCLAW_OUTPUT = NanoclawPrincipalOutputReceived.make({
+const NANOCLAW_OUTPUT = NanoClawPrincipalOutputReceived.make({
caseId: CASE_ID,
agentName: BOB_NAME,
agentId: BOB_ID,
- output: NanoclawGatewayOutput.make({ text: NANOCLAW_OUTPUT_TEXT }),
+ output: NanoClawGatewayOutput.make({ text: NANOCLAW_OUTPUT_TEXT }),
});
const CODE_SENT = CodePeerMessageSent.make({
@@ -169,8 +169,8 @@ it("declares the complete customer event universe", () => {
const eventClasses = [
OpenClawPrincipalInstructionAttempted,
OpenClawPrincipalFinalOutput,
- NanoclawPrincipalInputSent,
- NanoclawPrincipalOutputReceived,
+ NanoClawPrincipalInputSent,
+ NanoClawPrincipalOutputReceived,
CodePeerMessageSent,
CodePeerMessageReceived,
PeerExchangeNotObserved,
diff --git a/packages/evals/src/events.ts b/packages/evals/src/events.ts
index 06d88cbb2..ba7ed4859 100644
--- a/packages/evals/src/events.ts
+++ b/packages/evals/src/events.ts
@@ -5,12 +5,12 @@ import { type AgentId, agentId, agentName } from "@moltzap/protocol/identity";
import { messagePartsSchema } from "@moltzap/protocol/message";
import { EventCatalog, RouterMessageCommitted } from "@moltzap/simulator";
import {
- NanoclawGatewayInput,
- NanoclawGatewayOutput,
+ NanoClawGatewayInput,
+ NanoClawGatewayOutput,
OpenClawGatewayRequest,
OpenClawGatewaySucceeded,
OpenClawGatewayTimedOut,
-} from "@moltzap/simulator/runtime";
+} from "@moltzap/simulator/agents";
import { Chunk, Effect, Schema, Stream } from "effect";
import {
evaluationCaseId,
@@ -51,24 +51,24 @@ export class OpenClawPrincipalFinalOutput extends Schema.TaggedClass()(
+export class NanoClawPrincipalInputSent extends Schema.TaggedClass()(
"moltzap.nanoclaw-principal-input-sent/v1",
{
caseId: evaluationCaseId,
agentName: agentName,
agentId: agentId,
- input: NanoclawGatewayInput,
+ input: NanoClawGatewayInput,
},
) {}
/** The evaluation adapter received one output frame from NanoClaw. */
-export class NanoclawPrincipalOutputReceived extends Schema.TaggedClass()(
+export class NanoClawPrincipalOutputReceived extends Schema.TaggedClass()(
"moltzap.nanoclaw-principal-output-received/v1",
{
caseId: evaluationCaseId,
agentName: agentName,
agentId: agentId,
- output: NanoclawGatewayOutput,
+ output: NanoClawGatewayOutput,
},
) {}
@@ -129,8 +129,8 @@ export class EvaluationEvidenceSelected extends Schema.TaggedClass
+ left.localeCompare(right),
+ ),
+ createdAt: DateTime.unsafeMake(0),
+ provenance: {},
+ metadata: {},
+ });
+ const manifestText = json(Schema.encodeSync(LedgerManifest)(manifest));
+ const record = {
+ runId: manifest.runId,
+ eventId: "eval-controller-projection:0",
+ logicalSequence: 0,
+ elapsedNanos: 0n,
+ observedAt: 0,
+ producer: "eval-controller-projection",
+ event,
+ };
+ const recordsText = `${json(
+ Schema.encodeSync(makeLedgerRecordSchema(CATALOG))(record),
+ )}\n`;
+ const completion = LedgerCompletion.make({
+ ledgerFormatVersion: 1,
+ runId: manifest.runId,
+ recordCount: 1,
+ artifacts: {
+ manifest: digest(manifestText),
+ records: digest(recordsText),
+ },
+ });
+ return {
+ artifacts: {
+ manifest: manifestText,
+ records: recordsText,
+ completion: json(Schema.encodeSync(LedgerCompletion)(completion)),
+ },
+ receipt: CompletedLedgerReceipt.make({ ledger: REF, completion }),
+ };
+}
+
+test("projects a successful customer program from canonical ledger evidence", () => {
+ const fixture = completedArtifacts(ProgramSucceeded.make({}));
+ return projectEvaluationControllerResult(
+ DEFINITION,
+ fixture.receipt,
+ fixture.artifacts,
+ ).pipe(
+ Effect.tap((result) => {
+ assert.strictEqual(result._tag, "EvaluationExecutionCompleted");
+ assert.strictEqual(result.receipt, fixture.receipt);
+ }),
+ );
+});
+
+test("projects a typed customer failure without trusting controller process state", () => {
+ const fixture = completedArtifacts(
+ ProgramFailed.make({ cause: "the evaluation program rejected its input" }),
+ );
+ return projectEvaluationControllerResult(
+ DEFINITION,
+ fixture.receipt,
+ fixture.artifacts,
+ ).pipe(
+ Effect.tap((result) => {
+ assert.instanceOf(result, EvaluationExecutionFailed);
+ if (result instanceof EvaluationExecutionFailed) {
+ assert.strictEqual(
+ result.detail,
+ "the evaluation program rejected its input",
+ );
+ }
+ }),
+ );
+});
+
+test("rejects a controller completion that disagrees with the ledger", () => {
+ const fixture = completedArtifacts(ProgramSucceeded.make({}));
+ const mismatched = CompletedLedgerReceipt.make({
+ ledger: REF,
+ completion: LedgerCompletion.make({
+ ledgerFormatVersion: fixture.receipt.completion.ledgerFormatVersion,
+ runId: fixture.receipt.completion.runId,
+ recordCount: fixture.receipt.completion.recordCount,
+ artifacts: {
+ ...fixture.receipt.completion.artifacts,
+ records: decodeDigest("0".repeat(64)),
+ },
+ }),
+ });
+ return projectEvaluationControllerResult(
+ DEFINITION,
+ mismatched,
+ fixture.artifacts,
+ ).pipe(
+ Effect.flip,
+ Effect.tap((failure) => {
+ assert.instanceOf(failure, EvaluationControllerResultInvalid);
+ if (failure instanceof EvaluationControllerResultInvalid) {
+ assert.include(failure.detail, "does not match the ledger");
+ }
+ }),
+ );
+});
+
+/* eslint-enable agent-code-guard/no-hardcoded-assertion-literals -- Controller projection assertions end here. */
diff --git a/packages/evals/src/execution.test.ts b/packages/evals/src/execution.test.ts
index 390696327..62f9bddd6 100644
--- a/packages/evals/src/execution.test.ts
+++ b/packages/evals/src/execution.test.ts
@@ -4,15 +4,15 @@ import { agentName } from "@moltzap/protocol/identity";
import { agentId, conversationId, messageId } from "@moltzap/protocol/testing";
import type { EventOf } from "@moltzap/simulator";
import {
- NanoclawGatewayOutput,
- type NanoclawGateway,
- type NanoclawGatewayError,
- type NanoclawGatewayInput,
+ NanoClawGatewayOutput,
+ type NanoClawGateway,
+ type NanoClawGatewayError,
+ type NanoClawGatewayInput,
OpenClawGatewayResponse,
type OpenClawGateway,
- type OpenClawGatewayRequestFailed,
+ type OpenClawGatewayRequestError,
type StartedAgent,
-} from "@moltzap/simulator/runtime";
+} from "@moltzap/simulator/agents";
import { makeAgentHandle } from "@moltzap/simulator/network";
import {
Deferred,
@@ -30,13 +30,13 @@ import {
evaluationCases,
type EvaluationCaseDefinition,
type EvaluationCasePeers,
- type EvaluationCasePeerRuntimes,
+ type EvaluationCasePeerDefinitions,
} from "./cases.js";
import {
CodePeerMessageReceived,
EvaluationEvidenceSelected,
- NanoclawPrincipalInputSent,
- NanoclawPrincipalOutputReceived,
+ NanoClawPrincipalInputSent,
+ NanoClawPrincipalOutputReceived,
OpenClawPrincipalFinalOutput,
OpenClawPrincipalInstructionAttempted,
PeerExchangeNotObserved,
@@ -70,7 +70,7 @@ const GATEWAY_RESPONSE = Schema.decodeSync(OpenClawGatewayResponse)({
summary: "completed",
result: { payloads: [{ text: "I contacted the requested peer." }] },
});
-const NANOCLAW_OUTPUT = NanoclawGatewayOutput.make({
+const NANOCLAW_OUTPUT = NanoClawGatewayOutput.make({
text: "Uncorrelated native output.",
});
const EXPECTED_OPENCLAW_TOOLS = {
@@ -81,24 +81,14 @@ const EXPECTED_OPENCLAW_TOOLS = {
},
},
elevated: { enabled: false },
- exec: { mode: "deny" },
-};
-const EXPECTED_OPENCLAW_SANDBOX = {
- mode: "all",
- backend: "docker",
- scope: "session",
- workspaceAccess: "none",
- docker: { network: "none" },
+ exec: { mode: "full" },
};
const bundledOpenClawPolicyConfiguration = Schema.Struct({
tools: Schema.Struct({
definitionDigest: Schema.String,
redacted: Schema.Tuple(Schema.Literal("configuration")),
}),
- sandbox: Schema.Struct({
- definitionDigest: Schema.String,
- redacted: Schema.Tuple(Schema.Literal("configuration")),
- }),
+ sandbox: Schema.optional(Schema.Unknown),
});
type EvaluationEvent = EventOf;
@@ -180,14 +170,14 @@ function selectedSocialGateway(
};
}
-function instrumentation(
+function instrumentation(
definition: EvaluationCaseDefinition,
peers: EvaluationCasePeers,
emit: EmitEvaluationEvent,
): Effect.Effect<
EvaluationCaseInstrumentation<
OpenClawGateway,
- OpenClawGatewayRequestFailed,
+ OpenClawGatewayRequestError,
PeerRuntimes
>
> {
@@ -221,8 +211,8 @@ function principalPeers(): EvaluationCasePeers {
}
function nanoclawGateway(
- submitted: Ref.Ref,
-): NanoclawGateway {
+ submitted: Ref.Ref,
+): NanoClawGateway {
return {
submit: (input) => Ref.update(submitted, (current) => [...current, input]),
outputs: Stream.never,
@@ -230,16 +220,16 @@ function nanoclawGateway(
}
function nanoclawInstrumentation<
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerRuntimes extends EvaluationCasePeerDefinitions,
>(
definition: EvaluationCaseDefinition,
peers: EvaluationCasePeers,
- gateway: NanoclawGateway,
+ gateway: NanoClawGateway,
emit: EmitEvaluationEvent,
): Effect.Effect<
EvaluationCaseInstrumentation<
- NanoclawGateway,
- NanoclawGatewayError,
+ NanoClawGateway,
+ NanoClawGatewayError,
PeerRuntimes
>
> {
@@ -361,7 +351,7 @@ function nanoclawPrincipalOutputUnsupportedTest() {
return Effect.gen(function* () {
const definition = evaluationCases[8];
const recorder = yield* eventRecorder();
- const submitted = yield* Ref.make([]);
+ const submitted = yield* Ref.make([]);
const acquired = yield* nanoclawInstrumentation(
definition,
principalPeers(),
@@ -374,7 +364,7 @@ function nanoclawPrincipalOutputUnsupportedTest() {
assert.lengthOf(yield* Ref.get(submitted), 1);
const records = yield* Ref.get(recorder.records);
assert.lengthOf(records, 1);
- assert.instanceOf(records[0]?.event, NanoclawPrincipalInputSent);
+ assert.instanceOf(records[0]?.event, NanoClawPrincipalInputSent);
assert.isFalse(
records.some(({ event }) => event instanceof EvaluationEvidenceSelected),
);
@@ -390,7 +380,7 @@ function outputRecordingEmit(
.emit(event)
.pipe(
Effect.tap(() =>
- event instanceof NanoclawPrincipalOutputReceived
+ event instanceof NanoClawPrincipalOutputReceived
? Deferred.succeed(outputRecorded, undefined)
: Effect.void,
),
@@ -398,9 +388,9 @@ function outputRecordingEmit(
}
function outputBeforeSubmitGateway(
- submitted: Ref.Ref,
+ submitted: Ref.Ref,
outputRecorded: Deferred.Deferred,
-): NanoclawGateway {
+): NanoClawGateway {
return {
submit: (input) =>
Ref.update(submitted, (current) => [...current, input]).pipe(
@@ -410,12 +400,12 @@ function outputBeforeSubmitGateway(
};
}
-function assertUncorrelatedNanoclawEvidence(
+function assertUncorrelatedNanoClawEvidence(
records: readonly RecordedEvent[],
): void {
assert.lengthOf(
records.filter(
- ({ event }) => event instanceof NanoclawPrincipalOutputReceived,
+ ({ event }) => event instanceof NanoClawPrincipalOutputReceived,
),
1,
);
@@ -424,7 +414,7 @@ function assertUncorrelatedNanoclawEvidence(
1,
);
assert.lengthOf(
- records.filter(({ event }) => event instanceof NanoclawPrincipalInputSent),
+ records.filter(({ event }) => event instanceof NanoClawPrincipalInputSent),
1,
);
assert.isFalse(
@@ -436,7 +426,7 @@ function nanoclawIdentityOutputUnsupportedTest() {
return Effect.gen(function* () {
const definition = evaluationCases[10];
const recorder = yield* eventRecorder();
- const submitted = yield* Ref.make([]);
+ const submitted = yield* Ref.make([]);
const outputRecorded = yield* Deferred.make();
const acquired = yield* nanoclawInstrumentation(
definition,
@@ -454,7 +444,7 @@ function nanoclawIdentityOutputUnsupportedTest() {
const failure = yield* runEvaluationCase(acquired).pipe(Effect.flip);
assertUnsupportedPrincipalOutput(failure);
assert.lengthOf(yield* Ref.get(submitted), 1);
- assertUncorrelatedNanoclawEvidence(yield* Ref.get(recorder.records));
+ assertUncorrelatedNanoClawEvidence(yield* Ref.get(recorder.records));
});
}
@@ -468,9 +458,7 @@ function policyDigest(policy: object): string {
function bundledOpenClawPolicyTest(): void {
const condition = openClawEvaluationCondition({
- runtime: {
- installMode: "workspace",
- },
+ runtime: {},
execution: {
peerObservationTimeout: Duration.seconds(1),
caseTimeout: Duration.seconds(2),
@@ -483,10 +471,7 @@ function bundledOpenClawPolicyTest(): void {
definitionDigest: policyDigest(EXPECTED_OPENCLAW_TOOLS),
redacted: ["configuration"],
});
- assert.deepStrictEqual(configuration.sandbox, {
- definitionDigest: policyDigest(EXPECTED_OPENCLAW_SANDBOX),
- redacted: ["configuration"],
- });
+ assert.isUndefined(configuration.sandbox);
}
// @agent-code-guard/regression-only: native gateway output and autonomous social evidence have distinct selection paths
diff --git a/packages/evals/src/execution.ts b/packages/evals/src/execution.ts
index f4b85855e..a8712ba99 100644
--- a/packages/evals/src/execution.ts
+++ b/packages/evals/src/execution.ts
@@ -1,37 +1,45 @@
/** @file Concrete mixed-agent conditions and code-defined case execution. */
-import type { FileSystem, HttpClient, Path } from "@effect/platform";
-import type { CommandExecutor } from "@effect/platform/CommandExecutor";
import { agentName } from "@moltzap/protocol/identity";
import {
CompletedLedgerReceipt,
+ EventCatalog,
LedgerReceipt,
- ProgramFinished,
- simulator,
+ ProgramFailed,
+ ProgramInterrupted,
+ ProgramSucceeded,
+ RunSpec,
+ coreEvents,
+ type ClusterServices,
} from "@moltzap/simulator";
import {
type AgentRuntime,
+ type Image,
type StartedAgent,
nanoclawRuntime,
openClawRuntime,
runtimeConfigurationProjection,
- type NanoclawRuntimeOptions,
+ type NanoClawRuntimeOptions,
type OpenClawRuntimeOptions,
-} from "@moltzap/simulator/runtime";
-import type {
- LedgerStorage,
- JsonValue,
- LedgerStorageError,
+} from "@moltzap/simulator/agents";
+import {
+ openLedgerArtifacts,
+ type CompletedLedgerArtifacts,
+ type CompletedRunLedger,
+ type JsonValue,
+ type LedgerOpenError,
+ type LedgerStorageError,
+ type LedgerRef,
} from "@moltzap/simulator/ledger";
-import type { RouterProvider } from "@moltzap/simulator/network";
import {
Array as Arr,
- Cause,
Duration,
Effect,
- Exit,
+ type Layer,
Option,
+ Record as Rec,
Schema,
+ Stream,
} from "effect";
import {
TARGET_AGENT_NAME,
@@ -39,7 +47,7 @@ import {
type EvaluationCaseMetadata,
type EvaluationCasePeer,
type EvaluationCasePeers,
- type EvaluationCasePeerRuntimes,
+ type EvaluationCasePeerDefinitions,
type EvaluationCaseProgramContext,
} from "./cases.js";
import {
@@ -48,14 +56,15 @@ import {
evaluationEvents,
} from "./events.js";
import {
- decodeConditionId,
+ decodeEvaluationConditionId,
decodeEvaluationEvidenceId,
- type ConditionId,
type EvaluationCaseId,
+ type EvaluationConditionId,
type EvaluationEvidenceId,
} from "./model.js";
import type {
PeerExchange,
+ EvaluationPeerDefinition,
EvaluationPeerGateway,
EvaluationPeerObservation,
} from "./peer.js";
@@ -67,21 +76,35 @@ import {
type PrincipalDriverFactory,
} from "./principal.js";
-type EvaluationExecutionRequirements =
- | CommandExecutor
- | FileSystem.FileSystem
- | HttpClient.HttpClient
- | Path.Path
- | LedgerStorage
- | RouterProvider;
-
const decodeAgentName = Schema.decodeSync(agentName);
-/** Stable definition for every bundled behavioral case run. */
-export const behavioralEvaluation = simulator.define(
- "moltzap.behavioral-evaluation/v1",
- evaluationEvents,
-);
+/** Controller-owned services required by every evaluation cell RunSpec. */
+type EvaluationInfrastructure = Layer.Layer<
+ ClusterServices,
+ LedgerStorageError
+>;
+
+const evaluationCatalog = EventCatalog.merge(coreEvents, evaluationEvents);
+
+/**
+ * Reopen one case-specific RunSpec ledger against the exact evaluation catalog.
+ * @param definition Bundled case whose definition id owns the ledger.
+ * @param ref Physical ledger identity returned by the controller.
+ * @param artifacts Immutable manifest, records, and completion artifact text.
+ * @returns The fully validated completed evaluation ledger.
+ */
+export function openEvaluationLedger(
+ definition: EvaluationCaseMetadata,
+ ref: LedgerRef,
+ artifacts: CompletedLedgerArtifacts,
+) {
+ return openLedgerArtifacts(
+ evaluationCatalog,
+ ref,
+ artifacts,
+ definition.definitionId,
+ );
+}
/** Customer-owned deadlines for observable behavior and complete case work. */
interface EvaluationExecutionPolicy {
@@ -131,21 +154,38 @@ export type EvaluationExecutionResult =
| EvaluationExecutionCompleted
| EvaluationExecutionFailed;
-/** Concrete condition with no runtime gateway union at its public boundary. */
-export interface EvaluationCondition<
- RuntimeRequirements = EvaluationExecutionRequirements,
-> {
- readonly id: ConditionId;
+/** A controller receipt disagrees with its completed evaluation ledger. */
+export class EvaluationControllerResultInvalid extends Schema.TaggedError()(
+ "EvaluationControllerResultInvalid",
+ {
+ detail: Schema.NonEmptyString,
+ },
+) {}
+
+interface EvaluationConditionDefinitionConsumer {
+ readonly execute: <
+ Gateway,
+ DriverFailure,
+ RuntimeFailure,
+ ConfigurationSchema extends Schema.Schema.AnyNoContext,
+ >(
+ definition: EvaluationConditionDefinition<
+ Gateway,
+ DriverFailure,
+ RuntimeFailure,
+ ConfigurationSchema
+ >,
+ ) => Result;
+}
+
+/** Concrete condition with its exact gateway retained behind a rank-2 binder. */
+export interface EvaluationCondition {
+ readonly id: EvaluationConditionId;
readonly runtimeName: string;
readonly runtimeConfiguration: JsonValue;
- readonly execute: (
- definition: EvaluationCaseDefinition,
- input: EvaluationExecutionInput,
- ) => Effect.Effect<
- EvaluationExecutionResult,
- LedgerStorageError,
- EvaluationExecutionRequirements | RuntimeRequirements
- >;
+ readonly withDefinition: (
+ consumer: EvaluationConditionDefinitionConsumer,
+ ) => Result;
}
/** Exact runtime and adapter captured behind one code-defined condition. */
@@ -153,16 +193,10 @@ export interface EvaluationConditionDefinition<
Gateway,
DriverFailure,
RuntimeFailure,
- RuntimeRequirements,
ConfigurationSchema extends Schema.Schema.AnyNoContext,
> {
- readonly id: ConditionId;
- readonly runtime: AgentRuntime<
- Gateway,
- RuntimeFailure,
- RuntimeRequirements,
- ConfigurationSchema
- >;
+ readonly id: EvaluationConditionId;
+ readonly runtime: AgentRuntime;
readonly principal: PrincipalDriverFactory;
readonly execution: EvaluationExecutionPolicy;
}
@@ -172,8 +206,8 @@ interface OpenClawEvaluationConditionOptions {
readonly execution: EvaluationExecutionPolicy;
}
-interface NanoclawEvaluationConditionOptions {
- readonly runtime: NanoclawRuntimeOptions;
+interface NanoClawEvaluationConditionOptions {
+ readonly runtime: NanoClawRuntimeOptions;
readonly execution: EvaluationExecutionPolicy;
}
@@ -186,17 +220,9 @@ const BUNDLED_OPENCLAW_TOOLS = {
},
},
elevated: { enabled: false },
- exec: { mode: "deny" },
+ exec: { mode: "full" },
} satisfies NonNullable;
-const BUNDLED_OPENCLAW_SANDBOX = {
- mode: "all",
- backend: "docker",
- scope: "session",
- workspaceAccess: "none",
- docker: { network: "none" },
-} satisfies NonNullable;
-
Object.freeze(BUNDLED_OPENCLAW_TOOLS.allow);
Object.freeze(BUNDLED_OPENCLAW_TOOLS.sandbox.tools.allow);
Object.freeze(BUNDLED_OPENCLAW_TOOLS.sandbox.tools);
@@ -204,14 +230,12 @@ Object.freeze(BUNDLED_OPENCLAW_TOOLS.sandbox);
Object.freeze(BUNDLED_OPENCLAW_TOOLS.elevated);
Object.freeze(BUNDLED_OPENCLAW_TOOLS.exec);
Object.freeze(BUNDLED_OPENCLAW_TOOLS);
-Object.freeze(BUNDLED_OPENCLAW_SANDBOX.docker);
-Object.freeze(BUNDLED_OPENCLAW_SANDBOX);
/** Exact native gateway and observation capabilities for one acquired case. */
export interface EvaluationCaseInstrumentation<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerRuntimes extends EvaluationCasePeerDefinitions,
> {
readonly definition: EvaluationCaseDefinition;
readonly policy: EvaluationExecutionPolicy;
@@ -386,7 +410,7 @@ function runtimeStopped(
function principalInstruction<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerRuntimes extends EvaluationCasePeerDefinitions,
>(
instrumentation: EvaluationCaseInstrumentation<
Gateway,
@@ -410,7 +434,7 @@ function principalInstruction<
function observePrincipal<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerRuntimes extends EvaluationCasePeerDefinitions,
>(
instrumentation: EvaluationCaseInstrumentation<
Gateway,
@@ -445,7 +469,7 @@ function selectPrincipalOutput(
function caseContext<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerRuntimes extends EvaluationCasePeerDefinitions,
>(
instrumentation: EvaluationCaseInstrumentation<
Gateway,
@@ -470,7 +494,7 @@ function caseContext<
function runCaseProgram<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerRuntimes extends EvaluationCasePeerDefinitions,
>(
instrumentation: EvaluationCaseInstrumentation<
Gateway,
@@ -512,7 +536,7 @@ function runCaseProgram<
export function runEvaluationCase<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerRuntimes extends EvaluationCasePeerDefinitions,
>(
instrumentation: EvaluationCaseInstrumentation<
Gateway,
@@ -526,134 +550,229 @@ export function runEvaluationCase<
interface ExecuteConditionInput<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
RuntimeFailure,
- RuntimeRequirements,
ConfigurationSchema extends Schema.Schema.AnyNoContext,
> {
- readonly runtime: AgentRuntime<
- Gateway,
- RuntimeFailure,
- RuntimeRequirements,
- ConfigurationSchema
- >;
+ readonly runtime: AgentRuntime;
readonly principal: PrincipalDriverFactory;
readonly policy: EvaluationExecutionPolicy;
- readonly conditionId: ConditionId;
- readonly definition: EvaluationCaseDefinition;
+ readonly definition: EvaluationCaseDefinition;
readonly execution: EvaluationExecutionInput;
+ readonly peerApplicationImage: Image;
+ readonly infrastructure: EvaluationInfrastructure;
}
-function makeConditionRoster<
+type MaterializedPeerRuntimes<
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
+> = Readonly<{
+ [Name in keyof PeerDefinitions]: ReturnType;
+}>;
+
+function materializePeerRuntimes<
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
+>(
+ definitions: PeerDefinitions,
+ peerApplicationImage: Image,
+): MaterializedPeerRuntimes {
+ // eslint-disable-next-line agent-code-guard/require-assertion-rationale -- Record.map preserves the exact keys of the immutable input record while replacing every value with its materialized runtime.
+ return Rec.map(definitions, (definition: EvaluationPeerDefinition) =>
+ definition.runtime(peerApplicationImage),
+ ) as MaterializedPeerRuntimes;
+}
+
+function makeConditionRuntimes<
Gateway,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
RuntimeFailure,
- RuntimeRequirements,
ConfigurationSchema extends Schema.Schema.AnyNoContext,
>(
- runtime: AgentRuntime<
- Gateway,
- RuntimeFailure,
- RuntimeRequirements,
- ConfigurationSchema
- >,
- definition: EvaluationCaseDefinition,
+ runtime: AgentRuntime,
+ definition: EvaluationCaseDefinition,
+ peerApplicationImage: Image,
) {
- return behavioralEvaluation.agents({
- ...definition.peers,
+ return Object.freeze({
+ ...materializePeerRuntimes(definition.peers, peerApplicationImage),
[TARGET_AGENT_NAME]: runtime,
});
}
-function summarizeProgramFinished(
- outcome: ProgramFinished,
-): EvaluationExecutionResult {
- return Exit.isSuccess(outcome.exit)
- ? EvaluationExecutionCompleted.make({
- receipt: outcome.receipt,
- })
- : EvaluationExecutionFailed.make({
- receipt: outcome.receipt,
- detail: Cause.pretty(outcome.exit.cause),
- });
-}
-
-interface InfrastructureFailureOutcome {
- readonly receipt: LedgerReceipt;
- readonly cause: Cause.Cause;
+type EvaluationCompletedLedger = CompletedRunLedger;
+
+type ProgramCompletionEvent =
+ | ProgramSucceeded
+ | ProgramFailed
+ | ProgramInterrupted;
+
+function completionEvents(
+ ledger: EvaluationCompletedLedger,
+): Effect.Effect {
+ const initial: readonly ProgramCompletionEvent[] = [];
+ return ledger.records.pipe(
+ Stream.runFold(initial, (events, record) => {
+ const event = record.event;
+ return event instanceof ProgramSucceeded ||
+ event instanceof ProgramFailed ||
+ event instanceof ProgramInterrupted
+ ? [...events, event]
+ : events;
+ }),
+ );
}
-function summarizeInfrastructureFailure(
- outcome: InfrastructureFailureOutcome,
+function completionMatchesReceipt(
+ ledger: EvaluationCompletedLedger,
+ receipt: CompletedLedgerReceipt,
+): boolean {
+ const observed = ledger.completion;
+ const claimed = receipt.completion;
+ const sameHeader =
+ observed.ledgerFormatVersion === claimed.ledgerFormatVersion &&
+ observed.runId === claimed.runId &&
+ observed.recordCount === claimed.recordCount;
+ const sameManifest =
+ observed.artifacts.manifest === claimed.artifacts.manifest;
+ const sameRecords = observed.artifacts.records === claimed.artifacts.records;
+ return sameHeader && sameManifest && sameRecords;
+}
+
+function projectProgramCompletion(
+ event: ProgramCompletionEvent,
+ receipt: CompletedLedgerReceipt,
): EvaluationExecutionResult {
- return EvaluationExecutionFailed.make({
- receipt: outcome.receipt,
- detail: Cause.pretty(outcome.cause),
- });
+ return event instanceof ProgramSucceeded
+ ? EvaluationExecutionCompleted.make({ receipt })
+ : EvaluationExecutionFailed.make({ receipt, detail: event.cause });
}
-function summarizeOutcome(
- outcome:
- | ProgramFinished
- | InfrastructureFailureOutcome,
-): EvaluationExecutionResult {
- return outcome instanceof ProgramFinished
- ? summarizeProgramFinished(outcome)
- : summarizeInfrastructureFailure(outcome);
+function projectCompletedLedger(
+ ledger: EvaluationCompletedLedger,
+ receipt: CompletedLedgerReceipt,
+): Effect.Effect {
+ return Effect.gen(function* () {
+ if (!completionMatchesReceipt(ledger, receipt)) {
+ return yield* EvaluationControllerResultInvalid.make({
+ detail: "controller receipt completion does not match the ledger",
+ });
+ }
+ const events = yield* completionEvents(ledger);
+ if (events.length !== 1) {
+ return yield* EvaluationControllerResultInvalid.make({
+ detail: `completed evaluation ledger contains ${String(events.length)} program completion events`,
+ });
+ }
+ const [event] = events;
+ if (event === undefined) {
+ return yield* EvaluationControllerResultInvalid.make({
+ detail: "completed evaluation ledger has no program completion event",
+ });
+ }
+ return projectProgramCompletion(event, receipt);
+ });
}
-function runProvenance(
- conditionId: ConditionId,
+/**
+ * Reopen a controller-completed ledger and recover its customer-program result.
+ * @param definition Bundled case that owns the ledger definition and catalog.
+ * @param receipt Bounded controller result projected outside the run process.
+ * @param artifacts Immutable artifacts retrieved for the receipt's ledger.
+ * @returns The evaluation result recovered from canonical simulator evidence.
+ */
+export function projectEvaluationControllerResult(
definition: EvaluationCaseMetadata,
- execution: EvaluationExecutionInput,
-) {
- return {
- provenance: {
- caseId: definition.id,
- caseDefinitionId: definition.definitionId,
- conditionId,
- attemptId: execution.attemptId,
- },
- };
+ receipt: CompletedLedgerReceipt,
+ artifacts: CompletedLedgerArtifacts,
+): Effect.Effect<
+ EvaluationExecutionResult,
+ LedgerOpenError | EvaluationControllerResultInvalid
+> {
+ return openEvaluationLedger(definition, receipt.ledger, artifacts).pipe(
+ Effect.flatMap((ledger) => projectCompletedLedger(ledger, receipt)),
+ );
}
-function executeCondition<
+/**
+ * Construct one case-and-condition RunSpec using an injected infrastructure Layer.
+ * @param input Exact target runtime, peer roster, policy, and infrastructure.
+ * @returns The immutable RunSpec for one evaluation matrix cell.
+ */
+function evaluationRunSpec<
Gateway,
DriverFailure,
- PeerRuntimes extends EvaluationCasePeerRuntimes,
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
RuntimeFailure,
- RuntimeRequirements,
ConfigurationSchema extends Schema.Schema.AnyNoContext,
>(
input: ExecuteConditionInput<
Gateway,
DriverFailure,
- PeerRuntimes,
+ PeerDefinitions,
RuntimeFailure,
- RuntimeRequirements,
ConfigurationSchema
>,
) {
- const { conditionId, definition, principal, execution, policy, runtime } =
- input;
- const roster = makeConditionRoster(runtime, definition);
- const program = Effect.gen(function* () {
- const agents = yield* roster.startedAgents;
- const events = yield* behavioralEvaluation.events;
- const { [TARGET_AGENT_NAME]: target, ...peers } = agents;
- const driver = yield* principal.make(execution.attemptId);
- yield* runEvaluationCase({
- definition,
- policy,
- target,
- peers,
- driver,
- emit: events.emit,
- });
+ const {
+ peerApplicationImage,
+ definition,
+ infrastructure,
+ principal,
+ execution,
+ policy,
+ runtime,
+ } = input;
+ return RunSpec.define({
+ id: definition.definitionId,
+ events: [evaluationEvents],
+ agents: makeConditionRuntimes(runtime, definition, peerApplicationImage),
+ cluster: infrastructure,
+ execute: ({ agents, events }) => {
+ const { [TARGET_AGENT_NAME]: target, ...peers } = agents;
+ return Effect.gen(function* () {
+ const driver = yield* principal.make(execution.attemptId);
+ yield* runEvaluationCase({
+ definition,
+ policy,
+ target,
+ peers,
+ driver,
+ emit: events.emit,
+ });
+ });
+ },
+ });
+}
+
+/** Inputs that bind one report cell to a controller-owned infrastructure Layer. */
+interface EvaluationCellRunSpecInput<
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
+> {
+ readonly definition: EvaluationCaseDefinition;
+ readonly condition: EvaluationCondition;
+ readonly attemptId: string;
+ readonly peerApplicationImage: Image;
+ readonly infrastructure: EvaluationInfrastructure;
+}
+
+/**
+ * Construct exactly one case-by-condition controller RunSpec.
+ * @param input Exact case, condition, peer image, attempt, and infrastructure.
+ * @returns One immutable controller-owned RunSpec.
+ */
+export function evaluationCellRunSpec<
+ PeerDefinitions extends EvaluationCasePeerDefinitions,
+>(input: EvaluationCellRunSpecInput) {
+ return input.condition.withDefinition({
+ execute: (condition) =>
+ evaluationRunSpec({
+ runtime: condition.runtime,
+ principal: condition.principal,
+ policy: condition.execution,
+ definition: input.definition,
+ execution: { attemptId: input.attemptId },
+ peerApplicationImage: input.peerApplicationImage,
+ infrastructure: input.infrastructure,
+ }),
});
- return behavioralEvaluation
- .run(roster, program, runProvenance(conditionId, definition, execution))
- .pipe(Effect.map(summarizeOutcome));
}
/**
@@ -665,33 +784,22 @@ function evaluationCondition<
Gateway,
DriverFailure,
RuntimeFailure,
- RuntimeRequirements,
ConfigurationSchema extends Schema.Schema.AnyNoContext,
>(
definition: EvaluationConditionDefinition<
Gateway,
DriverFailure,
RuntimeFailure,
- RuntimeRequirements,
ConfigurationSchema
>,
-): EvaluationCondition {
+): EvaluationCondition {
return Object.freeze({
id: definition.id,
runtimeName: definition.runtime.name,
runtimeConfiguration: runtimeConfigurationProjection(definition.runtime),
- execute: (
- evaluation: EvaluationCaseDefinition,
- input: EvaluationExecutionInput,
- ) =>
- executeCondition({
- runtime: definition.runtime,
- principal: definition.principal,
- policy: definition.execution,
- conditionId: definition.id,
- definition: evaluation,
- execution: input,
- }),
+ withDefinition: