📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

📁 Give a workflow run's document its own filesystem (#366 PR 1) - #426

Merged
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli
Aug 11, 2026
Merged

📁 Give a workflow run's document its own filesystem (#366 PR 1)#426
taras merged 1 commit into
mainfrom
agent/issue-366-workflow-cli

Conversation

@taras

@tarastaras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd workflow needs a document filesystem that belongs to the run rather than to
whoever invoked the CLI. Until now the only Workspace mutation path was
adapter-private: <File> and <Glob> had no way to reach a run's Workspace, so
a workflow document would have written into the caller's directory with nothing
retained and nothing to resume from.

This is PR 1 of a two-PR stack for #366, rebased onto #433. It builds the
provider layer and the run installation the CLI composes over. PR 2 (#428) adds
xmd workflow start / resume, the Git definition adapter, cross-process
acceptance tests, and closes#366.

What changes

Before:

  • API.Files had one provider — the host adapter every entrypoint installs. The
    Deno Workspace coordinator existed, but only an adapter-private proof
    operation selected it and <File> could not reach it.
  • The only way to associate an execution with a run allocated its own id and
    resolved its own base, which a host that already created the storage record
    cannot use: the id it invents cannot agree with the record.

After:

  • withWorkflowWorkspace(database, operation) installs a run's Workspace effect
    coordinator, the logical working directory /, and a transaction-bound
    API.Files provider — inside the execution, so they answer ahead of the host
    adapter. A document's <File> and <Glob> then name entries in the run's own
    logical filesystem. It is the only installation the entrypoint publishes.
  • workflowInstallation({ base }) and retainedWorkflowInstallation(run) are
    ExecutionInstallation values a trusted host passes to executeInstalled().
    Each contributes a mandatory admission and a prepare hook; neither
    installs any Execution.document middleware. The retained one records the
    exact frozen { runId, base, pinnedCommit }, allocates nothing, and never
    calls Git.revParse().
import{executeInstalled}from"@executablemd/core/host";import{retainedWorkflowInstallation}from"@executablemd/workflow";yield*executeInstalled(options,[retainedWorkflowInstallation(run)]);

xmd run, xmd test and the host provider are untouched. No CLI reaches any of
this yet.

Where run identity is decided, and where the run is created

Two different places, and neither is middleware.

Identity is a mandatory admission. Canonical core captures it before any
installation, middleware or document code exists, and applies it inside the same
trusted readAll that already holds a resumed run to its recorded root
selection — on the one retained snapshot every later phase reads.

Creation and restoration are the installation's prepare hook — a
DurablePreparation core invokes inside the durable root, after admission and
before any public Execution.document policy, the root import and every
authored effect. On a completed terminal replay core never enters the durable
body, so preparation does not run and the admission is what restores the run; on
a partial continuation preparation re-enters and its durable operation restores
what it already recorded, so neither the identifier nor Git is reached again.

Earlier revisions of this PR put identity in ReplayGuard, then in an
Execution-installed stream wrapper, and kept creation in Execution.document
middleware. All three were defeated by registration order, which is what
motivated #432 and #433. None of that machinery lives here any more.

A record identifies a run only as the root coroutine's successfully settled
Yield, under the canonical type and name, holding a closed value of exactly
runId, base and pinnedCommit, agreeing with the installed identity. Any
history with events must carry at most one; a retained installation requires
exactly one, and a successful one.

A definition's exact document target (#431) is not identity. It says what the
run is a run of. A recorded value carrying a target as a fourth member is
refused as a value this version cannot account for, and the target is never
quoted back (RR23).

The slot is per invocation, not per installation value

install() runs once for each execution an installation value is passed to, and
that is where the slot is created. An earlier revision built it alongside the
value, so a host holding one workflowInstallation({ base }) and running two
documents with it gave both the same slot: each journal recorded its own run,
and both documents read whichever ran last. WR20 reproduces exactly that and
fails against it.

Admission and preparation reach the slot installed for their own invocation when
they run. Nothing is read back out of it to authorize retained history, select
the durable run or decide agreement — the captured RunPreparation and the
durable record decide all three, and the slot is only where the answer is
published.

One shape change the lifecycle forced

getWorkflowRun() reads a slot the installation puts in the execution's
scope, and the stable executablemd.workflow.run binding now holds that slot
rather than the run itself (WR17 asserts a foreign descriptor still reaches the
same frozen run, under run).

This is not a preference. Both writers now sit outside the scope that reads
them: admission runs inside core's own journal read, and preparation runs inside
the durable root. A context set from either ends with the operation that set
it, while the document that has to read it starts afterwards. The slot is the
same pattern core uses for LiveFailure. The binding was already explicitly
non-authoritative — a descendant may rebind the name, and durable enforcement
never depends on it — and that reasoning is unchanged.

Review guide

Start with:packages/workflow/src/deno/workspace/files.ts

  1. specs/workflow-spec.md §3.1, §3.2 and §10 — the contracts this adds.
  2. packages/workflow/src/run.ts — the two installation constructors, and the
    requirement each contributes.
  3. packages/workflow/src/journal.ts — canonical-record recognition, and the
    refusals that name differing fields without their values.
  4. packages/workflow/src/deno/workspace/logical-path.ts — why containment here
    needs no stable-namespace qualification.
  5. packages/workflow/src/deno/workspace/host.ts — what a host installs, and why
    the three pieces are not published separately.

Look carefully at:

  • The outcome envelope in files.ts. A refusal is retained as a phase and a
    reason, never as a serialized error — no DOFS message, errno payload, SQLite
    text or resolved path reaches the journal.
  • parseOutcome. Journal data is untrusted: a record must carry its variant's
    members and no others, each of the declared type, and a refusal's phase and
    reason must be words the operation's vocabulary holds. Everything else is the
    fixed cause-free FilesInvariantError("protocol").
  • writeOutcome: the mutation savepoint wraps parent creation and the write,
    so a refusal discards partial mutation before the sanitized result is
    published.
  • descend: a search answers with regular files, on HF3's contract. A symbolic
    link is neither a result nor a way into the tree it names.

What must stay true

  • A workflow document never reaches the caller's filesystem — the provider is
    installed at { at: "min" } inside the execution, and a host API.Files
    observer installed outside the run stays empty in every WF test.
  • <Glob> answers the same way under either provider — regular files only
    (WF11 mirrors HF3).
  • Replay consults no current state — every operation is a durable effect
    whose recorded value is read back (WF4, WF5).
  • Retained history is parsed, not believed — for the run record (RR8, RR9,
    RR14, RR20, RR21) and for every file outcome (WF13).
  • Suppressing or reordering policy cannot bypass run identity — RR10, RR11,
    RR17; RR12 and RR13 show valid replay and valid policy still compose.
  • The journal is the run's own — the coordinator's provenance check (WF9).
  • The three Workspace installations are one authority boundary
    withWorkflowWorkspace() is the only one exported from
    packages/workflow/deno.ts.
  • The transaction filesystem is the provider's — WF14.

Verification

6a3403cf6da12ec529920122b3b8841bda0f3add, rebased onto main at f4e3eee
(#434, which carries the merge of #433):

workflow-run, retained-run, workspace-files 3 passed (66 steps), 0 failed
deno task test packages/workflow/tests 32 passed (252 steps), 0 failed
deno task test --changed=origin/main 309 passed (2301 steps), 0 failed
deno task lint 0 errors
deno task check exit 0
deno task check:jsr exit 0
git diff --check clean

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

#MutationTests killed
M1descend drops the entry.kind !== "file" skipWF11
M2descend walks through symlinks as well as directoriesWF11
M3parseOutcome drops the exact-members checkWF13 (both)
M4parseOutcome falls back instead of refusing an unknown phase or reasonWF13
M5writeOutcome calls replace(...) without the nested savepointWF12
M8resolveLogicalPath stops refusing an empty pathWF6
M9host.ts does not install useWorkflowFiles16 of 17 WF steps
M10effect.ts skips validateJournalProvenance(...)WF9
N3canonical recognition weakened to effect type onlyRR14
N4the owning coroutine is not checkedRR14
N5more than one successful record is acceptedRR8
N6the recorded run value may carry extra membersRR14, RR23
N7refusals retain the recording descriptionRR16
N8the filesystem decorator becomes a contextual Api againWF14
N9content is coerced instead of type-checkedWF13
N10glob path entries are coerced instead of type-checkedWF13
P3a programmatic run demands a successful recordWR18
P4a retained run stops demanding oneRR8, RR14, RR15
Q2failed canonical claims left out of the duplicate countRR21
Q3a mixed successful/failed history is acceptedRR21
Q4the host-supplied retained run is read without totalityRR22
W1preparation moved back into Execution.document middlewareWM1, WM3
W2the prepare hook removed from the installation32 of the 32 WR/RR/WM steps
W3the admission removed from the installation15 WR/RR steps
W4a retained run allocates and resolves instead of using the supplied value12 RR steps
S1the slot is created with the installation value instead of per install()WR20

Four honest limits:

  • RR9 is caught by per-event checking, not by the admission, so a mutation
    that removes only the admission leaves it green. RR8/RR14/RR15 cover what the
    admission alone decides.
  • A mutation replacing FilesInvariantError("protocol") with a generic caused
    error fails nothing.
    invokeFiles in packages/core/src/files.ts already
    normalizes any non-Files provider throw into a fresh cause-free protocol
    invariant, so the observable guarantee is core's and the provider's own throw
    is defence in depth.
  • A mutation letting readWorkflowRun throw its own exception fails nothing,
    because retainEvents rebuilds a recorded value before anything reads it —
    runClaim's catch is the real converter, and Q4 covers the host-supplied side.
  • WF3–WF5's mutation evidence is inherited from 20b7034, where "reads the
    current frontier instead of the recorded result" killed them. That machinery is
    unchanged by the rebase and no mutation re-run here covers it.

Scope

Included

  • Transaction-bound API.Files provider over the run's logical DOFS Workspace.
  • withWorkflowWorkspace() — coordinator, logical cwd /, provider — as the
    single public installation boundary.
  • workflowInstallation() / retainedWorkflowInstallation() as
    ExecutionInstallation constructors.
  • parseFilesReason / parseFilesPhase / parseFileWritePhase exported from
    @executablemd/runtime, so a provider reading a retained refusal parses the
    one vocabulary rather than declaring a second copy.
  • Specification and architecture updates for the above.

Intentionally unchanged

  • No CLI. xmd workflow does not exist yet.
  • xmd run and xmd test keep useHostFiles() exactly as they had it.
  • Programmatic establishment behaviour: a base that would not resolve is recorded
    as a failed effect and that history is still this run's, so no successful
    record is demanded of it (WR18).
  • Repository, Worktree, Git effects, Agent materialization and history forks
    remain unbuilt.
  • JournalProvenance stays at its existing trusted wrapping sites; this PR adds
    none.

Rebase notes

  • This PR's own packages/core/src/journal-admission.ts and the admitJournal /
    requiredJournalAdmissions exports are removed🔒 Make canonical core authoritative for execution #432 replaced them, and
    🔒 Make canonical core authoritative for document expansion #433 replaced the document-middleware half.
  • RR17's stream hijacks were written against the old
    Execution.execute(options) signature and now use
    next(request.withOptions({ ...request.options, stream })).
  • Three assertions naming the old API in WR1/WR10 moved to
    workflowInstallation; the API name changed by design, so those expectations
    are contract maintenance rather than a test edited to pass.

Scope confirmation

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

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9010005 to 20b7034CompareAugust 10, 2026 14:31
@github-actions

github-actionsBot commented Aug 10, 2026

Copy link
Copy Markdown

PR #426: 📁 Give a workflow run's document its own filesystem (#366 PR 1)

25 files, +4280 / -229

Scope

🔴 PR has 4509 lines changed. Split into focused PRs.

🟡 4509 lines changed. PRs under 400 receive more thorough review.

🟡 25 files changed. Are all changes related?

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-unused-vars ×1: packages/workflow/src/deno/provider.ts
  • no-empty-function ×1: packages/workflow/src/deno/provider.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 11 diagnostics across 7 files (7 rules)
Density: 0.003 violations/added-line

no-shadow (2): packages/workflow/src/deno/workspace/private.ts
no-array-sort (2): packages/runtime/files.ts, packages/workflow/src/deno/workspace/files.ts
no-floating-promises (2): packages/workflow/tests/support/workspace-restart-child.ts, packages/workflow/tests/support/workspace-crash-child.ts
unbound-method (2): packages/workflow/src/deno/workspace/effect.ts
no-useless-spread (1): packages/runtime/files.ts
no-unused-vars (1): packages/workflow/src/deno/provider.ts
no-empty-function (1): packages/workflow/src/deno/provider.ts

Correctness

FILE: packages/workflow/src/deno/workspace/private.ts, PATTERN: no-shadow, CONCERN: Shadowing variable in same scope, QUESTION: Is the shadowed variable intentional or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-array-sort, CONCERN: Mutation of array in place, QUESTION: Is in-place sorting required or a potential bug?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-floating-promises, CONCERN: Unhandled promise, QUESTION: Are these promises intentionally unhandled?
FILE: packages/workflow/src/deno/workspace/effect.ts, PATTERN: unbound-method, CONCERN: Method may lose this context, QUESTION: Is this correctly bound in all usages?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: no-useless-spread, CONCERN: Redundant spread operator, QUESTION: Is the spread necessary or a leftover from refactoring?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-unused-vars, CONCERN: Unused variable, QUESTION: Is useWorkflowRunStorage intended to be used elsewhere?
FILE: packages/workflow/src/deno/provider.ts, PATTERN: no-empty-function, CONCERN: Empty function body, QUESTION: Is this function a placeholder or a mistake?
FILE: packages/workflow/src/deno/workspace/files.ts, PATTERN: signal cluster, CONCERN: Multiple Oxlint violations in same file, QUESTION: Are these violations clustered due to unreviewed code?

@taras

Copy link
Copy Markdown
OwnerAuthor

Stacked: #428 is PR 2 of this pair — the xmd workflow start / resume CLI lifecycle, the Git definition adapter, cross-process acceptance tests and documentation. It is based on this branch rather than main, and it closes #366. Review this one first.

@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from bdcb258 to 2adfc45CompareAugust 10, 2026 17:42
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 15c24ff to c6dbdafCompareAugust 11, 2026 02:13
@taras
taras marked this pull request as ready for review August 11, 2026 02:16
@taras
taras marked this pull request as draft August 11, 2026 16:19
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from c6dbdaf to 9e86cebCompareAugust 11, 2026 16:32
`xmd workflow` needs a document filesystem that belongs to the run rather
than to whoever invoked the CLI. Until now the only Workspace mutation
path was adapter-private: `<File>` and `<Glob>` had no way to reach a
run's Workspace, so a workflow document would have written into the
caller's directory with nothing retained and nothing to resume from.
`withWorkflowWorkspace(database, operation)` installs a run's Workspace
effect coordinator, the logical working directory `/`, and a
transaction-bound `API.Files` provider — inside the execution, so they
answer ahead of the host adapter. It is the only installation the
entrypoint publishes: the three pieces are one authority boundary.
A run is attached through `ExecutionInstallation` values a trusted host
passes to `executeInstalled()`. Run identity is a mandatory admission
canonical core captures before any installation, middleware or document
code exists and applies inside its own trusted journal read. Creating or
restoring the run is the installation's `prepare` hook — the trusted
durable preparation core invokes inside the durable root, after
admission and before any public `Execution.document` policy, the root
import and every authored effect. No public middleware carries
workflow-run authority: a handler that answers without delegating
cannot fabricate a run, and a substitute result replaces nothing.
The run a document reads is published through a slot `install()` creates
per invocation, so a host holding one installation value and running two
documents with it gets two runs and neither execution can see the
other's. Nothing is read back out of that slot to authorize retained
history, select the durable run or decide agreement — the captured
`RunPreparation` and the durable record decide all three.
`retainedWorkflowInstallation(run)` records the exact frozen
`{ runId, base, pinnedCommit }` a host already created, allocates
nothing and never calls `Git.revParse()`.
Because #433 binds a terminal created before the root import to the
exact root source and target it was about, a preparation that failed —
a base that will not resolve — now replays as the failure it was rather
than as a history this version cannot read. The workflow specification's
§6 limitation is removed rather than recorded.
@taras
tarasforce-pushed the agent/issue-366-workflow-cli branch from 9e86ceb to 6a3403cCompareAugust 11, 2026 16:52
@taras
taras marked this pull request as ready for review August 11, 2026 16:56
@taras
taras merged commit 1387222 into mainAug 11, 2026
16 checks passed
@taras
taras deleted the agent/issue-366-workflow-cli branch August 11, 2026 17:02
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import, through the same host-service slot
`xmd run` fills with a real adapter. A run's Workspace is attached only
for live and partial work: a completed replay has a recorded root result
and is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
taras added a commit that referenced this pull request Aug 11, 2026
`xmd workflow start <definition>` creates a run from committed Git bytes
and executes it; `xmd workflow resume <run-id>` continues one from its
retained definition and metadata rather than from whatever the checkout
holds now. A start may name its own id with `--id`, and reuses a
compatible run rather than creating a second one.
The run reaches canonical core the way #426 and #433 settled: as an
`ExecutionInstallation` the trusted host passes to `executeInstalled()`,
carrying the retained-run admission core applies inside its own journal
read and the `prepare` hook it invokes inside the durable root. One
execution, not an installed call followed by an ordinary one — `xmd run`
and `xmd test` pass an empty installation list, which is exactly what
`execute()` already does.
A run that ended is not a run to continue. `resume` admits interrupted,
suspended and — as a full replay — completed; failed and cancelled are
refused before the definition is fetched, before an orphaned execution
is closed, before a record is begun, before a Workspace is attached and
before anything is appended. Reusing a compatible id through `start`
still replays a failed run's retained failure; that is a separate rule.
A status line says what was retained, so it is published only once both
lifecycle writes have persisted — the completion record, then the run
state. The first refusal is the answer: the intended status is neither
published nor claimed, the invocation exits 1, and a document failure
that also occurred is still reported. An explicit lifecycle phase keeps
a post-execution storage refusal from being republished as a host
interruption.
The definition is pinned to a Git object, so an uncommitted edit beside
it never runs. A component search path would read that mutable checkout,
so there is none. Service denial is installed in the execution's own
scope, before the root import. A run's Workspace is attached only for
live and partial work: a completed replay has a recorded root result and
is given no filesystem to open a transaction against.
Node and Bun refuse to host a workflow with one settled sentence, before
reading a definition or creating a store, while rendering the same
grammar every host does.
Co-authored-by: Taras Mankovski <74687+taras@users.noreply.github.com>
Sign up for freeto 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.

Add the xmd workflow start/resume filesystem vertical slice

1 participant

@taras