Skip to content

✨ Run a workflow on a runner and its owner somewhere else (#698) - #763

Open
minkimcello wants to merge 81 commits into
taras:codex/github-actions-software-factory-architecturefrom
minkimcello:claude/698-remote-workflow-provider
Open

✨ Run a workflow on a runner and its owner somewhere else (#698)#763
minkimcello wants to merge 81 commits into
taras:codex/github-actions-software-factory-architecturefrom
minkimcello:claude/698-remote-workflow-provider

Conversation

@minkimcello

@minkimcello minkimcello commented Sep 5, 2026

Copy link
Copy Markdown

Why

A workflow run today needs one machine that holds everything: the SQLite store, the Workspace, the journal, the executor lock, and the tools. That is fine for a laptop and impossible for a GitHub Actions job, which is ephemeral by construction — it can disappear mid-run, and nothing about the run survives it.

#698 is F1 of the software-factory stack: it separates the two halves that were only ever together by accident. Authority and retained state move to one durable owner per run; native work stays where the tools are, on a runner that may vanish at any moment.

What changes

Before: a run's storage, authority and execution were one Deno host over a local directory. A runner that died mid-flight took the run with it, and nothing else could resume it.

After: a run can be owned by one SQLite-backed Cloudflare Durable Object, selected from the public run id by the same arithmetic local discovery uses, while an ephemeral runner performs the native work and submits complete validated transactions. A runner that dies exposes only a prior or a new complete transaction, never a partial one; the next acquisition performs the ordinary stale-execution recovery and resumes from the exact committed WorkflowRun and Workspace frontier. Everything a document can write behaves identically on either host.

Nothing selects the remote host. The shipped entrypoints are local, and trusted code assembles the remote one explicitly from configuration it supplies itself.

How it works

run id → one owner (arithmetic, no registry)
       → executor plane: a real WebSocket upgrade whose lifetime *is* the acquisition
       → read plane / delivery plane: ordinary authenticated POSTs, no acquisition
       → runner materializes one retained root, works in it natively,
         submits content-addressed changes
       → owner validates acquisition + expected root + submitted content,
         then atomically publishes the new root with the filtered journal result

The path from a document to that: useRemoteWorkflowHost() (trusted CLI assembly) → useRemoteWorkflowRunner() (the four WorkflowHost methods over a configured client) → remoteOwnerClient() (one client bound to one run) → the three planes → WorkflowOwnerObject (the Durable Object).

Review guide

Start with: architecture.md, "One remote owner for one run" — the whole contract in four paragraphs — then specs/workflow-workspace-spec.md §13.2.

Then review:

  1. packages/workflow/src/cloudflare/owner.ts and owner-reads.ts — what the owner is, and what it refuses.
  2. packages/workflow/src/remote/client.ts and src/cloudflare/routes.ts — the three planes and the private message shapes, which are one release talking to itself rather than a public wire contract.
  3. packages/workflow/src/deno/remote-runner.ts — the runner assembling the same four methods over that client.
  4. packages/cli/src/remote-workflow.ts — the configured public host, and the narrow capability projection it applies.
  5. packages/workflow/src/remote/workspace.ts and src/workspace/effects.ts — Workspace work routed by the exact storage handle.

Look carefully at:

  • Attachment binding. An attachment is bound to the exact storage handle the begin transition produced, compared by identity to the link this runner's own acquisition opened — two clients on two owners can hold handles that agree about everything except where they came from.
  • The executor connection. It is not a lease: no duration, expiry, renewal, heartbeat or liveness poll. Closing it releases executor ownership without rolling back what committed.
  • Replay. A completed run may read its own retained history from the owner while attaching no external-effect provider at all.
  • Credential containment. Endpoint, release and token live in closure state that reaches no record, event, error or document-visible value.

What must stay true

  • One run, one owner. The run id selects the object arithmetically — enforced by admitRunId/ownerFor, checked by remote-owner-routes.vitest.ts.
  • A client is bound to one run. It refuses another before a token is minted — enforced in remoteOwnerClient(), checked by remote-workflow-host.test.ts.
  • Publication is one transaction. Root, staged content, mappings and the filtered journal result commit together or not at all — enforced by the owner's transaction, checked by remote-publish.vitest.ts and remote-workspace.test.ts.
  • A retained mapping and the journal move together. An owner never presents a root and a mapping whose journal prefix omits the transaction that created them — checked by the Repository continuation in remote-workflow-host.test.ts.
  • The shared surface names no host. Enforced by Tier DLC — DLC13 and host-neutrality.test.ts.
  • Nothing selects the remote host. No selector, no ambient endpoint/release/OIDC source — stated in specs/workflow-workspace-spec.md §13.2 and true of the diff.

How to verify it

  • packages/workflow/tests/cloudflare/*.vitest.ts (14 files, 196 tests) run against a real Durable Object namespace, real SQLite storage, a real WebSocket and real eviction. They catch anything a model of workerd would hide: acquisition outliving a hibernation, a transaction that is not atomic, an owner that answers after eviction.
  • packages/cli/tests/remote-workflow-host.test.ts drives the configured public host end to end: an authored <Repository>/<Git.Switch>/<File> document that is cancelled with its Git proposal in flight and then continued from the retained prefix, and an authored <Agent>/<Session>/<Prompt> document through the shipped Agent profile. It catches a continuation that re-runs durable work, a mapping committed after the first prompt, a reattachment that creates a second conversation, and a conflicting assertion that replaces one.
  • packages/workflow/tests/remote-runner.test.ts proves the runner attaches the handle its own lifecycle opened and no other, and that a cancelled attachment proposes nothing.
  • packages/workflow/tests/remote-interoperability.test.ts proves the local and runner capture implementations produce the same root identity for the same tree — without it, a run would change its Workspace by moving between hosts.
  • packages/workflow/tests/host-neutrality.test.ts and Tier DLC — DLC13 catch a shared module that imports a host entrypoint, detects a runtime, or borrows one host's vocabulary.

Manual: deno task test:cloudflare runs the workerd suite; deno task test packages/cli/tests/remote-workflow-host.test.ts packages/workflow/tests/remote-runner.test.ts runs the two host suites.

Scope

Included

  • One SQLite-backed Durable Object owner per run: record, filtered journal, Workspace roots and content, Agent-session mappings, delivery state, intake records, executor ownership.
  • Connection-lifetime executor acquisition over a real WebSocket upgrade, with admission ordered release → token → run.
  • Three request planes and the gateway that routes on the run id alone.
  • One configured client bound to one run, and an explicit trusted assembly of it into the same four WorkflowHost methods.
  • Workspace publication, Repository/Worktree/Dir composition and the transactional Git components against either owner.
  • Canonical completed replay from what the owner holds.
  • A test-cloudflare CI job, and the *.vitest.ts suffix that keeps those files out of the Deno, Node and Bun corpora.

Intentionally unchanged

  • Choosing the remote host. No runtime or CLI selector, no ambient endpoint, release or OIDC source. The shipped entrypoints remain local.
  • Deployment. No deploy script, workflow, account or binding configuration. The only wrangler.jsonc is the test worker's.
  • F2–F6 and E11. The factory's components and protocol records remain specified and unbuilt; the only software-factory source here is the neutral run-id derivation.
  • Local behaviour. The Deno host's storage, locking and Workspace behaviour is unchanged.

New abstractions

  • WorkspaceHostBinding (create / read / sessions) exists because a document reaches exactly three kinds of Workspace work, and both hosts must answer all three behind the exact storage handle. Two concrete implementations.
  • Transaction.undoable(body) exists because the shared Files and composition rules must be able to undo part of one mutation without knowing which host performs it — a SQLite savepoint locally, an attempt restored from the accepted root on the runner.
  • RemoteWorkflowConfiguration exists because a trusted host must supply run id, endpoint, release, token and capabilities explicitly, and nothing may infer them.

New dependencies

  • @cloudflare/vitest-plugin@1.1.3, @cloudflare/workers-types@^5.20260831.1, vitest@4.1.11 (with @vitest/runner and @vitest/snapshot pinned to match), typescript@^5 — used only to run and typecheck the workerd suite. Existing runners cannot help: acquisition lifetime, owner eviction and transaction atomicity are properties of workerd, and neither Deno's test runner nor Node's can start one.

Generated or mechanical changes

  • pnpm-lock.yaml follows the dependency additions above.
  • package.json and deno.json carry cosmetic rewrites an older pnpm made during development: description re-encoded with a escape, effection reordered among the dependencies, and deno.json's workspace array expanded across lines. Inert, and no behaviour change is intended by any of them.
  • packages/workflow/tests/remote-workspace.test.ts was dedented one level in twelve places by local/no-redundant-test-scope's own fixer; no assertion changed.

Delivery status

  • Audited head: b6c127f9944c0414a661767adfdc94941ab04ccb.
  • Passed locally: lint, typecheck, Node corpus (4,442 tests), Cloudflare typecheck and workerd suite (14 files, 196 tests), JSR dry run, site check and site build.
  • The Deno corpus passed 808 tests and had one environment-only failure because this machine has no Bun executable; the Bun corpus could not start for the same reason. verify:clean passed setup and every offline build/compile/resolution phase, then its Bun interference participant could not start. Required CI remains authoritative for Bun and the final delivery gate.
  • CI had not run on this audited revision when this description was prepared.

Dependency

GitHub base: PR #736 has a fork-only head, so this PR correctly targets its upstream base, codex/github-actions-software-factory-architecture. Until #736 merges, GitHub includes the prerequisite commits in this comparison; the diff contracts to #698 after #736 lands.

This branch is stacked directly on #710 / PR #736 at 817d3dd36cb07fddd972c6ae33716444f3cb75da, which is not in main. origin/main has advanced 33 commits past the merge base the two share, so a diff taken against current main would both attribute unrelated commits to this PR and hide that it depends on unmerged work. Review it against 817d3dd…, and merge it only after #736.

Risks and limitations

  • Nothing selects the remote host yet, so this ships behind trusted assembly and changes no shipped entrypoint. That is deliberate: the selector, the ambient configuration and the deployment are later slices.
  • The private runner-to-owner messages are one release talking to itself. They are journaled by neither side and named in no public type; a mixed-release runner and owner refuse at admission rather than negotiating.
  • The workerd suite needs its own runner. It is invisible to deno task test by file suffix and runs in its own CI job.
  • Bun's corpus was not exercised locally during the delivery audit — no bun binary on the auditing machine — and is covered by CI's test-bun job.
  • Recovery: the branch adds no migration and no deployed state; reverting it removes the remote host and leaves the local one exactly as it was.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — except the pre-existing manifest churn identified above.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

minkimcello and others added 30 commits September 2, 2026 09:33
Turn PR taras#630's draft into a decision-complete first-release architecture. No
product, deployment, authority, identity, ownership, failure, replay, public
form, permission or ordering decision is left for a later Implementor.

The factory specification now settles: the canonical `github-issue-v1` run-ID
derivation and unsupported issue-transfer drift; `User` as the only Stage 1
spelling; the deployment topology of one SQLite-backed Cloudflare Durable
Object, an authenticated executor WebSocket, an ephemeral Actions runner and a
dedicated GitHub App; authenticated ingress order, bounded intake, dispatch
payload and OIDC claims; the exact App permissions, host ceilings, human
permission floor and `.github/workflows/**` denial; suspend-on-every-conflict
as the single Stage 4 profile; the Stage 7 trusted merge, `Git.PublishTarget`
compare-and-swap and remote-effect-before-terminal ordering; and the exact
public contract inventory. Section 8's remaining material decisions are gone.

architecture.md gains the terminology, the remote storage and executor
topology, the delivery-plane generalization, the Project-provider boundary,
comments/readiness/closure, ordered merge, target publication, trusted evidence
execution, terminal settlement, the split trusted host, and ten construct
inventory rows marked "specified by taras#710; implementation unbuilt".

The workflow and Workspace specifications gain the remote host, the executor
connection, which requests need it and which do not, the host-derived public
run ID, and provider-neutral contracts for `Git.Merge`, `Git.PublishTarget`,
`PullRequest.Comment`/`Ready`/`Close`, `Issue.Comment`/`Close`, `Evidence.Run`
and `Project.Status` — none of them reachable by an Agent or generated XMD,
whose write table stays exactly `File:write`, `Dir` and `File.Delete`. The
executable-MDX specification gains the exact authored forms and the frozen
WRH/WGI/WGE/WGM/WER/WFP/WFL acceptance tiers.

Documentation only: no production code, workflow YAML, dependency, generated
artifact or executable fixture changes, and the ownership SVG already agrees
with the amended text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Answer the Architect's three blocking findings on 86a4694. The public contracts
are now closed shapes rather than descriptions of shapes, run identity has one
spelling, and the two terminal paths are separate everywhere.

Finding 1 — exact bindings and durable records. specs/workflow-workspace-spec.md
is the normative schema location for every authored construct: §7.8 defines the
`GitMergeResult` clean/conflicted union, the conflict-entry record, its sorting,
duplicate and missing-side rules, and restoration failure; §7.9 defines the
`Git.PublishTarget` request and result with five exhaustive pre-states; §7.10
defines the comment request/result and how an engine-derived effect identity is
made remotely observable without using body text, plus literal `Ready`/`Close`
bindings; §10.3 does the same for `Issue.Comment` and `Issue.Close`; §10.5
settles `Evidence.Run` — the whole list runs, separate bounded stdout and stderr
with stated truncation, and an explicit table of which cases bind, which fail
and which commit nothing, with cancellation and teardown precedence fixed; §10.6
defines `Project.Status`. The factory specification §11.2 defines the versioned
closed factory protocol schemas — subject, stage, revision, handoff, actor,
outcome, invalidation, verdicts, conflict suspension, Stage 7 decisions, the
frontier and its complete reduction table, and the two asymmetric terminals.

Finding 1 also adds `PullRequest.Merged` (§7.11), the reconciled Git-host
observation that owns the post-publication merged fact, and settles the remote
host boundary (§13.2): the existing four-method `WorkflowHost` stays the host
assembly contract with a Cloudflare implementation beside the Deno one, the
runner-to-owner transport is a closed versioned envelope that refuses rather
than adapts, and an ownership table says which side owns each concern.

Finding 2 — every restatement of the run-ID derivation now uses `canonical
GitHub authority` byte for byte; the undefined Issue-provider spelling is gone.

Finding 3 — architecture.md and the factory specification now state the merged
and abandoned paths as separate ordered step lists, and the terminal record's
two shapes differ exactly as the paths do, so neither can be read as requiring
the other's effects.

Inventories, evidence tiers and the acceptance checklist are reconciled with all
of it. Documentation only; the ownership SVG is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Apply rulings A-K from the Architect's answers on 80884f9.

A. Keep the four-method `WorkflowHost` and add that its transition and request
types — `WorkflowExecutionTransitions`, `WorkflowBeginRequest`,
`WorkflowExecutionBegun`, `WorkflowForkRequest`, `WorkflowForkSelection`,
`WorkflowRunCreation` — are provider-neutral and become package-root public
types, with runtime-named entrypoints keeping implementations and retained
encodings. Recorded as authorized future export work, not performed here.

B. `Evidence.Run` becomes an ordered fail-fast pipeline binding the executed
prefix: `{ completion, authoredCommands, executed, runTimeout? }`, rows carrying
`limit` for whichever of the two host-owned ceilings fired, a `runTimeout`
record for a whole-run expiry between commands rather than a fabricated argv
row, timeouts as ordinary unsuccessful outcomes, and fixed precedence —
cancellation over everything, first infrastructure failure authoritative with a
later teardown failure as secondary evidence, teardown authoritative alone —
with bounded diagnostic evidence retained on a failed effect's Error.

C. A comment provider must support a stable opaque correlation marker or refuse
before its first mutation. The authored logical body is preserved byte for byte
and the correlation representation rides outside it. Absence is now judged
against attempt state: no marker after an attempted-but-uncommitted creation is
permanent ambiguity, not proven absence.

D. `PullRequest.Merged` splits still-open (temporary unavailability) from
closed-unmerged (conflict), and the factory gains §10.4 — a bounded
host-configured retry, then a durable machine wait with its own protocol record,
woken by intake or an explicit resume and carrying no verdict.

E. A configured total bijection between the nine stages and nine exact Project
status option IDs, validated against a complete reread and refusing before any
intake, token, run or projection.

F. `Git.Merge`'s `purpose` is validated authorization against the
provider-authenticated merge ceiling, not write-only provenance.

G. The `RunnerRequest` envelope is deleted. The runner and owner ship as one
release identity checked by build fingerprint at admission; the messages are
private, while the owner-side authority invariants stay public and exact.

H. `FactoryTerminal` keeps journal event references, with cross-path, kind,
completeness and agreement validation spelled out.

I. The factory protocol schemas stay in the factory specification §11.2; the
other four documents link rather than duplicate.

J/K. Mixed wrapping left alone, new prose unwrapped; the tenth construct is
reflected everywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
taras#710)

Ruling C asked for the comment contracts, the architecture, the WGE evidence and
the generic external-effect explanation to agree. The first three landed in
0d08dcc; §10.2 did not.

Split the two kinds of reconciled effect where the difference actually lives.
An effect that mutates a subject already there — Push, a numbered pull-request
update, ready, close, issue close, Project status — reads that subject, and a
complete observation is decisive whether or not the effect has attempted
anything. An effect that creates a new object the host names has nothing
pre-existing to read and no client-supplied idempotency key, so its completion
is visible only through a correlation value it wrote itself, and absence means a
different thing before and after a mutation has been attempted.

Only the second kind carries attempt state, and it narrows the decision in one
place: unattempted with nothing found performs once, while attempted with no
committed completion and nothing found is permanent ambiguity rather than
absence. A provider that cannot write, preserve and completely query such a
value refuses the effect from observation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Four narrow corrections from the Architect's verdict on b364b98.

1. The merged-observation wait is not a typed-answer suspension. A machine wait
is a second durable wait kind inside the existing lifecycle: it asks nobody
anything and ends because a later execution observed a provider again, so it
has no response schema, no `xmd workflow answer` route, no form and no bound
value, and it publishes no `suspension_request` and consumes no
`suspension_answer`. It shares only the atomic boundary — its `machine_wait`
event and the `suspended` status commit together, and the acquisition is
released after that commit. Its identity is a `waitId`, never a suspension ID;
`MergedObservationWait.suspensionId` becomes `waitId`, while
`ConflictSuspension.suspensionId` and the typed-answer protocol are untouched.
A new `MergedObservationWake` names the same `waitId` and a closed `source`,
with `intakeId` required exactly for a provider intake and absent exactly for
an operator resume. Waking is permission to look again: the intake retains a
bounded notification carrying no answer, verdict, stage, transition or
observation result, a later executor consumes it and appends the wake event in
one transaction, and a resume without a pending wake or operator authority
reports the same wait and settles suspended again.

2. The generic create-effect rule now distinguishes its two safe mechanisms. An
effect with a provider-native client key — an Issue upsert, a pull-request
upsert — reconciles on that key and carries no attempt state; creating an
object is not by itself what makes an effect attempt-stateful. A comment has
neither a native key nor a pre-existing subject, which is why it needs the
marker and the unattempted/attempted distinction. An effect with neither
mechanism refuses before its first mutation, as an intentional constraint.

3. Completed replay may reach and read the run's durable owner — lifecycle
storage access, not external-effect replay — while attaching no external-effect
provider, performing no effect again and starting no native operation. The
"no remote storage session" wording is gone from every document.

4. The authored comment body is preserved byte for byte as the authored portion
of the projection, with transport metadata outside it, rather than the provider
payload being claimed to equal the authored bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
)

The first slice of F1: the seams a remote host needs, and the identity it is
addressed by. No Cloudflare code yet — this is the boundary work that has to be
true before a second host can exist.

`WorkflowExecutionTransitions`, `WorkflowBeginRequest`, `WorkflowExecutionBegun`,
`WorkflowForkRequest`, `WorkflowForkSelection` and `WorkflowRunCreation` now
export from the package root, which is where they mean what they mean. They were
already defined in the provider-neutral lifecycle module but published only
through `@executablemd/workflow/deno`, so the shared CLI imported its own return
type from an adapter and a second host would have had to load that adapter to
name it. The Deno entrypoint keeps its re-exports for source compatibility with
a comment saying what belongs behind it — implementations and retained
encodings, not the shape of a request — and the CLI and its tests now import
from the root.

`deriveFactoryRunId()` implements the settled derivation: lowercase unpadded
RFC 4648 Base32 over all 32 SHA-256 bytes of `github-issue-v1`, NUL, the
canonical GitHub authority, NUL, the exact issue node id. The authority folds
case and keeps a non-default port; a scheme, user information, path, query,
fragment, whitespace, malformed host or port, and a written-out default port
each refuse by name rather than being repaired, because two spellings that both
became one authority would be two runs quietly becoming one. The node id is held
to non-empty and NUL-free and otherwise passes through byte for byte.

Three tests. The identity suite checks fixed vectors computed outside this
implementation, the RFC 4648 §10 encoding vectors, that the NUL separators make
(authority, node id) unambiguous, and every refusal. A host-neutrality suite
walks the shared modules and fails on a host-owned specifier or a runtime
detection outside a runtime-named entrypoint, so a Cloudflare import cannot
leak into shared code unnoticed. A host-boundary suite pins `WorkflowHost` to
exactly four methods and proves it is satisfiable from root-exported types
alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…aras#698)

`DLC13` in packages/workflow/tests/workspace-effect.test.ts scans
`packages/workflow/mod.ts` and `packages/workflow/src/**` (minus the Deno
adapter) and refuses a set of names that includes `GitHub`. Its reason is that
the shared external-effect boundary exists so any Git host can be adapted to it,
and "the first adapter naming itself in a shared contract is how a neutral
surface quietly becomes one provider's."

8f27b04 put `deriveFactoryRunId()` under `packages/workflow/src/factory/` and
exported it from the package root, which broke that test — correctly. The
derivation names GitHub in its scheme tag, its authority rule and its node id,
because the software factory is a GitHub product by definition.

Move it to `packages/cli/src/factory-run-id.ts`, beside the CLI's existing
`github-issues-config.ts` where naming GitHub is already legitimate, and take it
back out of `mod.ts`. The module and its tests are unchanged otherwise.

The host-neutrality scan added in 8f27b04 no longer exempts `cloudflare.ts` or
`src/cloudflare`: neither exists yet, and a scan that exempts a path nothing
occupies is a claim about a boundary nobody drew.

Where the derivation lives permanently is not settled by this commit. The
Durable Object owner needs it too, and F1 cannot place it until DLC13's scanned
set is reconciled with a Cloudflare adapter subtree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Applies the Architect's answers to Q1 and Q2 in .vscode/698/698-a-1.md.

Q1. `DLC13` now excludes `packages/workflow/src/cloudflare/**` and
`packages/workflow/src/software-factory/**`, each with its own reason. The two
implementation subtrees are runtime-owned — scanning an adapter for the
vocabulary of the runtime it adapts is a category error, and Code Rule 12 puts
host behavior behind exactly those names. The software factory is the other kind
of exception: not a runtime adapter, still held to the host-import and
runtime-detection rules by `host-neutrality.test.ts`, and allowed only the
product vocabulary, because §1.1 of the factory specification makes GitHub the
subject matter of that contract rather than one provider capturing a neutral
boundary. `packages/workflow/mod.ts` and every shared module stay scanned, and
no forbidden word gained an exception.

Beside the existing Deno assertion, `found` is now checked to contain no
`/src/cloudflare/` and no `/src/software-factory/` path, so an exclusion that
matched nothing or matched too little cannot pass quietly.

`host-neutrality.test.ts` gains `cloudflare.ts` and `src/cloudflare` in
`RUNTIME_OWNED` and asserts the Cloudflare subtree is absent from what it scans.
`software-factory.ts` is deliberately not runtime-owned there: it uses the
cross-runtime Web primitives and is checked like any shared module, which the
new assertion that `src/software-factory/run-id.ts` is among the scanned modules
now proves.

Q2. The derivation moves from `packages/cli/src/factory-run-id.ts` to
`packages/workflow/src/software-factory/run-id.ts`, published as
`@executablemd/workflow/software-factory` from both manifests. One
implementation, reachable by the provider host and by F2's GitHub intake without
either carrying a hash that has to agree byte for byte with the other's. The
package root does not re-export it — that neutrality is the point — and no
forwarding copy is left in the CLI. Its tests move to the workflow package and
import the published subpath, so the export map is what they exercise.

Q3 needs no change yet; it governs §8 and §9, which are unstarted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Plan §13. `@cloudflare/vitest-plugin@1.1.3` and `vitest@4.1.11` as root
development dependencies, which is what brings workerd in — plan §12 requires
the real Durable Object namespace, real SQLite storage and real WebSocket
admission, and an in-process fake is not evidence for any of them.

Both lock layouts are updated in the documented order: the pnpm add, then
`deno install --frozen=false`, then `deno task setup`. `pnpm-lock.yaml` gains
1231 lines and deletes none; `deno.lock` gains the npm graph for the same
packages.

Two things worth knowing for anyone reproducing this.

The lockfile is `lockfileVersion: '9.0'` and `package.json` declares
`pnpm@9.15.0`, but the pnpm on PATH here is 7.5.0, which cannot read a v9
lockfile — it warns "Ignoring broken lockfile" and rewrites it as
`lockfileVersion: 5.4`. That downgrade happened once and was reverted; this
commit was produced with the declared version through `corepack pnpm`. Anything
that shells out to a bare `pnpm`, `deno task setup` included, needs 9.15.0 ahead
of 7.5.0 on PATH or it will silently downgrade the lockfile again.

pnpm 9 also re-sorts `package.json` dependency keys, which is why `effection`,
`mdast-util-to-string` and `zod` move. That is the package manager's own
canonical ordering; hand-restoring it would only be undone by the next install.

`deno task setup` still exits 1 at its last step, `build:web`, with
`Module not found "file:///…/bundle"` from
`deno bundle --packages=bundle`. That is not this change: the identical argv
succeeds when run directly and under a preflight-shaped parent, it fails only
through `@effectionx/process`'s `exec()`, `packages/web/generated/` has never
existed in this checkout, and the browser bundle's module graph does not reach
vitest or workerd. `deno task check`, `deno task lint` and the focused suites
all pass on the new dependency state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`@executablemd/workflow/software-factory` now publishes only
`deriveFactoryRunId`, `admitFactoryRunSubject`, `FactoryRunSubject`,
`FactoryRunSubjectFailure` and `FactoryRunSubjectError`. The scheme tag, the
Base32 alphabet, the authority rule, the preimage layout, the encoder and the
length constant are implementation: a caller that could reach them could also
reimplement the hash, and two implementations of an identity that must agree
byte for byte is the failure §1.1 exists to prevent.

`factoryRunIdPreimage()` and `base32Unpadded()` stay visible to their own module
so the encoding tests can prove an internal algorithm, and are absent from the
entrypoint. Everything the tests assert about public behavior — the fixed
vectors, the 52-character shape, case folding, the non-default port, and every
authority and node-id refusal — now goes through `admitFactoryRunSubject()` and
`deriveFactoryRunId()`.

The `as BufferSource` assertion is gone. `factoryRunIdPreimage()` returns an
`ArrayBuffer` rather than a view, which is what `crypto.subtle.digest()` accepts
with no assertion at the call site, so Code Rule 6 holds without one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…aras#698)

Plan §12's harness, and the first thing it was built to answer.

`vitest.config.ts` at the repository root runs `*.vitest.ts` under
`@cloudflare/vitest-plugin` against real workerd, with a wrangler config
declaring `StorageProbeObject` as a `new_sqlite_classes` Durable Object. The
suffix keeps these files out of `deno task test`, `pnpm test:node` and
`bun run test:bun`: discovery matches `*.test.ts`, and
`scripts/tests/test-file-discovery.test.ts` still agrees with it, so nothing is
stranded and no runtime exclusion was needed. `pnpm test:cloudflare` and
`deno task test:cloudflare` run it.

Two things had to be settled to get it running. The plugin binds to `vitest`
through peer dependencies, and pnpm was resolving a second `vitest@4.1.11` copy
with a different peer set, so the pool was configured but never took over and
every test reported "failed to find the current suite" — adding the plugin's
declared `@vitest/runner` and `@vitest/snapshot` peers at the root collapses
that. The config lives at the repository root rather than in the package,
because a package-level config resolves `vitest` from the package's own
isolated `node_modules` and multiplies the copies again.

`StorageProbeObject` asks the runtime what it accepts rather than assuming.
Recorded on workerd 1.20260831.1:

- `PRAGMA application_id` and `PRAGMA user_version` — refused, read and write,
  with `not authorized: SQLITE_AUTH`.
- `sqlite_schema` introspection, ordinary DDL, and a plain metadata table — all
  fine.
- One outer `ctx.storage.transactionSync()` — fine.
- A reentrant `transactionSync`, and a direct `SAVEPOINT` — refused: the runtime
  requires its own transaction API instead of SQL transaction statements.
- DOFS schema initialization and DOFS filesystem writes outside a transaction —
  fine.
- A DOFS filesystem write *inside* one `transactionSync` — refused, because
  `writeFileSync` opens a `transactionSync` of its own and that nests.

The last two contradict plan §4, which has the owner reuse the pragma-based
recognition the Deno host uses and commit Workspace mutations inside one
`ctx.storage.transactionSync()`. Neither is possible as written. The probe is
committed so the finding is reproducible rather than reported.

An earlier version of the probe called the asynchronous `WorkspaceFilesystem`
wrapper and never awaited it, which reported success for work that had failed.
It now uses the same synchronous primitives the Deno provider imports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`@cloudflare/vitest-plugin` binds to `vitest` through peer dependencies, and the
pool only takes over when the plugin and the CLI hold the same instance. pnpm
was producing two: `acpx` depends on `tsx@4.23.1` exactly, the root range
`^4.19.0` resolved to `4.21.0`, and `vite` peers on `tsx`, so `vitest@4.1.11`
existed twice under different peer contexts. The plugin bound to one and the
root link to the other, which is why the pool reported itself configured while
every test failed with "Vitest failed to find the current suite".

One `pnpm.overrides` entry pinning `tsx` collapses that. Both the root link and
the plugin now resolve the same `vitest`, and `pnpm test:cloudflare` passes.

Ordering note for anyone reproducing this: `deno install` prunes the links pnpm
placed — `scripts/deps.ts` says so in its own header — so `deno task setup`
leaves the pnpm-owned test tooling unlinked and `pnpm install` afterwards
restores it. Setup itself completes: exit 0, "ready", browser bundle generated,
lockfile still v9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
taras#698)

The storage correction from `.vscode/698/698-c.md`.

`packages/workflow/src/sqlite/workflow-schema.ts` now holds version 1 once: the
application identity, the logical version, every declared table and index, the
DDL, the expected inventory, the pre-release shapes, and
`declaredStructureFailure()`, which compares what a database declares against
what this build writes and answers with the disagreement instead of raising it.
Two adapters keeping their own copy of that DDL would be two schemas that happen
to look alike, and the first amendment either missed would be a run neither
could recognize. The module names no runtime, owns no connection, path,
transaction or lifecycle authority, and is published from no entrypoint.

`src/deno/schema.ts` loses 505 lines and keeps what is genuinely its own:
`DatabaseSync`, filesystem paths, SQLite error translation, and its existing
`PRAGMA application_id`/`PRAGMA user_version` carrier. It reports the shared
finding as its own failures, so the Deno host's behavior and its released
recognition are unchanged.

`src/cloudflare/marker.ts` is the other carrier. A Durable Object's SQLite
refuses both pragmas — `not authorized: SQLITE_AUTH`, on read as well as write —
so that adapter records the same two values in `_xmd_workflow_schema`, a
singleton row fixed at `id = 1` by a CHECK and a primary key so a second
identity row cannot exist. Same logical version, different physical carrier;
adapter-private recognition metadata, not a WorkflowRun record, journal value,
exported field or second schema. `src/cloudflare/storage.ts` reconciles the
runtime's concrete `SqlStorageValue` rows with the vendor's structural row type
in one place, and deliberately does not forward `transactionSync`.

`DLC13` excludes `src/sqlite/**` with its own recorded reason and asserts the
subtree is absent from what it scanned; `host-neutrality.test.ts` keeps scanning
it and now proves it is among the modules checked.

The exploratory probe moves out of production source into
`tests/cloudflare/support/`, and its console dump becomes three asserted tests
matching stable categories rather than platform wording: the pragmas are
refused as unauthorized; direct savepoints, reentrant transactions and a DOFS
filesystem write inside an owner transaction are all refused toward the storage
transaction API; and ordinary DDL, `sqlite_schema`, an outer transaction, DOFS
schema initialization and a strict metadata table are accepted.

`pnpm check:cloudflare` type-checks the Cloudflare production source and its
tests against `@cloudflare/workers-types`, which the root Deno check cannot do
because `cloudflare:` modules do not resolve there. Two `pnpm.overrides` pin
`tsx` and `@cloudflare/workers-types`: both are peers that were splintering
`vitest` and the plugin into several instances, and a single copy of each is
what makes the pool engage. `@cloudflare/workers-types` is pinned to
`5.20260831.1` rather than the newest, which Deno's minimum-dependency-age
policy blocks.

Ordering, unchanged from `670ad0c` and now deterministic in both directions:
`deno task setup` completes and leaves the workerd suite unable to run, and a
`pnpm install` afterwards restores it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The Cloudflare owner's storage paths, and the evidence that they hold.

`src/cloudflare/owner-transaction.ts` is the transaction every authoritative
commit runs inside. It enters `ctx.storage.transactionSync()` once and hands
DOFS a wrapper whose `transactionSync` runs its callback directly — inside the
real callback that is not a weaker promise, because the outer transaction is
already open, and it is the only way DOFS can participate at all: asked to
transact while it believes one is open, the vendor falls back to `SAVEPOINT`,
which the runtime refuses, and every DOFS filesystem primitive opens a
transaction on the way in. The enlistment is created for one callback, refuses
use outside it, and clears the DOFS resolve and blob caches on both sides so
nothing populated from rows a rollback discarded is read later. The vendored
snapshot is untouched and `deno task vendor:verify` still passes.

`src/cloudflare/recognition.ts` initializes pristine storage in one transaction
— schema, DOFS schema, run row, then the marker last, so the code says what the
marker means even though atomicity hides the ordering — and recognizes it again
through the same four conditions the Deno host distinguishes: foreign,
unsupported version, corrupt, or a version-1 run.

Fourteen assertions on real workerd, in two suites. Initialization writes the
marker and is recognized again; storage that already holds an object is refused
rather than written into; nothing at all, objects without a marker, another
application's identity, version 2, version 0, and a dropped table are each
refused as their own condition. A mixed commit publishes a DOFS filesystem
write and a WorkflowRun row together. A body that throws after changing both
leaves neither — the run row is unchanged, the file is absent, the object still
recognizes, and a later commit succeeds from the frontier the failure left.
That last test is the one the whole design turns on.

`pnpm check:cloudflare` type-checks all of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`test-cloudflare` runs `pnpm check:cloudflare` and `pnpm test:cloudflare`, and
is in `green.needs`. It runs on every event and carries no condition, so `green`
requires success from it unconditionally.

The job owns evidence no other job can produce. A Durable Object's acquisition
lifetime, its eviction and its transaction atomicity are properties of that
runtime, and the `.vitest.ts` files proving them are invisible to the Deno, Node
and Bun corpora by design — so without this job the evidence would simply stop
running while everything else stayed green.

`ci-workflow.test.ts` already fails when a job is missing from the aggregate;
verified by removing the entry and watching "requires every other job and no
future job can be omitted" fail. Added a test naming this job's two commands as
well, so what it is *for* is legible rather than only that it is depended on.

`pnpm install` is the job's last preparation step: `deno install` prunes the
links pnpm placed, and the plugin only takes over the pool when it and the CLI
hold the same vitest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The executor plane: who may advance a run, and what they may say.

`release.ts` compares an exact build fingerprint. `admission.ts` holds verified
OIDC claims to a configured policy — issuer, audience, repository ID, owner ID,
event name, workflow ref, workflow SHA and the immutable workflow identity —
checking IDs rather than names, because a repository can be renamed and a check
on the name would admit whoever holds it today. It reads a closed claim set, so
a claim this contract does not name cannot be depended on, and it takes claims a
verifier already authenticated rather than a token: how a signature is checked
is not this module's business.

`acquisition.ts` makes the connection the acquisition. Hibernation is why it
cannot be a field — an evicted object has no memory of what it admitted — so
authority is the pair the runtime hands back: `getWebSockets()` says which
sockets are real and a bounded attachment says what one was admitted as. Copied
attachment bytes prove nothing, because the question is not whether a value
looks right but whether this socket is the one live socket holding an
acquisition. There is no lease, expiry, renewal, heartbeat, alarm or poll.

`commands.ts` reads the private transport strictly: unknown command, unknown
member, wrong kind, oversized message, too many chunks — each refuses whole,
nothing is partially adopted. The answer is a serialized record keyed on
`outcome` rather than an Effection `Result`, because an `Error` does not cross a
connection; Code Rule 13 governs in-process results and this is not one.

`owner.ts` is the Durable Object. Its admission order is the contract: build
before token, token before run, acquisition last, so a refusal at any step
leaves no acquisition and no state. A message proves its acquisition before it
is parsed. A closed connection releases ownership, rolls nothing back and
settles nothing — an executor that disappeared decided nothing.

`routing.ts` selects the owner arithmetically and admits the run ID first,
because `idFromName` answers for any string and a mistyped id would silently
address a fresh, empty owner.

Twenty-nine assertions on real workerd across three suites. A mismatched build
is refused while its claims are deliberately unusable, proving the order. Every
policy claim is refused one at a time. A second healthy executor is refused
rather than followed. A closed connection owns nothing and the next executor may
take it with no lease having expired. A stranger's socket is refused before its
command is read.

`./cloudflare` is an npm export only: JSR cannot typecheck a `cloudflare:`
entrypoint, and `deno task check:jsr` stays green because the Deno host is what
JSR serves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`transact()` for a run whose storage is somewhere else, as `698-a-1.md` Q3
settles it.

A Durable Object commits synchronously and cannot hold a transaction open
across a network wait, so the obvious reading is unavailable. What makes it
tractable is that the body does not need the transaction open while it runs: it
needs the starting history, somewhere for its writes to go, and all of them
landing together. So the callback runs in a runner-owned scope against a
collector. The starting frontier is one bounded read that opens and closes its
own owner-side read. Appends go into a local buffer that `readAll()` reads back
after the starting prefix, so a body sees its own writes. Nothing is sent while
the body runs; when the body and everything it started have torn down, one
closed intent goes to the owner.

The callback is never serialized, interpreted or executed on the owner. That is
what makes arbitrary control flow safe — nothing tries to infer what the body
did, and only what it *enlisted* travels.

`src/remote/collector.ts` rather than `src/cloudflare/**`: this is the client
half and it runs on the runner, not in the Worker. It names no host, so the
ordinary Deno check and both boundary scans cover it.

Nine assertions with a deterministic fake link, because Cloudflare mechanics are
not the subject — what the owner does with an intent is proven on workerd, what
the client sends is proven here. One intent carries what was enlisted and the
frontier it was proposed against. `readAll()` gives read-your-writes. A body may
cross a suspension point with no owner transaction open. A body that throws
sends nothing and leaves the gate closed. An owner refusal is returned instead
of the body's value, which never crossed the connection. A nested transaction
and an ordinary same-handle operation inside a body each refuse. A handle used
after its body closed refuses. And an event mutated after it was appended
commits as it was handed over, because the collector cloned it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The runner half of the connection.

`src/remote/client.ts` holds one connection open for the calling scope and
correlates answers by the id they name rather than by arrival order — a socket
delivers what the owner sent whenever it sent it, and a client that assumed
order would attribute one command's refusal to another. Teardown fails every
request still waiting, because a caller blocked on an answer that can never
arrive would outlive the connection it asked through.

It decides nothing about the run. A refusal comes back as an answer rather than
a transport failure: what a command means is the owner's, and a client that
interpreted a refusal would be a second place deciding what a run may do.

Seven assertions against a socket a test drives by hand. The command reaches the
wire carrying its id; two answers returned in the opposite order to the asking
still reach the right callers; a refusal is handed back as an answer; an answer
that is not JSON and an answer naming nobody are both dropped without disturbing
the caller that was waiting; a connection that ends fails the request in flight
and refuses the next one; and a second request under an id already in flight is
refused rather than silently replacing it.

This is client code and it names no host, so it lives beside the collector in
`src/remote/` where the ordinary Deno check and both boundary scans cover it.

Not included, deliberately: `packages/cli/src/remote-workflow.ts`. The four-
method assembly composes client operations — a remote `WorkflowRunDatabase`,
lifecycle, delivery and inspection over this connection — and those do not exist
yet. A host whose four methods all raise would be the placeholder the plan says
not to add, so the assembly waits until there is something real to assemble.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Corrections 1 and 2 from `.vscode/698/698-c-2.md`. The first is a real hole I
opened, and the review is right about it: `admit()` took an
`AdmissionRequest.claims` and compared its values to the policy, so a caller
could assert every configured identity and obtain the acquisition without ever
holding a signed token. A comment saying a verifier had authenticated them was
not an authority boundary — `OwnerConfiguration` held no verifier, and the
claims arrived through the same request surface the owner distrusts.

`src/cloudflare/token.ts` verifies a compact JWS: one algorithm family by
allowlist rather than by reading `alg` and believing it, keys the deployment
configured, signature, then temporal validity — and only then is a payload
member read as a claim. The raw token, the key material, the header and the
claims the policy does not name stop there; none is retained, attached,
journaled or returned.

`admitClaims()` and `parseClaims()` are no longer exported. `admitToken()` is
the only way into that module, because an exported "check these claims" is
precisely the surface that made this forgeable. `AdmissionRequest` now carries
the raw token and has no member for a verified result, a claim set, an
acquisition identity or verification material — a request that could name any of
those would be a request choosing what it is allowed to be. Verification
material is closure state on the owner.

`admit()` is an Effection operation, since verification suspends; the test owner
drives it through one scope at the runtime callback boundary.

Correction 2: the acquisition correlation is minted on the owner after both
checks pass, from `crypto.getRandomValues`, and is no longer a caller argument.
It partitions acquisition-private staging and duplicate handling and is not a
bearer credential — the exact live socket is still what proves a message may
act.

Thirty-five assertions. Tokens are signed with an RSA key pair generated in the
test process, so these are real signatures rather than a stub that agreed. A
correctly signed token admits; an edited payload, a wrong key under a configured
key id, an unknown key id, `alg: "none"`, an absent token, a non-JWS, an expired
one and a not-yet-valid one each refuse before acquisition and before state. The
wrong-release test now presents an unusable token, so it still proves the build
is compared first. Two sequential acquisitions receive different correlations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…ed (taras#698)

Corrections 3, 4 and 5 from `.vscode/698/698-c-2.md`. All three were real.

Correction 3. `transactRemotely()` checked `gate.open`, then suspended in
`frontier()`, and only took the gate afterwards — so two calls on one handle
could both pass the check and act from the same starting frontier. It also
released the gate when the body ended, before `commit()` returned, so later work
could run while which state won was still undecided. The gate is now taken
synchronously before the first suspension and released in one finalizer after
the commit answer, on success, refusal, body failure, transport failure and
cancellation alike. The transaction object still closes when the body does, so a
retained handle refuses while the handle-level gate is held.

Correction 4. `append()` cloned, but `readAll()` handed back references into the
collector's own arrays: a body could read an event and mutate what it received,
and the committed intent would differ from what `append()` admitted. Every
crossing is now a fresh copy — in, out, and into the intent — and the
collector's array never leaves. Events are admitted rather than assumed, with a
count bound and an aggregate serialized-byte bound; an invalid or oversized one
fails locally and sends nothing.

Correction 5. The connection dropped malformed answers and answers for unknown
ids, which meant a malformed reply to an in-flight commit left the caller
waiting forever while the owner may already have committed. All three of those
are evidence that the two sides disagree about which command completed, so the
channel now fails closed: an unreadable answer, an answer naming a request
nobody made, and a second answer to a request already settled each stop the
channel, close the socket and reject every waiter. Owner refusals remain typed
answers, and answers are bounded.

Nine assertions on the channel and fifteen on the transaction. Two of the
transaction ones are the concurrency cases the review asked for: a second
transaction refuses while the first is suspended in `frontier()`, and the handle
stays owned while a blocked `commit()` is undecided. Two more prove detachment
from both directions — a reader mutating what `readAll()` returned, and a caller
mutating what it appended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
D1 item 1, and Ruling 1 from `.vscode/698/698-c-4.md`.

The owner has to parse a settle request with the shared
`parseDocumentExecutionCompletion()`, and could not: `storage/record.ts` imports
`canonicalize` from the `@executablemd/core` root, whose barrel reaches
`node:crypto`, `node:process` and `node:readline/promises`. None resolves in a
Workers typecheck, so a pure function was unreachable because of where it sat.

`canonicalize()` moves to `packages/core/src/canonicalize.ts`, importing only a
type. `canonicalFingerprint()` stays in the Node-capable module, importing the
pure function and `node:crypto`. Both keep their package-root exports and their
behavior, and `@executablemd/core/canonicalize` publishes the pure half.

Two more predicates blocked the same graph for the same reason, which the ruling
anticipates and authorizes handling the same way. `isComponentName` was
co-located with the registration machinery and `isCanonicalTarget` with the
document-target catalog and its Markdown parser; both are string arithmetic.
They move to `src/component-name.ts` and `src/document-target-spelling.ts`, with
`@executablemd/core/component-name` and `@executablemd/core/document-target`
selecting them. `storage/definition.ts` imports through those.

No copy of any of them exists — each original module re-exports the leaf, so
there is one implementation and the root surface is unchanged. This is not a
`portable` barrel: three narrowly named leaves, each holding what it is named
after.

Two adjustments fell out. `tsconfig.cloudflare.json` no longer sets
`exactOptionalPropertyTypes` or `noUncheckedIndexedAccess`: it was stricter than
the repository holds itself to, so it failed pre-existing shared code the Deno
check accepts, and a check that invents rules proves the wrong thing. And the
percent-decoder now states `ignoreBOM: false` — already its behavior everywhere,
and Cloudflare's own type declares both `TextDecoder` options required.

Evidence. `packages/core/tests/canonicalize.test.ts` proves root and subpath
answer identically, including for `__proto__`, and that the fingerprint still
composes over the same ordering. `settle-parser.vitest.ts` runs on real workerd,
which is the assertion that matters: it loads the shared parser where a Node
builtin is genuinely absent, so a test-only import could not have passed for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
D1 items 2 through 6 from `.vscode/698/698-c-4.md`.

**Token.** `verifyToken()` accepted a token with no `exp`, and a non-numeric
`exp` or `nbf`, because it checked those claims only when they happened to be
numbers — so omitting one was treated as satisfying it. All three of `exp`,
`iat` and `nbf` are now required finite integer NumericDates. The expiration
boundary is expired, per RFC 7519 §4.1.4's "before". An `iat` in the future
beyond tolerance is not-yet-valid rather than accepted. The header must say
`typ` is a JWT, and `kid` must name exactly one configured key: the old filter
fell back to an unkeyed candidate, so an unrecognized key id still got a
signature check against whatever else was configured. The token and its segments
are bounded before anything is decoded, and skew is bounded at both ends —
negative would reject tokens for being on time, and unbounded is
indistinguishable from not checking.

**Transaction ownership.** The module-global `let open` is gone. It was shared
by every Durable Object in an isolate, so one object's transaction would refuse
another's. The ruling suggested a `WeakSet` keyed by storage; this repository's
`local/no-module-scoped-registry` forbids that too, and for a related reason. So
the claim belongs to the object: `OwnerTransactions` is created and held by the
owner, its lifetime is the object's, and no other object can see it. Same
guarantee, no process-lifetime table.

**Settle.** `{ status: string }` becomes a closed request carrying
`completion: DocumentExecutionCompletion` and `expectedWorkspaceRootId`, with
the completion read by the shared `parseDocumentExecutionCompletion()` — a
failed parse becomes this transport's own `malformed-member` without carrying
the parser's message, which names members a request supplied.

**Answers.** `ask()` now takes the parser for its own success value and returns
`OwnerAnswer<T>`. `unknown` exists only at the JSON boundary; a value the
command's parser cannot read fails the channel closed like any other
disagreement about what completed, and a refusal is delivered without consulting
the parser at all. The generic stays inside the closure the request built, so
the reader settles an answer without asserting what it is.

**Removed claims.** `duplicate-conflict` is gone until the mechanism that
produces it exists. `perform()` stays abstract and the echo stays test-only.
Every checked-then-asserted `Record<string, unknown>` became `Object.entries`
narrowing, and the one double assertion in the tests became a helper that passes
`unknown` through. One documented assertion remains, in `storage.ts`, bridging
two type declarations of the same runtime rows.

Forty-eight workerd assertions and thirty portable ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Closing two gaps in D1 item 5 that I found auditing my own work against the
list, rather than leaving them for review.

The refusal was checked to be a string and nothing more, so an arbitrary remote
sentence became this side's public failure identity — and a refusal is something
a caller branches on. It is now held to the category shape the owner actually
produces, and the correlation id is bounded. Anything else is an answer this
build cannot read, which fails the channel closed like every other disagreement
about what completed.

Item 5 also asked for cancellation cleanup evidence and there was none. The test
asserts what is observable rather than what I first assumed: Effection halts a
cancelled task instead of raising into it, so the proof is that the scope
completes at all with a request in flight — a waiter nothing settled would hang
teardown — and that a late answer afterwards reaches nothing and raises nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The runner's socket is the executor acquisition, and D1 left it behind. The
resource installed listeners and never removed them, never closed the socket,
and never observed `error`, so a scope that ended — normally, by failure, or by
cancellation — left the owner looking at a healthy socket that no longer had
anybody on the other end. With no lease and no heartbeat by design, that made
the run unadvanceable by anybody, forever.

Teardown is now one idempotent operation with one owner. Scope exit,
cancellation, a remote close, a socket error, a protocol failure, a
command-specific parse failure and a failed send all reach it; it runs once,
removes the exact callbacks it installed, and closes the socket once. The
finalizer is registered before anything can suspend.

Answers are now read where they arrive rather than through a signal the reader
drained later. That ordering is the point: a close arriving in the same turn as
an unreadable answer used to reach teardown first and tell the caller `closed`
for something that was actually `malformed-answer`. What went wrong is decided
where it is observed.

The envelope is closed for real: each outcome declares its whole key set, so a
performed answer carrying a `refusal`, a refused one carrying a `value`, a
missing member and an unknown member are all refused. An outgoing correlation id
is held to the contract an incoming one is held to, and refusal text gets its
own small bound. The regular expression proves spelling, and the comment now
says so — narrowing to the declared union stays the adapter's job.

A retained Workspace root is a content identity, so every command that names one
parses it as 64 lowercase hexadecimal characters rather than as any non-empty
text.

The transaction machinery is no longer exported from the Cloudflare subpath. It
stays private to `src/cloudflare/**`, owned by the object.

The old teardown test asserted nothing: its fake retained the message callback,
so the connection could leak both listeners and the socket and still pass. The
fake now counts closes and live listeners, and the evidence is what those
counters say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
An admitted connection could prove who it was and ask nothing. This gives it
the reads a runner actually needs — the committed frontier, one retained
Workspace root, and the content that root names — and the private mechanics
those reads depend on.

Reads are bounded and coherent. One frontier request returns the parsed run
record, the current canonical root and the last journal event that existed at
that moment; the journal itself comes back in anchored pages, so a journal
larger than one message is still one snapshot and later appends cannot enter an
earlier one. Each page carries its own predecessor, and the runner reassembles
them by checking rather than trusting: a page that skips, repeats, reorders or
ends in the wrong place closes the connection before one event reaches a
caller, because half a journal that looks whole is worse than none.

A root comes back as its canonical manifest, then one referenced piece at a
time. The owner proves the manifest is canonically encoded, that its identity
is the digest of its own bytes, that the retained references are exactly the
ones the entries name, and that every piece it sends is referenced by that root
and hashes to the digest it is asked for. The runner proves it all again on
arrival. The owner being honest is not evidence about the wire, and content
that is not what it is named must never become a materialization.

A runner that hears no answer cannot tell a lost question from a lost answer,
so it asks again. That is only safe if asking twice is asking once. Every
command is now decided once per acquisition and its decision retained beside
it. Two requests are the same request when their parsed commands are equal, so
member order does not make a retry into a new command and a changed value does:
reusing an id for something else is refused as a conflict rather than answered.
Reads whose answers are fixed by immutable state and an anchor the request
already carries are remembered as a decision to read again; the frontier is
kept whole, because it is the one read whose answer would otherwise move, and a
retry that returned a later frontier would hand back a snapshot nobody asked
for. The ledger never evicts while the acquisition lives — dropping an id would
make a retry look new, which for a mutation is the difference between doing
something once and twice — so a full ledger refuses and fails closed.

Content a runner offers is staged, and staging is not publication. It is
digest-checked, bounded per piece and in aggregate, stored detached, keyed by
the acquisition that offered it, and visible to no retained read. Adopting it
is a later checkpoint's transaction. Both private tables live in SQLite because
an evicted object remembers nothing and the attachment is 16 KiB of identity,
not somewhere to grow a ledger; a replacement acquisition discards its
predecessor's scratch before it accepts, and touches no retained history doing
it.

The rules a root is held to now live in one place instead of two. Both hosts
retain the same roots and must name them identically, so the manifest format,
its canonical encoding and the stored-row parsers moved beside the schema they
belong to, and the Deno host reads through the same implementation it always
described. The digest is arithmetic in the language: a shared module cannot
import a host's crypto, and the one place this is needed most is inside a
synchronous transaction, where there is nothing to await into.

`commit` and `settle` parse strictly and refuse. Applying them is D3 and D4,
and a placeholder that reported success would be the one answer a runner cannot
recover from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
A root the owner returns is where the run stands. The runner materializes it,
works inside it, and proposes against it. So a root whose content cannot all be
found is not a frontier that happens to be incomplete — it is not a frontier,
and saying otherwise is the one answer that cannot be taken back.

The reads checked the root manifest and the manifests its entries named, and
stopped. Everything past that — whether those manifests were still there, what
their bytes were, whether the blobs they named existed, whether the root
retained references to exactly those blobs — was checked only when a runner
later asked for that particular piece. A root missing half its content
therefore answered `frontier` and `root` performed, and failed afterwards, one
piece at a time, once the run had already been told where it stood.

Now the graph is walked before either answers. The manifests the entries name
must be exactly the manifests the root retains; each must exist, be bounded,
decode canonically, hash to its identity, and agree with its recorded size and
with every file naming it. The blobs those manifests name must be exactly the
blobs the root retains; each must exist, be bounded, hash to its identity, and
agree with its recorded size and with every chunk naming it. Both directions of
each reference set are checked: a missing row is content nothing is keeping
alive, an extra row is content no manifest accounts for, and neither describes
a root anybody should start from.

The bytes are read and dropped. What survives the walk is the proof, and a
content request still re-reads the single piece it sends — a validated root is
not permission to answer with all of it at once.

The schema already refuses to let content vanish from under a root that
references it, so the states worth reproducing are the ones that restriction
cannot prevent: a reference collected together with its content, bytes that no
longer hash to the identity they are stored under, a recorded size that
disagrees with what it describes, and a reference to content no manifest names.
Each of those now refuses before a root is returned, and says only that storage
is damaged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The owner holds the run and can run nothing. Git, an Agent and an evidence
command need real files, so a root has to become a directory somewhere the
runner owns, and whatever happens there has to come back as a root again.

The equality that matters is that those two operations compose to nothing: a
materialization nobody touched must capture to the exact root it came from. If
it did not, every Workspace operation that changed nothing would still propose
a new root, and the owner could not tell a real change from an artefact of how
the runner unpacked the tree. Directories, an empty file, a symbolic link, a
hardlink group, modes and modification times all have to survive for that to be
true, which is why the evidence is a real temporary filesystem rather than a
map that would only prove it kept what it was given.

Ordering, hardlink numbering, manifest encoding and chunk size now live beside
the format instead of inside whoever happened to walk the tree. The local host
walks SQLite rows and the runner walks a directory; those walks cannot be
shared and their meaning must not diverge, because a root identity is a digest
of the encoding and two encodings would be two names for one Workspace.

The materialization knows no runtime and no path. Native operations arrive
injected, adapted from the runtime's asynchronous primitives with `until`
where `@effectionx/fs` has no equivalent; the logical root stays `/`, and the
temporary directory the invocation happens to use reaches no manifest, event,
proposal or error. A run that recorded where it was unpacked could not be
resumed anywhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Materialization indexed a hardlink group's first path by the content digest of
its bytes. Two groups holding identical bytes legally share one DOFS manifest
and are still two inodes, so the second group linked to the first and recapture
saw one group of four names where the root said two of two. The Workspace that
came back was a different Workspace, arriving under the identity of the one
that was asked for. Group membership comes from the root's own `hardlink`
value now, which is the only thing that ever said what a group was.

Modes and times were left to whatever creation happened to produce. A creation
mode is narrowed by the process umask, so a root retaining a group-writable
file materialized without that bit; a symbolic link's own mode and time were
never restored at all, and `utimes` could not have done it without following
the link — which may point outside the tree deliberately. Permissions are now
set explicitly after creation, deepest-first so a mode that forbids writing is
not applied while children are still arriving, and a link's own metadata is set
through the operations that do not follow it, where the platform has them.

Where a platform has none, materialization refuses. Every entry is read back
and compared with what the root declared before anything executes against the
tree: a host that cannot represent a legal retained mode or time says so, once,
before native work begins. Quietly normalizing it would hand the run a
Workspace whose durable identity differs from the history it accepted, and the
run would have no way to notice.

The round trip now carries two hardlink groups with identical bytes, two
independent files with identical bytes that must stay independent, modes the
test's own umask would narrow, and a symbolic link with an old time of its own.
Reverting the grouping key alone fails it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The shared coordination boundary is scanned for host vocabulary, and the root
manifest module had been failing that scan since D2: its refusals named DOFS in
string literals, which the scan reads as code because they are. D2's evidence
list did not include the scan, so nothing said so until this checkpoint's wider
verification ran it.

The names were a symptom of the wrong seam. A Workspace root names a file's
content by one identity and says nothing about how those bytes are kept; how
they are kept is a separate format, and it was sitting inside the module that
describes roots. It now has its own, which decides only whether a sequence of
bytes is a canonically encoded manifest and produces the bytes one ought to be.

Its refusals describe the format rather than the store implementing it. That is
not a rename to satisfy a scan: every host keeps content this way, the vendored
layer is one implementation, and a neutral module naming that implementation
would be the Workspace surface learning where it happened to be stored. No
message this reworded is asserted anywhere, so nothing observable moved.

The obvious home — beside the schema that declares the content tables — is
closed. A neutral module importing from there names the storage engine in its
own import specifier, which is the same crossing by a different route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`commit` carried a proposed root identity and nothing that could justify it. An
identity with no manifest and no content closure is a name, and an owner
adopting one would be taking the runner's word for what a root contains. The
command now carries the whole thing — what the runner started from, what it
proposes, the canonical manifest that identity is the digest of, the exact
content that manifest closes over, the retained mappings the same operation
produced, and the filtered events to append — and the owner recomputes all of
it before anything is written.

The frontier is re-read inside the transaction rather than before it, and
compared with what the runner said it started from, root and terminal event
both, `null` included exactly. A frontier read outside the transaction is a
frontier that can move before the write.

The inventory has to be exactly the closure of the proposed manifest: every
manifest its file entries name, every blob those manifests name, once each, and
nothing else. A missing piece is a root that cannot be materialized; an extra
one is content the root does not account for. Each piece resolves from content
already authoritative or from bytes this exact acquisition staged — staging
supplies bytes and grants nothing, so another acquisition's scratch is
unreachable and a digest already retained under different bytes is a
disagreement rather than an overwrite.

Everything lands together or not at all: content, the immutable root and its
exact references, the mappings, the current pointer moved by compare-and-set
from the expected root, the journal rows, and the retry decision. Journal rows
name the root this commit selected — the proposed one when there is a
publication, the unchanged one when there is not — which is what makes history
readable against the Workspace it happened in. A journal-only transaction and
an empty one are both ordinary, and neither invents a Workspace change to look
uniform.

Retained mappings go through the parsers the local host holds its own rows to,
and creation identity is immutable: a second proposal naming the same
Repository must describe the same Repository. An Agent-session mapping carries
the canonical assertion and the derived key and nothing of the conversation;
the owner never contacts a provider.

The evidence runs on real workerd because none of it is provable otherwise. A
forced failure after every category has been written rolls all of them back,
the retry decision with them, leaving the id free. A lost response retried
across eviction publishes once. A moved root, a moved anchor, an identity that
is not its manifest's digest, an inventory missing or inventing a piece,
unstaged content, a mapping that would rewrite an established identity, and a
foreign socket each change nothing at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Continuing a fork asked the destination to take a request's word for what fork
it was. The command named no source and no checkpoint, and the lineage row was
only checked for existing — so a request naming somewhere else entirely could
take up this destination as though it were the fork it asked for. It now names
the source and the checkpoint it claims, and the destination compares them with
what it retains.

The two head records are held to their exact associations rather than to
membership. Proving that a head names some root this store happens to hold
proves nothing: every retained root satisfies it. So the roots those two rows
were committed against are retained with the lineage when the fork is made, and
a head reassociated to another perfectly valid root is refused as the different
fork it is.

A decision that granted an execution is only re-observable once that execution
is this acquisition's. A continuation records which execution it began — and
records nothing when it began nothing, because a conflict and a refusal are
answers too — so a replacement acquisition either adopts the exact open
execution atomically or is told the run has moved past it. What it can no
longer be handed is a database paired with an execution it may not settle.

An exact command that was resent is now heard out. Every outcome is classified
before anything else is sent: a second silence keeps the claim and sends
nothing further, a refusal or conflict ends the question, and one closed answer
— the destination holds nothing and was offered nothing — is the only one that
sends the same logical fork back to its source to stage again. A lost
cancellation is kept the same way, so the cancellation that committed is the
one the next attempt hears about.

A call that is interrupted lets go on purpose. Every transition registers how it
ends before it can be interrupted, and where it was interrupted decides what
that means: nothing sent, so the guard lifts and the acquisition is free; a
command sent and unanswered, so the acquisition is retired instead — because the
one thing that must not follow an unknown decision is a fresh mutation racing it.
A fork asks its destination several things under one call: whether it already
holds this fork, an offer for every member of the snapshot, and the commit.
All of them went out under the call's single identity, and an owner keys a
retained decision by the identity it was asked under. So the commit met the
continuation's own fingerprint and was refused as a repeat of something it is
not, and a fork that had staged everything could never be committed at all.

Each question now carries its own name, derived from the call and from which
question it is — a part's section and position included — so two of them never
collide, and a retry still spells every one of them exactly the way the first
attempt did. The staged parts stop being minted fresh per attempt for the same
reason: a part the destination already holds has to be recognizable as that
part rather than arriving as a new one.

An acquisition retired while an outcome is unknown now ends its connection.
Retiring the lock and leaving the socket open left the owner holding a live
executor nobody was allowed to use: this runner would send nothing more on it,
and no one else could take the run while it was held, so the run was
unreachable by anybody until the enclosing scope happened to end. The
connection is the acquisition, so it goes when the acquisition does — through
the same teardown that scope exit reaches, once, with whoever was still
waiting told the connection closed.
…its question

A destination that answers `needs-transfer` has answered. It says the run is
not there and this connection offered it nothing, and it says so under the
exact name it was asked by. The fork then staged the whole snapshot again and
sent its final command under that same answered name, which the client refuses
before it reaches an owner, because a settled correlation may not carry a
second question. On the one recovery path this answer exists for, no fork could
commit at all.

So what follows a `needs-transfer` is a second transfer, and it carries an
identity of its own: a new command name for the commit, new names for every
offered part, and a new execution to begin, because the answered decision began
nothing to inherit. An invocation now says which of the two phases it is in, so
a retry resumes the phase it is in rather than reaching back for a name that has
been answered — and it no longer asks whether the destination already holds this
fork, having just been told it holds nothing.

A cancelled call keeps the question it was asking. A transition registers what
to retain before each command goes out and clears it when the answer comes
back, so an interruption with an owner decision outstanding retains that exact
identity — and the exact bytes, when a fork's final command is what was
outstanding — before it retires the acquisition and closes the connection. What
follows is what follows a lost answer: a replacement acquisition asks the same
question under the same name and adopts the decision, or is told the run has
moved past it. Nothing is retained for an interruption that mutated nothing:
reading a source and offering scratch parts stay free of ambiguity, and the
acquisition they were interrupted on stays usable.

That distinction is now the grip's, rather than a single answered-once flag: one
transition sends several commands, and a preliminary answer in the middle of a
fork is not the end of the fork. The flag made it one, so a cancellation during
the source read or the staging that followed left the acquisition guarded
forever and a cancellation at the final commit retained nothing at all.

A retained decision is re-observable on the terms the decision itself sets. An
answer that carries a begun value grants execution authority, and that is
returned only when the exact execution becomes this acquisition's — a ledger row
that no longer names one is a disagreement, not authority. An answer that began
nothing adopts nothing and invents no hold to go with it.
A run that is waiting has no executor, and handing it a value must not start
one. So delivery is a third plane on the owner beside the executor socket and
the read plane: it accepts no socket, mints no acquisition, moves no lifecycle,
and can be answered while another executor is live. What separates it from the
read plane is that it writes exactly one row — the value, correlated to the wait
it answers — and nothing else. No journal event, no execution, no status, no
root, no mapping.

The value is judged before it is retained, and it is judged on the runner. A
wait retained a response schema, and judging against it means compiling that
schema with the secret scanner beside it; both are the document runtime's, and a
run's owner is not a document runtime. So the owner says what the run is waiting
at, the runner judges the offered value against exactly that, and the owner
writes only after reading the same facts again inside the transaction that
writes. The fingerprint is what makes that safe: the runner recomputes it from
the description it was given and refuses an owner that names another, and the
owner refuses a retention whose fingerprint is not what its retained request
spells now. A value judged against one request can never be retained against a
different one.

Spending it is one commit. The execution that reaches the wait — the one
standing at it, which is not something a row can decide — reads what the owner
retained through its own acquisition, publishes the answer into the transaction
it is already inside, and enlists the consumption beside it. The owner receives
one proposal carrying both: it reads the row it retained, requires it pending
and delivered against this request, requires exactly one appended event to be
that wait's answer carrying exactly the value it holds, and then spends the row
and appends the events. A runner cannot publish one value and spend the row for
another, cannot spend without publishing, and cannot publish without spending;
any disagreement refuses the whole commit, which leaves the answer pending and
appends nothing.

The value reaches the document only after that commit returns. An offered
publication proves nothing — the transaction it was offered inside may not have
committed — so what ends the wait is the owner having committed, and a later
execution replays the recorded event rather than reading delivery state again.

Three pieces moved to make this possible without a host reaching into another.
The wait's two effect names and its identity now live in a leaf module, and the
fingerprint is composed from the canonicalization and the SHA-256 this package
already carries rather than from `node:crypto`, so a Durable Object can
recognize a request and an answer without loading a document runtime. And the
check for whether an execution is standing at its own wait is shared, because
every host that ends a wait asks exactly that question and none of them may
answer it differently.
…once

Three places took a caller's word for something only the owner can decide.

The delivery plane accepted a value beside identities the caller supplied, and
retained it without ever asking whether the value satisfied the schema the wait
published. A caller with delivery admission could read the wait, then offer any
canonical JSON against it and leave durable state holding an answer the wait
does not admit — one a later execution would publish and hand to authored
control flow. The lower operation that made that possible is gone. There is one
mutating delivery operation now, it carries the value and the gate decision and
nothing else, and the owner resolves the wait, judges the value against the
schema that wait retained, and applies the selected gate, all inside the
transaction that writes.

Judging it there needed a judgment that runs there. The compiler the document
path uses builds validators with `new Function`, which a Worker refuses, so the
judgment is written in the language itself and both sides run that exact module:
the runner before it offers anything, and the owner before it writes. It is
closed rather than lenient — every keyword it applies is listed, and a schema
using anything else is refused rather than judged with that constraint quietly
skipped, so a value it accepts is one every constraint its wait stated was
actually checked against. A parity table holds it to the compiler's own verdicts
across every schema it admits. The credential gate is the same shape and is
honestly narrower: the full scanner needs a Node runtime the owner does not
have, so the runner still runs it and the owner applies a floor over the same
two framings. What the caller may no longer do is omit the choice.

Reading the value took only a socket. Any admitted connection that knew a
suspension identifier was handed the retained answer — including one whose
execution had already settled, and one that had begun nothing — which discloses
document input and lets a publication be built before anything owns the right to
publish it. A claim now names the journal event the request was published as,
and the owner requires the asking acquisition to hold an execution the run has
not moved past. A settled, replaced or never-begun acquisition is told nothing.

Publishing an answer was not tied to spending one. A commit could append a
`suspension_answer` with no consumption at all, or append a second answer event
beside the one its consumption authorized, and the owner would write both. The
two are one act, so they are checked as one: an answer event with no consumption
is refused, a consumption with anything other than exactly one answer event is
refused, and a consumption from an acquisition holding no open execution is
refused. Every check happens inside the owner's transaction before any durable
mutation, so a refusal takes the whole proposal with it.
…hema check is the compiler

The gate a delivered answer crosses is the one durable journal persistence is
written through. The owner was applying a summary of it instead — a pattern
list, documented as a floor — and the difference was not theoretical: a
credential-named field carrying an opaque value passes the summary and is
refused by the configured scanner, so an authenticated caller reaching the
owner directly could retain a value the journal itself would not accept.

The owner now runs that scanner. It is the recommended preset and this
repository's own credential rule, over the same two framings the local host
scans, and it needed nothing but a resolution path that does not drag a
terminal renderer into a Worker — so `@executablemd/core/secrets` publishes the
gate the way `@executablemd/core/canonicalize` publishes the ordering. It runs
before the transaction, because it is asynchronous and a Durable Object
transaction cannot wait, and what it read is pinned by a request identity the
transaction requires to still be the retained one before it writes. A scanner
that cannot run at all is a refusal. The explicit opt-out is still the only way
past it, and the pattern list is gone rather than kept beside the real thing.

The schema check is a different story and this commit tells it plainly rather
than papering over it. The settled contract names the local host's compiler,
and that compiler builds validators with `new Function`: it performs four code
constructions to compile one schema, where the scanner performs none. A
deployed Worker does not generate code from strings, and the reason it works
under test is that the test pool proxies `globalThis.Function` into an unsafe
evaluation binding. So the owner cannot be that compiler without a capability
its deployment would have to grant, and this commit does not pretend otherwise:
every claim of equivalence is removed from the module, its tests and its
comments, and the disagreement is asserted instead — `{ multipleOf: 0.1 }`
accepts `0.3` at the owner and refuses it at the compiler. What the owner runs
is named for what it is, an additional check that refuses early and refuses
more, and the gap it leaves is reported rather than described as closed.

One gate is now proved to be one gate: a regression runs the scanner the
journal is written through and the gate the owner applies over the same content
and requires the same verdict, so a rule or configuration change cannot split
them again without saying so.
…t is judged

A response schema was decided by two different things. The document runtime
compiled it; a run's owner could not, because a Worker refuses code generation
during a request and a schema learned from a retained wait cannot be compiled
before the request that carries it. So the owner ran something else, and the two
disagreed — `{ multipleOf: 0.1 }` refused `0.3` on one side and accepted it on
the other. A value's verdict depended on where it happened to be judged.

Now there is one judgment and it generates no code, so the boundary that writes
a value is the boundary that judges it: `<Elicit>` and `xmd prompt` judging a
provider's answer, local delivery and artifact delivery judging a delivered one,
the remote client before it offers anything, and the owner inside the
transaction that retains it. The settled draft-07 verdict is the same at all of
them, `0.3` included.

Preparation still refuses everything it refused, and one thing it had stopped
refusing. `$async`, `__proto__` as a declared name, a reference that leaves the
supplied schema, and a keyword draft-07 does not define are all decided in one
walk, before a question is rendered or a provider is contacted — and the schema
is then validated against the draft-07 meta-schema, which is what the compiler
did and what the owner's own subset never could. `format` annotates and
constrains nothing, as it did.

The public schema surface comes back with it. The subset the owner had been
using refused `if`/`then`/`else`, `patternProperties`, `contains` and
`dependencies` as unjudgeable; those are draft-07 keywords a document may write,
and they are judged again. Props, returns, `<Parse>` and `<SafeParse>` keep the
compiler they had: they never run inside an owner, and nothing about them
changes.

Two assertions moved with the implementation, because failures now read in its
words rather than the previous compiler's. Neither was weakened: both still
require the issue to name the member that failed and what was expected there.
And `enum` and `const` failures are described rather than quoted, because the
old text listed the schema's own allowed values and an issue travels further
than the document that declared them.
…saying the value

Four things the shared judgment got wrong.

It deleted every key named `format`, everywhere, treating each object it met as
a schema. A `format` is an annotation and belongs nowhere in a decision, but a
literal under a `const` is data a document wants matched exactly, and a property
a document declares as `format` is a name rather than a keyword. So the exact
literal stopped matching, and the declared property was deleted and then refused
as one the schema never declared. The transform now walks the same positions the
schema walker already knew about: keywords are keywords, data is carried across
by value, declared names stay names, and the authored declaration is never
touched — a provider still sees the `format` that describes what to answer.

A reference whose target did not exist passed preparation and failed later, and
only if some value happened to reach that branch. Everything that makes a schema
unusable has to fail before a question is rendered and before a provider is
contacted, so every reference is now resolved when the schema is prepared, in
the same lookup the validator will use, at every schema position — including
branches nothing samples.

A value was judged by what the language answered for it rather than by what it
held: `{}` satisfied a schema requiring `toString`. What the validator is given
is now a private projection whose objects hold only their own names, so an
inherited name is absent, and absence is an ordinary reported failure rather
than something thrown from inside a dependency. What the caller keeps, binds,
retains and journals is untouched.

And a failure said too much and too little at once. Independent rules were being
dropped — a `minProperties` failure disappeared because a member failed too —
where only the wrapper of another failure should go. Locations came back as URI
fragments, so a member named `🐲` was reported as `/%F0%9F%90%B2` instead of a
JSON pointer. Messages were the dependency's, and quoted the rejected value and
the schema's own thresholds into something that is printed, bound into the
evaluation environment and carried across a journal. Each is fixed: independent
failures survive, pointers are raw, and every message is this repository's own,
naming the rule and never the value — except `required`, which names the member
that is missing, because the location cannot.

The deduplication key had a NUL byte in it, which is why Git showed the file as
binary. It is a serialized tuple now.
… else

A completed run asked to run again answers from the terminal its journal
records: it imports nothing, performs nothing and appends nothing. But
canonical execution still has to be given a root document, and locally that
value came out of Git because a checkout was there. A run whose durable owner
is somewhere else has no checkout at all, and fetching one for a replay that
imports nothing is live retrieval performed for a document nobody reads.

So it comes from the run. `retainedReplay()` reads the root the run recorded —
the retained `__root__` import, or the binding core writes into a terminal
created before any import — and holds its path to the definition the run
record retains, which is the half of the comparison that never came from the
journal. The bundle is the same judgment made narrower: a completed replay is
handed no component source and no authority to resolve a name, only the
admission that holds every recorded import to the name, canonical path and
object id the definition declares. A declared member the history never
imported is neither read nor fetched.

The shared CLI decides that from the frontier its owner answers with, right
after admission, and therefore ahead of every live-only construction below it:
the retained definition, this host's own adapter, the suspension controller,
the answer provider, the `<Evaluate>` declaration and `host.attach()`. A
lifecycle row claiming completion over a history that records no result, a
history continuing past the result it records, a missing or malformed root
import, and a retained root naming another document each refuse closed.
… it says

Three corrections to completed replay, each about believing a record instead
of establishing what it claims.

**Recovery decides, not the status a run still carries.** An executor may
commit its document result and disappear before settling, and the run then
reads `running` while its journal already holds the outcome. The definition
was fetched ahead of the lifecycle transaction that recognizes that, so a
bundled run in the supported crash window went to a checkout an ephemeral
runner may not have — on the one path required to reach only its durable
owner. The definition is now fetched after admission or not at all: the begin
transaction reconciles the stale execution, publishes the terminal its retained
root result implies, and answers `replay`, and every live-only construction
below it is downstream of that answer. A live or partial run still
reconstructs and authenticates its whole bundle before any document effect,
and a reconstruction it cannot perform leaves the journal and the Workspace
root exactly as they were, with the envelope it began closed by the settled
recovery at the next acquisition.

**The lifecycle row and the recorded result have to be the same outcome.** A
row claiming `completed` over a root `Close` that records a failure was
accepted, so canonical core could reuse a terminal the run's own state
contradicts. `retainedReplay()` now requires `rootOutcome()` — the settled
mapping recovery and settlement already publish through — to agree with the
retained status and stop reason, and refuses the pairings the lifecycle cannot
produce before terminal reuse.

**A retained component source is held to the object id, not merely beside
it.** The replay admission required the history to repeat the declared hash,
which establishes nothing about the bytes recorded next to it: altered source
was admitted under the original identity. It now names those bytes the way Git
names a blob — the kind, the encoded byte length, a NUL, the content — under
the definition's own object format, and requires that name to be the object id
the definition declares.
A `start` naming a run that has already ended is the same terminal reuse a
`resume` of one is, and the begin transaction says so: it holds the supplied
definition, base, props and bundle to the immutable record, and answers
`replay`. The branch below it asked a second question — whether a candidate
definition had been supplied — and a compatible `start --id` always has one.

So the shipped compatible-reuse surface took the live path. It skipped the
lifecycle/root agreement entirely, so a row saying `completed` over a root that
raised was handed to canonical core, which returned the retained failure and
left the invocation offering `failed` for a run the owner records as
completed — two authoritative outcomes for one run. It also constructed the
suspension controller, the answer provider and the live bundle execution view
for an execution canonical core returns from before expansion.

`replay` is now the whole question. What a caller established describes the
request; what the run retained describes the result.

The agreement judgment moves to `lifecycle/policy.ts`, beside the mappings it
is derived from, and now accounts for both authorities that publish a terminal
for one root Close. Recovery reads the coroutine's own settlement, so a
document that *returned* a failure recovers as `completed`; the executor that
watched it settles the same Close as `failed`, naming the row it failed at.
Both are states this system produces. What no settled path produces — a failed
row over a document that succeeded, a terminal row over a root that raised or
was cancelled and says otherwise, a reason naming something the run does not
hold — still refuses before terminal reuse.
… its own envelope

Two corrections, both about a run being described twice.

**The root Close says what the document did, not merely that it returned.** A
durable Close has two layers: the coroutine's own settlement, and — when it
settled by returning — the document result it returned. Reading only the outer
layer called every finished document a completed one, so recovery published
`completed` for a run whose document failed while an uninterrupted settlement
published `failed`. The correction had then written that disagreement down as
two authorities allowed to differ, and admitted whatever reason the second one
carried: an unrelated successful row, an invented host code, or a terminal with
no readable document result at all.

`rootOutcome()` now reads the whole thing. A returned result decides
`completed` or `failed`; an outer error or cancellation keeps its own meaning;
and a returned value that is not a document result is `damaged` — refused
before canonical core is handed it, rather than becoming an outcome nobody can
account for. The failed reason comes from `retainedFailureReason()`, the one
rule the live runner already used, which settlement, stale recovery and
retained-history admission now all reach. A stale run whose document failed
therefore recovers to exactly the `failed` status and reason an uninterrupted
settlement publishes; `resume` then applies the settled failed-run refusal and
a compatible `start --id` replays that same failure.

**A replay closes its own envelope and nothing else.** The remote owner already
skipped publication for a terminal run; the local adapter published
unconditionally, so even a coherent replay rewrote the run's status, stop reason
and `updated_at`. It now applies the same rule the owner does, decided from the
retained state inside the same transaction and exact-lock checks.
…ion over it

A retained root Close proves the document ended. When its form is not one
canonical core writes, this build has no authority to say what it ended as —
and the previous correction, having noticed that, then threw the fact away.
`closingOutcome()` encoded damage as "interrupted, publishes nothing", both
adapters read that as the stored `running` status, and their begins went on to
insert another execution, publish `running` and hand the document back to a live
invocation. The remote cancel lost it the same way and published `cancelled`
over history it could not read, while the local one had grown a check of its
own — two providers deciding one lifecycle differently.

Damage is now a decision of its own. `Closing` carries it, both reconciliations
return it without writing anything at all, and both begins and both cancels
refuse with one shared sentence that repeats nothing the history held. The
settled behaviour for the stale envelope is the same on both: it is not closed
either, because what the execution became is exactly as unreadable as what the
run became.

The terminal judgment also got stricter in the two places it was loose. A
`root_binding` is not decoration a failure may carry: core writes one in exactly
one situation, and the whole pre-root form — the empty output, the segment that
repeats the failure's own message, the closed path/source/target — is what makes
an import-free history attributable to one document. Anything else wearing a
binding is a terminal core did not write. And the terminal has to be the
frontier: `terminalFrontier()` requires exactly one root Close, last, and both
stale recovery and replay admission ask it, so recovery can no longer publish an
outcome from the first of two results that replay would refuse whole.
… around it

Two ways an unreadable or impossible retained history still got past the
lifecycle.

**A terminal row does not vouch for its own journal.** `closingOutcome()`
answered the stored status before it looked at the damage, so a run already
recorded as `completed` or `failed` came back from both reconciliations with no
damaged marker at all, and both begins went on to insert a replay envelope and
return `replay: true`. Only afterwards did admission refuse the malformed
history — by which time durable execution state had already moved for history
that authorizes no execution. Damage is now the first question `closingOutcome()`
asks, before the stored status is consulted, so a damaged terminal refuses on
both providers for every row it can sit under.

**A terminal is only canonical in the history that surrounds it.** A
`root_binding` is written by a run that failed before importing anything, and an
ordinary document result by one that failed or finished after importing — so the
terminal's own members cannot tell a genuine pre-root failure from an ordinary
one wearing a binding, and could not tell an ordinary result recorded with no
import from a real one. `rootOutcome()` now correlates the two: a bound failure
is canonical only with no root import, and an ordinary result only with exactly
one root import this coroutine recorded and settled. `rootImports()` is the one
reading of that frontier, and retained replay builds its root from the same
answer rather than scanning for one of its own.

Both rules together say what a workflow run's journal is: a document
execution's. The restart harness wrote neither half — no root import, and a
plain string where the document result goes — and passed only because a settled
`completed` row used to excuse the journal beneath it. It now records the run's
own import and returns the result canonical execution returns, both durably, so
a second process restores them rather than recording them again.
`rootImports()` filtered retained imports by the root coroutine, so a second
event naming `import_component/__root__` recorded under a child was ignored
rather than refused, and a settled root import was treated as sufficient
without reading the selection it carried. Canonical `admitRootHistory()`
gathers every event naming the root import first, then requires exactly one in
total owned by the root coroutine, and parses it — so lifecycle recovery could
publish `completed` or `failed` from a history canonical execution rejects,
and retained replay accepted one it rejects too.

Uniqueness is now asked of the name, not of the ownership: one shared judgment
gathers every event that names the root import, requires exactly one total,
requires the root to own it, and parses it into the selection replay rebuilds
the document from. Retained replay reads that judgment instead of deciding
readability again afterwards, so neither boundary can accept a history the
other refuses.
The lifecycle's own reader checked field counts and primitive types, so a
retained root import naming a target its document does not offer, spelled a
way canonical encoding never spells it, or carrying a "failure" reduced to a
selector was read as a complete selection. Canonical `readRootSelection()`
does more than recognize a shape: it parses the retained markdown, resolves an
exact target against that outline and requires the recorded target back, and
re-derives a recorded failure through `findTarget()` requiring the kind, the
matches and the whole catalog to agree. History the executor refuses could
therefore publish an outcome, insert a replay envelope, and hand execution a
selector taken from a forged record.

So there is one parser again. It moves out of `execute.ts` into its own module
with the reading primitives it needs, canonical execution admits partial
histories through it exactly as before, and the lifecycle reaches it through
`@executablemd/core/host` — the same reasoning that already carries the Prompt
record across that boundary. What comes back is the parser's own copy of the
record, so the document a replay is built from is never the object the journal
still holds.

A selection outcome also has to be able to reach the terminal beside it. A
recorded selection failure is raised out of the root import, so the document
never ran: a successful result over one is two histories, and the run is
damaged rather than completed.

Two fixtures recorded selections canonical execution cannot produce — an exact
target in a document offering none, and a failure carrying a catalog that
document does not have — and now record what the same selector really decides.
The documents still called the internal remote lifecycle, transport, fork,
inspection and completed replay unbuilt, and still said the neutral transition
types were waiting to be exported. All of it is implemented behind the same
four-method host, so the inventories now say what is built and, separately,
that nothing yet assembles a configured public client, selector, endpoint,
release-identity or OIDC supply — which is the whole of what remains before a
caller could choose that host.

A finished run also has a contract now, in one place. `specs/workflow-spec.md`
§9.9 states what a retained terminal is and what may be concluded from it: one
final root `Close`, two layers that each decide something, exactly one root
import owned by the root coroutine and parsed by canonical execution's own
parser against the document it recorded, damage outranking the stored row, and
a coherent replay that changes nothing but the execution envelope it opened.
The architecture and the Workspace specification reach that statement rather
than restating it, and Tier WRH is no longer described as scenarios without
tests: WRH1-WRH16 name the committed suites that prove them, and WRH17-WRH22
stay with the machine-wait work they belong to.

Documentation only. No production module, test, dependency, lockfile,
generated artifact, export or public API changes here.
… them

The owner had three planes and no way in. It has one now: a path names the
plane, one run id selects the object arithmetically, and a gateway forwards on
that id alone — it parses no command, verifies no token and commits nothing,
because an owner that trusted a gateway about any of those would have moved
its own admission outside itself. The executor plane performs a real upgrade,
checks the release before the token and the token before the run, and takes
the acquisition last.

A runner reaches all three through one configured client: an already-selected
run id, one credential-free endpoint parsed before anything is sent, the exact
release identity, a token minted for the immediate request, and the HTTP and
WebSocket I/O its host performs. It is bound to that one run and refuses
another before a token exists. Trusted code assembles it into the same four
methods the local entrypoint installs — nothing selects it, reads a flag or an
environment variable for it, or deploys it.

Composing that exposed a real gap in the runner's Workspace coordinator. Its
routed journal was not the handle's journal, so a document run through the CLI
would have published its effects beside the transaction that made them and the
provenance check would have refused every one. The remote handle now routes
its own journal and records the provenance taken over it, the way the local
handle always has, and the coordinator opens the run from the handle its own
lifecycle produced rather than a second one.

That makes the attachment exact. A handle remembers the link it was opened
from; attaching compares that link, by identity, to the ones this runner's own
acquisitions produced. Two clients on two owners can hold handles that agree
about run id, root and anchor, so nothing a handle says about itself could
settle which run it is — where it came from can.

Two suites found real defects while proving this: a token was minted for a run
the client is not bound to, and admission refusals were read through the
command vocabulary, so a release or token category raised a malformed answer
instead of an unavailability. Both are fixed here.
The fourth method installed the Workspace coordinator and stopped. So an
authored `<File>` on a remote run resolved through whatever Files provider was
in scope — a runner has the ambient host one installed by its entrypoint — and
a document wrote to the runner's own filesystem instead of the run's Workspace.
The new regression reproduces exactly that against the previous revision.

An attachment now installs what a live or partial document reaches: the
logical Workspace working directory, the document filesystem, the Repository,
Worktree and `<Dir>` composition, transactional Git, the composition
components, the pull-request and Issue middleware, elicitation, and whatever
Agent profile its host configured. The set is inseparable — the Files provider
alone would resolve authored paths against the surrounding host's working
directory — and it is one set: `withDocumentCapabilities()` is what both hosts
install, so there is one account of what `<File>`, `<Repository>` and the Git
components mean.

What a host contributes to those rules is two things. The effect a mutation
becomes, and the savepoint that undoes a part of one mutation which could not
be finished. Locally that is the lease it validated and a real transaction
savepoint; on a runner it is the exact remote run its own acquisition opened,
and an attempt restored from the accepted root — correct because one effect
performs one mutation.

Which of the two answers is decided by the exact storage handle. An attachment
registers its binding under the handle it was given and removes it when the
attachment ends, so a handle another client opened finds that client's binding
or none, a handle nobody attached is refused rather than performed, and no
context value is what says a filesystem, journal or publication target belongs
to this run.
…al store

Two capabilities were assembled remotely in name only. A Repository or
Worktree committed its identity and its content through the run's own binding
and then reattached its checkout through `transactWorkspaceRoots`, whose
implementation is the local storage provider's private workspace — which a
runner never installs. An Agent profile read and committed its session mapping
through the same door. So a remote run could publish a checkout it could not
then attach, and establish a conversation whose mapping nothing retained.

A host now answers four questions for one run rather than two: the effect a
mutation becomes, the savepoint that undoes an unfinishable part of one, the
read an ephemeral attachment needs, and the transaction a session mapping is
retained by. Locally all four come from the lease this handle already
validated. Remotely the read takes one coherent owner snapshot, materializes
the selected root into a directory the invocation owns, hands the attachment
only the retained metadata and that filesystem, and closes before native Git
runs — no owner read and no transaction held across a subprocess. The mapping
transaction stages from the admitted state on the runner, where the provider
is, and submits bounded deltas to one owner transaction that revalidates each
subject and commits them all or refuses them together. No provider call
happens inside it, a body that failed or was cancelled sends nothing, and a
conflicting assertion never replaces what is retained.

A handle binds its host when it is opened, not only when a document is
attached: reading a retained mapping needs no Workspace, and a caller that
opened storage alone has always been able to. An attachment binds a narrower
one over the top and gives the outer one back when it ends.

The public configuration is the published one. `capabilities` on the runner
and on the configured host accept what a *host* owns — the issue tracker, the
readable pull requests, the credential helper, the Agent profile — and no
substituted repository host, Git-host transport or invocation observer, each of
which is a seam a credential this run acquires would become visible through.
One projection does that for both, member by member, and a type-level
regression fails if the seam comes back.
… run

Both capabilities were argued from below rather than executed. The Repository
evidence preloaded a record and called the attachment read directly; the Agent
evidence called the session transaction directly, against an owner whose later
snapshots never included the mapping it had just accepted. Either one could
pass while the composed path failed, which is exactly what happened twice
before.

So the scripted owner now adopts what it performs. A commit that is performed
moves the pointer to the root it published, keeps the bytes it was staged, and
merges the mappings it validated — so a later coherent snapshot answers with
the run as it became, and a reattachment or a continuation reads what was
actually retained.

On that owner, one authored document through the configured public host clones
a real repository on the runner, retains the checkout and its immutable
identity in one owner transaction, reattaches it, and switches its branch in a
second transaction that starts from the root the first one published. The
remote is then deleted and the same document runs again: it reconstructs the
checkout from the committed frontier, retains no second Repository, and reads
the branch's own file back. The retained record names the checkout by its
logical Workspace path and the remote by a fingerprint — no locator, no host
path, no transport detail.

The Agent path is driven through `capabilities.agent` on the configured host,
with the shipped policy and the shipped transaction and only the provider
standing in. It establishes, commits and then prompts, in that order; a later
attachment reattaches the exact retained assertion and creates nothing; a
failure before the commit retains no mapping; and a different conversation
under the same identity is refused before any replacement.

The contract inventory says which providers each capability has now, without
implying that anything selects the remote one.
The two D5 regressions asserted structure they did not reach.

The scripted owner discarded every event it accepted, so a mapping and a
root sat beside an empty journal — a state no owner can produce from a
successful effect. It now retains the ordered journal: each entry with
the id the owner minted for it and the Workspace root the transaction
that appended it selected, appended in the same step that moves the root
and merges the mappings. `frontier` and the mappings snapshot name the
terminal event, and a `journal` read answers one anchored page at a time
from exactly where the client says it is.

So the Repository regression can be about continuation. The first
execution clones, retains the creation whole, and is cancelled with the
Git mutation's proposal still in flight: the owner decided the creation
and never decided the mutation, which is a prefix an owner can be
holding. The continuation reads that prefix back in anchored pages,
restores the recorded creation with no remote left to clone from,
rebuilds the checkout from the root the replayed record selected — the
live switch starts from that root and moves a checkout really on `main` —
and performs only the work the cancellation left undone.

The Agent regression installed its own policy. It now installs the
shipped `useWorkflowAgentProfile()` through the public configuration and
runs an authored Agent/Session/Prompt document, with only the agent
process and its own session store substituted. Sampled at the owner, the
order is the contract: the conversation exists, the owner then accepts
which one it is, and only then does anything prompt it. A restart
reattaches that exact provider-native session and prompts only the
unfinished turn; a completed document restores without a provider at
all; an establishment that fails and one cancelled in flight propose no
mapping; and a provider asserting a different conversation is refused
before a replacement session or a prompt.
Two assertions could not fail for the reason they were written.

The replay check looked only at commits that published. A Repository
effect that ran a second time while the owner already held a compatible
mapping would neither clone nor publish a root: it would return that
mapping and append its own event, in a commit that publishes nothing —
exactly the shape the check had filtered away first. It now reads every
commit the continuation made, and what the owner ends up holding: no
`workspace_repository` event and no Repository mapping after the prefix,
and one Repository effect row in the whole journal, the one already in
it.

The conflict case transplanted a mapping into a second owner that had
never committed it and held neither the root nor the journal of the
transaction that did. It now runs on one owner: the session is
established through the shipped profile, the execution is cancelled with
its first Prompt in flight, and the provider then comes back holding a
different conversation under the same placement. Nothing else changes.
The continuation refuses before a replacement session exists — the
provider is never started — before anything is prompted, and before any
commit at all, so the mapping, the root and the journal are the ones the
interrupted run left. The refusal arrives as the cause of the divergence
that stopping before a retained history is, so the assertions read the
whole cause chain rather than the outermost wrapper.

Beside them, the scripted owner answers a commit with the identities that
commit minted rather than with a slice computed from the journal's
length, and a mappings-only intent is now shown being answered with
none. Its paged journal is capped at the anchor it was asked for and
refuses an anchor it never minted or a cursor outside that prefix, which
the Repository case reads back directly once the journal has run past
the prefix its continuation consumed.
The anchored journal contract admits a cursor only strictly before the
anchor: `readJournalPage()` refuses `afterSequence >= anchorSequence`,
because a cursor at the end of a snapshot is a reader asking for a page
after its own snapshot ends.

The scripted owner refused only a cursor past the anchor and answered
one at the anchor with an empty successful page, and the direct read
that checks the anchored prefix recorded that answer as valid. So a
client that asked for another page after reaching its terminal event
could have passed this suite while the owner it will actually talk to
refused. Both cursors are now the same refusal, and the test expects it.

Nothing else moves: a null cursor still starts at the first entry, a
cursor strictly before the anchor still continues after it, the page is
still capped at the anchor however far the journal has run since, and an
anchor this owner never minted is still refused as one.
`local/no-redundant-test-scope` refuses a `scoped()` call that wraps a
whole `it()` body: the BDD adapter already runs each test in its own
scope, so such a wrapper opens a lifetime that begins and ends exactly
where the test's own does, and leaves a reader two candidate owners for
the test's resources.

Twelve of them were in the runner's Workspace suite. This is the rule's
own fix — each body dedented one level, nothing else moved — applied
because the rule is only reachable through the full `deno task lint`,
which runs its JS plugin, and the focused per-file checks that carried
these commits never loaded it.
… takes them

`Tier DLC — DLC13` holds the provider-neutral Workspace coordination
surface to a vocabulary no host owns, and a savepoint is one host's
answer rather than the question: it is what SQLite offers, and naming a
shared contract after it is how a neutral boundary quietly becomes one
provider's. The guard caught two files doing exactly that —
`src/workspace/savepoint.ts`, whose Api member, context identity and
refusal text all said savepoint, and `src/remote/workspace.ts`, which
implemented that member.

So the contract now asks for what it wants: `Transaction.undoable(body)`,
in `src/workspace/undoable.ts`. Nothing else changes. The local host
still nests a real SQLite savepoint, and the module it installs from
re-exports the operation under that name, so its own call sites read as
they always did. The runner still restores its disposable attempt from
the accepted root. No behavior, retained state, durable identity or
authority moves; the failing name does.

The guard only runs in the full battery, which is why the D-phase
focused evidence never reached it.
Discovery puts every new `*.test.ts` in all three runtime jobs, so
staying out of one is a deliberate act this file records. Two of the
remote suites never made that act, and the Node job failed on them
exactly as the file predicts: `createWorkflowRun` takes a run's advisory
lock through `Deno.FsFile.tryLock`, and no Deno runtime is present.

Both belong to the local host by subject rather than by accident. The
interoperability suite exists to show the two capture implementations
agree, so its fixture has to be produced by the local one — a real
`node:sqlite` store, a run created through the Deno provider, a DOFS
capture — before the runner materializes and recaptures it. The staged
fork suite assembles its candidate through the Deno adapter's own
connections, staging directory and lifecycle installation. Neither claim
survives being made portable, and the portable halves of both are
already elsewhere: `remote-fork.test.ts` for what a staged candidate
must be over the no-acquisition plane, and every other Tier WRH suite
for what the runner does.

Node now reports 4442 passing and none failing.
`deno task test:cloudflare` failed with "Vitest failed to find the
runner" and collected no tests at all. The cause is two installs: the
Workers plugin imports `vitest` and resolves pnpm's copy, while
`node_modules/.bin/vitest` is whatever wrote that link last — and a
`deno task` re-materializes `node_modules/.bin` into Deno's layout
before it runs anything, so in any tree that `deno task setup` prepared,
the CLI and the plugin are different vitest instances and the pool never
takes over.

Naming the module instead of the link settles it: `node
./node_modules/vitest/vitest.mjs` is the same copy the plugin resolves,
whichever installer touched `.bin` last. CI already ran this script the
one way that happened to work — `pnpm test:cloudflare`, with no Deno
layout in the job at all — and still does; what changes is that the
alias beside it works too, on the tree a contributor actually has.

`deno task test:cloudflare`: 14 files, 196 tests, 0 failures.
@minkimcello minkimcello changed the title 🏭 Build remote workflow provider checkpoints D1–D3 (#698) ✨ Run a workflow on a runner and its owner somewhere else (#698) Sep 11, 2026
@minkimcello
minkimcello marked this pull request as ready for review September 11, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant