Skip to content

🚀 Start and resume a workflow run from the CLI (#366 PR 2) - #428

Draft
taras wants to merge 10 commits into
agent/issue-366-workflow-clifrom
agent/issue-366-workflow-lifecycle
Draft

🚀 Start and resume a workflow run from the CLI (#366 PR 2)#428
taras wants to merge 10 commits into
agent/issue-366-workflow-clifrom
agent/issue-366-workflow-lifecycle

Conversation

@taras

@taras taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

xmd run executes against the caller's filesystem and promises nothing
afterwards. A workflow needs the opposite: an interrupted procedure has to
continue from where it stopped. Everything that makes that possible was already
built — run storage (#291), atomic Workspace effects (#365), the transaction-
bound Files provider (PR 1) — with nothing able to reach it. This is the command.

PR 2 of 2 for #366, stacked on #426. It is based on agent/issue-366-workflow-cli,
not on main; review #426 first. This PR closes #366.

What changes

Before: xmd workflow did not exist. Specifications described it; no CLI branch
executed it.

After:

xmd workflow start [--id=<run-id>] [--props-*=…] <definition>
xmd workflow resume <run-id>

Both stream the document to stdout and report identity and outcome on stderr:

workflow run: release-1.4
workflow status: completed

completed exits 0, failed 1, suspended 2, cancelled 3, interrupted
130. A refused request — bad grammar, missing run, incompatible reuse, damaged
storage, unsupported host — exits 1 and publishes no status line.

How it works

xmd workflow start flows/release.md
  → grammar (configliere: `workflow` command, two positionals)
  → establish definition from Git: repository root → HEAD^{commit} → object format
                                 → repository-relative POSIX path → cat-file blob
  → props phase inspects the PINNED bytes and builds --props-* bindings
  → host adapter opens storage; create() creates or compatibly finds the run
  → "workflow run: <id>"; begin document-execution record
  → attach: coordinator + logical cwd "/" + transaction-bound Files provider
  → execute: retained run installed, service denial installed, database journal
  → finish record, publish status, "workflow status: <status>", exit

resume skips the establishment and loads the retained object through the
run's retrieval metadata instead.

Review guide

Start with: packages/cli/src/workflow.ts — the whole lifecycle, and the
WorkflowHost interface that is the only thing it cannot do itself.

Then review:

  1. packages/cli/src/workflow-definition.ts — why the bytes that execute are the
    commit's, and why the checkout path is metadata rather than identity.
  2. packages/cli/src/cli.ts — the workflow command config, prepareWorkflowProps,
    and the two new seams in runDocument (config.stream, mode.install).
  3. packages/cli/src/deno-workflow.ts and the four entrypoints — where the
    capability is, and where it is refused.
  4. specs/workflow-workspace-spec.md §3.7 and §3.9 — the exit codes and exactly
    what is shipped.

Look carefully at:

  • The props phase runs Git, because generated --props-* arguments have to
    come from the document that is going to execute. Inspecting the working tree
    and running the commit would let help describe one document and the run be
    another.
  • The interruption finalizer in runWorkflow. It is registered before the
    execution begins and after storage is installed, so LIFO teardown reaches it
    while the database handle is still open. A scope torn down by Ctrl-C settles
    the run rather than leaving a record with no end.
  • componentDir: [] for a workflow execution. A definition is one immutable
    object; a search path would read the mutable checkout beside it. An
    unresolvable component fails explicitly instead (asserted by WFC6).
  • retainedSource() is branded (retained: true). Widening
    RootDocumentSource to accept any {path, source} would have broken IR3's
    contract that supplied text cannot travel under an identity nobody vouched
    for. The brand keeps IR3 true while letting a pinned document report its real
    path.

What must stay true

  • A document path never selects a run — enforced by resume taking only an
    id, refusing --id, every props form, and a third argument; checked by WFC3,
    WFC5 and WFC8.
  • The committed bytes execute — enforced by readObject(commit, path);
    checked by WFC4, which edits the working tree and asserts the edit does not
    appear.
  • The shared CLI names no host — enforced by the WorkflowHost interface;
    packages/cli/src/workflow.ts imports no SQLite, no DOFS and no runtime
    detection, and deno-workflow.ts is the only module that names the run store.
  • An unsupported host creates nothing — enforced by requesting the adapter
    before storage; checked by WFH1/WFH3, which assert no run id, no status and no
    run store directory.
  • Damaged storage is described, never replaced — checked by WFC7, which
    overwrites a run file and asserts it is byte-identical afterwards.

How to verify it

  • WFX1 is the load-bearing one. It starts a real xmd workflow start over 60
    file effects, waits — via when() on what a second connection can see, never
    a sleep — until at least three have committed, then SIGKILLs the child. It
    asserts the signal killed it, the run is still running (nothing ran after the
    signal), and then that the resume replays the committed events by their own
    event ids and in the same order
    , ends with exactly 60 effects, one per authored
    element, and advances the current root. A duplicate from an interrupted
    transaction that had already published, or a gap from one that had not, both
    fail it. Green on three consecutive runs, ~5s each.
  • WFC7 proves storage is left alone by comparing the file bytes before and after.
  • WFC6 proves a failed run replays as failed rather than retrying, and that the
    empty component search path is real.
  • WFC10 proves xmd run still writes into the caller's own filesystem.

Commands run on this commit:

deno task lint            # clean (pre-existing warnings only)
deno task check           # clean
deno task check:jsr       # Success Dry run complete
deno task test --changed=origin/main            # 466 passed (3227 steps), 0 failed
deno task test <cli+workflow+runtime+exclusions># 54 passed (427 steps), 0 failed
git diff --check                                # clean

deno task verify was not run: it includes the site pair and the full tri-runtime
corpus, which CI runs per job. The battery above covers every suite this change
selects plus the three new ones explicitly.

Scope

Included

  • xmd workflow start / resume, exit codes, and the two stderr metadata lines.
  • The Git definition adapter, and GitApi gaining repositoryRoot(),
    objectFormat() and readObject().
  • Host workflow adapters: Deno and compiled install the local store; Node and Bun
    refuse.
  • retainedSource() in core, so a pinned document reports its own path.
  • useQuietProcessOutput() in the runtime, so the Git capability can keep its
    answers out of the caller's output without importing a host process module.
  • Specification, architecture, README, site and CLI help updates.

Intentionally unchanged

  • xmd run and xmd test keep useHostFiles() and their host service adapters.
  • No status, list, history, cancel, fork or delete.
  • No generic --prop, --journal, inline --eval, agent flags or host selector
    on workflow.
  • No Repository, Worktree, Git effects, Agent or Worker Shell inside a run.
  • Document targets (Run document sections as hierarchical workflow targets #412): the definition descriptor cannot record one yet, so
    workflow start takes a plain path, exactly as xmd run does today.

New dependencies

  • @executablemd/workflow (workspace:*) added to @executablemd/cli. The CLI
    needs useRetainedWorkflow, useWorkflowServiceDenial, WorkflowRunStorage
    and the Git capability; only deno-workflow.ts reaches the /deno subpath.
    deno.lock, pnpm-lock.yaml and bun.lock are regenerated for it.

Risks and limitations

  • Concurrent resume is not safe and is not claimed to be. Add workflow inspection, cancellation, suspension, and deletion #367 owns durable
    ownership. Until then a run left running by a vanished host is treated as an
    orphaned interrupted execution by the next resume, which closes that unfinished
    record before beginning its own. Two resumes at once are not defended against.
  • The in-transaction kill is not this suite's. WFX1 kills at an uncontrolled
    point and asserts the invariant that holds either way. The stricter case — a
    child stopped at a point it announced from inside an open transaction — is
    Tier WAC's (workspace-crash-recovery.test.ts), and this PR does not restate it.
  • XMD_WORKFLOW_RUNS is how tests get an isolated store. It is a real user-facing
    variable, documented in the README, not a test hook.
  • The props phase resolves the Git definition before the host adapter is asked
    for, so Node and Bun run git rev-parse before reporting that the command is
    unavailable. Nothing is created or executed, but the ordering is worth naming.

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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/cli.ts
// current-run diagnostics only; existing traces are never loaded.
// Every CLI invocation starts from an empty stream unless the caller owns
// one. --journal writes current-run diagnostics only; existing traces are
// never loaded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// never loaded.

Comment thread packages/cli/src/cli.ts
// A workflow definition is one immutable object. A component
// search path would read the mutable checkout beside it, so a
// repository component fails to resolve rather than resolving
// to content the definition does not describe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// to content the definition does not describe.

cancelled: 3,
interrupted: 130,
// A run this process is still holding has not reported an outcome. Reaching
// here with one is a defect, and zero would call it success.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// here with one is a defect, and zero would call it success.


// Interruption is the outcome nothing else publishes. Registered before the
// execution starts, so a scope torn down by Ctrl-C settles the run rather
// than leaving a record with no end and a status of `running`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// than leaving a record with no end and a status of `running`.

around<T>(operation: Operation<T>): Operation<T> {
// A completed run replays its retained output and result. Attaching a
// Workspace for it would open a transaction and capture a root for
// work that is not going to happen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// work that is not going to happen.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Warning

This PR changes release configuration (.github/workflows/publish-packages.yml) without touching
specs/release-process-spec.md. Review the spec and update it to match, or
state spec-reviewed: no changes needed in the PR description (AGENTS.md
rule 8).

PR #428: 🚀 Start and resume a workflow run from the CLI (#366 PR 2)

28 files, +2309 / -57

Scope

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

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

🟡 28 files changed. Are all changes related?

🟡 Changes span 11 directories.

🟡 PR mixes config and source changes.

Structural

Oxlint structural signals:

  • no-empty-function ×2: packages/runtime/apis.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-assertion ×2: packages/runtime/apis.ts

Slop

  • packages/cli/src/cli.ts:1283 (removed)
  • packages/cli/src/cli.ts:459 (removed)
  • packages/cli/src/cli.ts:1322 (removed)
  • packages/cli/src/cli.ts:459 (removed)
  • packages/cli/src/cli.ts:1322 (removed)
  • packages/cli/src/cli.ts:1350// one that creates and executes nothing.
  • packages/cli/src/workflow.ts:439// work that is not going to happen.
  • packages/workflow/src/git.ts:165// here rather than executing as whatever show chose to print.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts

Static Analysis

Oxlint: 10 diagnostics across 2 files (6 rules)
Density: 0.004 violations/added-line

no-console (3): packages/cli/src/cli.ts
no-empty-function (2): packages/runtime/apis.ts, packages/cli/src/cli.ts
no-unnecessary-type-assertion (2): packages/runtime/apis.ts
no-unsafe-type-assertion (1): packages/cli/src/cli.ts
no-floating-promises (1): packages/cli/src/cli.ts
unbound-method (1): packages/cli/src/cli.ts

Correctness

No extraneous code patterns detected.

@taras

taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

CI does not run on this PR. ci.yml triggers on pull_request: branches: [main], and this one targets agent/issue-366-workflow-cli. The only checks here are PR Review (pass) and the Deno deploy build (pass) — the tri-runtime suites, lint, jsr, smoke, site and filesystem-contract jobs have not executed against this diff, and the two green marks should not be read as though they had.

The local evidence in their place, run on f5b701a with a quiesced tree:

deno task lint                                    clean (pre-existing warnings only)
deno task check                                   clean
deno task check:jsr                               Success Dry run complete
deno task test --changed=origin/main              466 passed (3227 steps), 0 failed
deno task test packages/cli/tests packages/workflow/tests packages/runtime \
               scripts/tests/runtime-exclusions.test.ts \
               packages/core/tests/inline-root.test.ts
                                                  54 passed (427 steps), 0 failed
git diff --check                                  clean

The three new suites were also run explicitly, and workflow-crash.test.ts three consecutive times (green, ~5s each) because it depends on a real SIGKILL landing part-way through.

deno task verify was not run: it adds the site pair and the full tri-runtime corpus, which CI parallelises per job. Everything this change selects is covered above.

Full CI will run once #426 merges and this retargets main.

@taras
taras force-pushed the agent/issue-366-workflow-lifecycle branch from f5b701a to 220bf0b Compare August 10, 2026 15:43
@taras

taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto #426's revised head 14b00d6, and fixed a defect that only a tri-runtime run could see.

New head: 220bf0b.

The defect

WFH1 failed under Node and Bun. preparePropsPhase ran before the workflow dispatch, and for start that phase establishes the definition from Git — so on a host without the capability the caller saw fatal: not a git repository … instead of the settled sentence. The requirement is that Node and Bun fail before creating or executing anything with that exact message; reporting a Git error first is the wrong reason for the right refusal.

I had named this ordering as a limitation in the PR body and judged it harmless because Git resolution creates nothing. That was wrong: the user-visible consequence is the wrong diagnostic, and it took a run under all three runtimes to show it. My Deno-only battery could not — WFH1 takes the Deno branch there and only asserts the message is absent. Credit to the peer session that ran the full verify on a clean checkout of f5b701a and reported it.

The fix

Host resolution is hoisted ahead of the props phase in runXmd, and the resolved WorkflowHost is passed into runWorkflow instead of an installer, so it is resolved once rather than twice. --help is exempt: the grammar is identical everywhere and describing it costs nothing, which is what WFH2 asserts.

Verification on 220bf0b

pnpm test:node        2934 pass, 0 fail
bun run test:bun      2934 pass, 0 fail        (201 files)
deno task test packages/cli/tests packages/workflow/tests
                      47 passed (384 steps), 0 failed
deno task test --changed=origin/main
                      466 passed (3233 steps), 0 failed   (pre-fix, post-rebase)
deno task lint / check / check:jsr             clean
git diff --check                               clean

WFH1 is the regression that catches this, and it already existed — it was simply never exercised on a host that refuses until the Node and Bun suites ran.

Rebase notes

Nothing conflicted. #426's revision keeps withWorkflowWorkspace exported, keeps useRetainedWorkflow's signature and its field-naming refusal, and appends to the Tier RR and Tier WF tables ahead of this PR's WFC/WFH/WFX block. The new completed-journal admission rule is exercised by this PR's replay paths — WFC2 (compatible reuse), WFC5 (resume of a completed run) and WFC6 (failed history replays as failed) all still pass against it.

The "Risks and limitations" bullet about props-phase ordering in the description above is now obsolete; this comment supersedes it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/cli.ts
// current-run diagnostics only; existing traces are never loaded.
// Every CLI invocation starts from an empty stream unless the caller owns
// one. --journal writes current-run diagnostics only; existing traces are
// never loaded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// never loaded.

Comment thread packages/cli/src/cli.ts
// On a host without workflow support the first thing a caller would otherwise
// see is whatever Git said about their directory, which is not the reason the
// command is not going to run. Help is exempt: the grammar is the same
// everywhere, and describing it costs nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// everywhere, and describing it costs nothing.

Comment thread packages/cli/src/cli.ts
// A workflow definition is one immutable object. A component
// search path would read the mutable checkout beside it, so a
// repository component fails to resolve rather than resolving
// to content the definition does not describe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// to content the definition does not describe.

cancelled: 3,
interrupted: 130,
// A run this process is still holding has not reported an outcome. Reaching
// here with one is a defect, and zero would call it success.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// here with one is a defect, and zero would call it success.

around<T>(operation: Operation<T>): Operation<T> {
// A completed run replays its retained output and result. Attaching a
// Workspace for it would open a transaction and capture a root for
// work that is not going to happen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// work that is not going to happen.

@taras

taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Independently reproduced on a clean checkout.

Because this PR gets no CI, a second session verified 220bf0b on a detached worktree that had never held either of our working trees, and reported the same numbers:

pnpm test:node      2934 pass, 0 fail
bun run test:bun    2934 pass, 0 fail   (201 files, 10713 expect() calls)

WFH1 is in that corpus and passes under both runtimes, so the unsupported-host diagnostic is confirmed on all three rather than only where the fix was written. The stack was also checked to be intact — #426 at 14b00d6, this PR at 220bf0b.

That independent run is the closest thing to CI evidence available until this retargets main, and it is not the same as CI: it does not cover the compiled-binary smoke, the filesystem-contract matrix, the site pair, or JSR publishability on a fresh clone. Those first run when #426 merges.

@taras
taras force-pushed the agent/issue-366-workflow-lifecycle branch from 220bf0b to 856fa25 Compare August 10, 2026 16:17

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/cli.ts
// current-run diagnostics only; existing traces are never loaded.
// Every CLI invocation starts from an empty stream unless the caller owns
// one. --journal writes current-run diagnostics only; existing traces are
// never loaded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// never loaded.

Comment thread packages/cli/src/cli.ts
// A workflow definition is one immutable object. A component
// search path would read the mutable checkout beside it, so a
// repository component fails to resolve rather than resolving
// to content the definition does not describe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// to content the definition does not describe.

cancelled: 3,
interrupted: 130,
// A run this process is still holding has not reported an outcome. Reaching
// here with one is a defect, and zero would call it success.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// here with one is a defect, and zero would call it success.


// Interruption is the outcome nothing else publishes. Registered before the
// execution starts, so a scope torn down by Ctrl-C settles the run rather
// than leaving a record with no end and a status of `running`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// than leaving a record with no end and a status of `running`.

around<T>(operation: Operation<T>): Operation<T> {
// A completed run replays its retained output and result. Attaching a
// Workspace for it would open a transaction and capture a root for
// work that is not going to happen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// work that is not going to happen.

@taras

taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Restacked onto #426's bdcb258. New head: 856fa25. Still draft.

That round moved workflow-run identity out of ReplayGuard and into the journal's own readAll, installed at Execution.around({ execute }, { at: "min" }), with the consequence that a journal holding any events must now carry exactly one canonical workflow_run record. That is the change most able to break this PR, because every resume path here replays a real journal: WFC2 (compatible reuse), WFC5 (resume of a completed run), WFC6 (failed history replaying as failed) and WFX1 (resume after SIGKILL). No fixture here hand-builds a journal — they all run the real CLI — so every journal carries the record the new rule requires. All four pass.

Nothing in this PR imports the surfaces that changed shape: no usePrivateWorkspace call (storage goes through useWorkflowRunStorage in deno-workflow.ts), and no import of the src/journal.ts error constructors whose signatures dropped their EffectDescription.

Verification on 856fa25

pnpm test:node                                    2941 pass, 0 fail
bun run test:bun                                  2941 pass, 0 fail  (201 files)
deno task test packages/cli/tests packages/workflow/tests
                                                  47 passed (392 steps), 0 failed
deno task lint / check / check:jsr                clean
git diff --check                                  clean

Node and Bun are run explicitly rather than left to CI, because this PR still has none — its base is agent/issue-366-workflow-cli, and ci.yml triggers only on pull_request: branches: [main]. WFH1 is the test that needs them: under Deno it takes the other branch and only asserts the unsupported-host sentence is absent.

taras added 6 commits August 10, 2026 13:25
A workflow run's `<File>` and `<Glob>` now reach the run's logical Workspace
instead of the caller's filesystem. Each read, write and search is one durable
Workspace effect, so the mutation, the immutable root it produces and the
filtered journal result commit together, and a replay restores the recorded
outcome without performing the mutation or asking what the file is now.

An authored path is resolved by arithmetic on POSIX segments rooted at `/`, so
no host path exists for a namespace race to replace. A documented DOFS refusal
rolls its mutation savepoint back before the sanitized result is published and
crosses the boundary as a `FilesReason` and nothing else; everything that is
not a documented refusal stays an infrastructure failure. A temporary directory
is refused rather than emulated.

`useRetainedWorkflow(run)` is the other half: a host that has already created
the run's storage record installs the exact frozen value, so the execution
allocates no identifier and resolves no base, and every journal state requires
the record to agree in run id, base and pinned commit.

The CLI cannot reach any of this yet; `xmd run` and `xmd test` keep the host
provider untouched.
A search answers with regular files, on the contract the host provider
already answers on: a symbolic link is neither a result nor a way into
the tree it names.

A recorded outcome is parsed rather than believed. A record must carry
its variant's members and no others, and a refusal's phase and reason
must both be words the operation's vocabulary holds; anything else is
the one fixed cause-free provider invariant, carrying nothing the record
happened to hold.

A retained installation requires the retained history as a whole to hold
exactly one successful workflow_run record that reads as a run and
agrees with the retained one. Reading a record can only refuse a record
the journal holds, so a completed journal recording a terminal result
and no run at all had nothing to refuse.

The document filesystem is installed through withWorkflowWorkspace() and
nowhere else. The Files provider alone would resolve a document's paths
against the surrounding host's working directory, and retain it.

DOFS stops nowhere between creating a write's parents and writing the
file, so the savepoint's rollback is observed through an adapter-private
interposition on the filesystem a Workspace transaction hands its body.
A ReplayGuard is composable policy: a handler installed further out may
answer without delegating. Identity decided there depended on middleware
ordering, and a completed journal reached under a suppressed guard
handed its recorded root result to whichever run asked.

The comparison is now a step inside the journal's own readAll, on the
terms core holds a resumed run to its recorded root selection: reachable
through no context, replaceable by nothing, ahead of every guard phase,
of terminal reuse, of live execution and of any append. It carries the
witness its source stream already had and establishes none.

A record identifies a run only as the root coroutine's successfully
settled Yield under the canonical type and the canonical name, holding a
closed value of exactly the three members a run has. Any history with
events must carry exactly one, so a same-typed Yield written elsewhere
cannot stand in for the record that was removed.

The filesystem a Workspace transaction hands its body is injected where
the provider is installed and kept in its closure. A stable Api name is
composition, and a component that reconstructs one reached the
authoritative filesystem through the seam this replaces.

Refusals retain a description holding the effect's type and name alone,
so nothing about the run stays reachable on the error object.
@taras
taras force-pushed the agent/issue-366-workflow-lifecycle branch from 856fa25 to ab57b0e Compare August 10, 2026 17:57
@taras

taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Restacked onto #426's 2adfc45 (force-push, so this was a real restack rather than a fast-forward). New head: ab57b0e. Still draft.

Two defects CI caught that my local battery did not

1. The generated publish workflow was stale. test-deno failed on 856fa25 at the Publish workflow is generated from manifests step. Adding @executablemd/workflow to packages/cli's dependencies changes the publish topology — workflow must publish before cli, and cli's needs list gains it — so .github/workflows/publish-packages.yml had to be regenerated and committed. Done, and deno task gen:publish-workflow is now a no-op on this tree. This check is a CI step rather than part of deno task test, which is why the local battery missed it; it is a known trap for any workspace-dependency change in this repo.

2. runXmd's new third parameter broke a call site on main. packages/cli/tests/targets-cli.test.ts (from #427) calls runXmd(args, installService). Rather than edit that test, installWorkflowHost now defaults to unsupportedWorkflowHost — a caller that does not name a workflow host has no run store, and inheriting one by omission is exactly the failure the boundary exists to prevent, so the default is the one that creates and executes nothing.

Restack notes

packages/cli/src/cli.ts conflicted with #427's xmd targets and document references. Resolved by union: the usage header carries both forms, COMMAND_NAMES is ["run", "test", "targets", "test-agent", "workflow"], the commands map holds targets and workflow, and exactRoot() is untouched. workflow start deliberately keeps taking a plain path rather than a document reference, because a definition descriptor cannot record a target yet — the header says so.

#426's other change this round narrows the strict one-canonical-record rule to useRetainedWorkflow(), which is the installation this PR uses, so the resume paths are unaffected — and no fixture here asserts on workflow-spec §6 wording.

Verification on ab57b0e

pnpm test:node                                    2986 pass, 0 fail
bun run test:bun                                  2986 pass, 0 fail
deno task test packages/cli/tests packages/workflow/tests
                                                  49 passed (437 steps), 0 failed
deno task lint / check / check:jsr                clean
deno task gen:publish-workflow                    no diff
git diff --check                                  clean

Correction to an earlier comment

I said above that this PR gets no CI. That was wrong — the full battery does run on it, and it is what found both defects. The earlier comment claiming otherwise should be disregarded.

On #426's failing deploy check

deploy/taras/executablemd passed on 856fa25, which contained all of bdcb258's content plus this PR's. The "Build failed" on bdcb258 is therefore not caused by that tree's content.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/cli.ts
// current-run diagnostics only; existing traces are never loaded.
// Every CLI invocation starts from an empty stream unless the caller owns
// one. --journal writes current-run diagnostics only; existing traces are
// never loaded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// never loaded.

Comment thread packages/cli/src/cli.ts
// Defaults to the host that refuses. A caller driving this without naming a
// workflow host has no run store, and inheriting one by omission is the
// failure mode the whole boundary exists to prevent — so the default is the
// one that creates and executes nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// one that creates and executes nothing.

Comment thread packages/cli/src/cli.ts
// On a host without workflow support the first thing a caller would otherwise
// see is whatever Git said about their directory, which is not the reason the
// command is not going to run. Help is exempt: the grammar is the same
// everywhere, and describing it costs nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// everywhere, and describing it costs nothing.

cancelled: 3,
interrupted: 130,
// A run this process is still holding has not reported an outcome. Reaching
// here with one is a defect, and zero would call it success.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// here with one is a defect, and zero would call it success.


// Interruption is the outcome nothing else publishes. Registered before the
// execution starts, so a scope torn down by Ctrl-C settles the run rather
// than leaving a record with no end and a status of `running`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// than leaving a record with no end and a status of `running`.

around<T>(operation: Operation<T>): Operation<T> {
// A completed run replays its retained output and result. Attaching a
// Workspace for it would open a transaction and capture a root for
// work that is not going to happen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// work that is not going to happen.

*readObject(commit: string, path: string): Operation<string> {
// `cat-file blob` rather than `show`: it refuses a tree or a commit instead
// of rendering one, so a root document path that names a directory fails
// here rather than executing as whatever `show` chose to print.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// here rather than executing as whatever `show` chose to print.

@taras

taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

CI fully green on ab57b0e — 16 checks pass, composability skipping as designed (it runs on main only).

green · lint · jsr · site · smoke · review · deploy
test-deno · test-node · test-bun
filesystem-contract × 5 platforms

That is the same battery #426 passes, run against this stack's head, and it includes the two steps that caught the last round's defects — the publish-workflow regeneration check and the full tri-runtime corpus.

Both PRs in the stack are now green and draft: #426 at 2adfc45 (17/17), this one at ab57b0e. Ready for architecture re-review.

taras added 4 commits August 10, 2026 14:14
`xmd workflow start [--id] [--props-*] <definition>` and
`xmd workflow resume <run-id>` run a document as a retained workflow run:
one implicit logical Workspace and one journal in a database that outlives
the process, so an interrupted procedure continues from its journal frontier
instead of from the beginning.

`start` names a document and `resume` names a run, and that asymmetry is the
lifecycle rule — a path locates a definition and never selects a previous run,
so two starts without `--id` are two runs. What executes is the *committed*
document: `start` resolves HEAD once and stores that commit as the run's
identity, so uncommitted edits do not change what a run is a run of, and a
resume loads the same object through retained, credential-free retrieval
metadata rather than the current HEAD or a same-named working-tree file.

Identity and outcome go to standard error as two stable lines, leaving stdout
to the document. Only a completed run exits zero; failed, suspended, cancelled
and interrupted are distinguishable so automation cannot mistake an incomplete
workflow for a finished one.

The capability lives on one host and the grammar on all of them: the Deno
entrypoints own the local run store, and Node and Bun refuse before creating or
executing anything. Resolving the host happens before the props phase, because
that phase establishes the definition from Git in order to read what the pinned
document declares — a host without the capability would otherwise answer with
whatever Git said about the caller's directory instead of the reason the
command is not going to run. The shared CLI module imports no SQLite, no DOFS
and no runtime detection: it asks a host adapter to open storage and attach the
run's Workspace.

Until #367 supplies durable ownership, a run left running because its host
disappeared is closed as an orphaned interrupted execution by the next resume.
Nothing here claims concurrent resume is safe.
@taras
taras force-pushed the agent/issue-366-workflow-lifecycle branch from ab57b0e to 31f7d3a Compare August 10, 2026 19:42

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/cli.ts
// Defaults to the host that refuses. A caller driving this without naming a
// workflow host has no run store, and inheriting one by omission is the
// failure mode the whole boundary exists to prevent — so the default is the
// one that creates and executes nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// one that creates and executes nothing.

around<T>(operation: Operation<T>): Operation<T> {
// A completed run replays its retained output and result. Attaching a
// Workspace for it would open a transaction and capture a root for
// work that is not going to happen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// work that is not going to happen.

*readObject(commit: string, path: string): Operation<string> {
// `cat-file blob` rather than `show`: it refuses a tree or a commit instead
// of rendering one, so a root document path that names a directory fails
// here rather than executing as whatever `show` chose to print.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// here rather than executing as whatever `show` chose to print.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add the xmd workflow start/resume filesystem vertical slice

1 participant