✨ Execute Markdown workflows without Git (#443) - #826
Merged
Merged
Conversation
A workflow definition can now be the source itself. The v2 descriptor addresses exact bytes by logical path, so a file outside a repository, an untracked file and a modified tracked file all have one immutable identity before anything retains them. Identity is two domain-separated, length-framed SHA-256 computations over the entrypoint, the canonical source manifest and the component mapping, through the platform's own `crypto`. The exact target stays outside the bundle hash and inside the definition: selecting a section does not change the bytes in the bundle. Host paths, props and retrieval metadata are not members of the shape at all. Parsing is closed and canonical. Logical paths are NFC scalar values without NUL, C0, DEL, backslash or `#`; manifests arrive in UTF-8 byte order with no duplicates, and a parser refuses a malformed order rather than sorting it. Snapshot verification copies the caller's buffers before checking them, so a later mutation of a caller-owned array cannot change what a run would retain. `WorkflowDefinition` stays v1-only here, so existing storage and runtime code remains coherent; storage, lifecycle, journals and artifacts become version-aware in the next commit. Refs #443
A workflow run can now be a run of the bytes it retains. Storage, lifecycle, journals and artifacts each became version-aware by their own identity rather than by widening version 1's, so a Git run keeps exactly the schema, record, descriptor, journal value and artifact bytes it always had. Live storage gains a second immutable inventory. Version 2 keeps every version-1 object byte for byte, replaces `workflow_run` with one that has no base column, and adds the two definition-source tables. Recognition reads the application id, then dispatches on version 1 or 2 and holds the whole inventory to that version's declaration: a hybrid, an extra object or version 0 is the file disagreeing with itself, never a migration candidate. Creating a version-2 run crosses only the trusted lifecycle transition. Public `WorkflowRunStorage.create()` stays version-1 and refuses a source-bundle descriptor, because that request carries a descriptor and no bytes. The transition copies the caller's buffers and holds them to the descriptor before the transaction opens, then writes schema, run, manifest, de-duplicated content, Workspace, lifecycle and the first execution together or not at all. Source is proved before anything moves. A version-2 run's content is re-derived from its own store — every length, every source hash and the bundle hash — and a missing entry, damaged content or a disagreeing hash refuses with the run's lifecycle and journal untouched. A version-1 run's content crosses the host-captured `LegacyWorkflowSourceReader`, which is a direct closure reachable through no Context, Api, component or Plugin; Workflow recomputes the returned root and component blob identities from the bytes themselves before any of it counts as this run's. A creation is held to its own descriptor before persistence, so a host that cannot obtain a Git definition's Markdown never reaches the transaction that would make the run exist. The public and durable `WorkflowRun` is one closed union. Version 1 keeps its exact three members; version 2 records `definitionVersion`, `bundleHash` and its exact target and invents no base or pinned commit. Readers accept either exact member set in any key order and nothing between them. Artifacts keep the physical container at version 1 and dispatch the semantic format from the header. Format 1 keeps its manifest, identity domain and Git closure byte for byte; format 2 has manifest version 2, the `xmd-artifact\0v2\0` domain, its own closed inventory, and one entry/content pair per logical source. The writer selects the format from the run's version and the reader verifies only the format its header declares. Refs #443
`xmd workflow start ./notes.md` now runs the file's current bytes. A file outside a repository starts, an untracked file starts, and a modified tracked file runs what it says rather than what its last commit said — because the run retains those bytes before it becomes durable, and a record naming a commit while executing something else was the thing that could not be true. Establishment reads the supplied root once. The logical entrypoint is the file's own final segment, normalized; the containing directory, the absolute path and the invocation working directory never reach the descriptor. Declared components are resolved against the root's own directory, read as bytes, decoded strictly and parsed — so a declaration this command cannot read refuses the start before any storage exists, rather than surfacing the first time a document writes the name. Every source's identity is the hash of the bytes that were actually read, and the descriptor is parsed back through storage's own closed parser before it is offered to anything. Git is provenance now, not retrieval. A start records where it happened when that is cheaply available, as replaceable metadata; a directory that is not a working tree simply has none, and the start is an ordinary start. Nothing reads it back to find a source. After admission the CLI executes the closure the lifecycle authenticated and nothing it read itself. The run id is reported only once the creation transaction has committed, and the post-admission reloads are gone: a candidate that remained a second path to execution would be a second answer to what the run is a run of. `workflow-source.ts` is now only the legacy version-1 Git reader, captured by the Deno host on both the run-host and lifecycle installations and reachable through no Context, Api, component or Plugin. It maps an unreadable object into the categorical failure Workflow declares for it and judges nothing else: Workflow recomputes the returned blob identities itself. Status and history render a source-bundle run by its entrypoint and bundle hash, with no base line and no fabricated commit. Refs #443
A workflow definition is two versions now, and the five authoritative documents say so. Version 2 is a source bundle: the exact bytes themselves, addressed by logical paths and retained with the run, so a file outside a repository, an untracked file and a file edited since its last commit all start and all stay runnable after the original is edited, moved or deleted. Version 1 is unchanged in shape, identity, behavior and bytes, and nothing migrates between them. `specs/workflow-spec.md` states the closed descriptor union and its dispatch by kind, the logical-path grammar, the domain-separated source and bundle hashes and what each commits to, the target's exclusion from the bundle hash and its place in identity, per-version compatible reuse, the two immutable live schema versions and their recognition, what a retained source has to prove, the creation union with its owned copies and one atomic transaction, the order that puts every source check before recovery and admission, the legacy source reader and its validation, and the five source refusals. `specs/xmd-artifact-spec.md` keeps the physical container at schema version 1 and splits the semantic format: format 1 retains its manifest, identity domain and Git closure byte for byte, and format 2 declares its own manifest version, `xmd-artifact\0v2\0` domain, closed inventory and one entry/content pair per logical source. The reader selects the verifier from the header and never reads one version's closure as the other's. `specs/workflow-workspace-spec.md` states the start ordering, the boundary that proves source before recovery, execution records, Workspace attachment or replay, the reference grammar `start` accepts, the fork candidate's independence from the source run and from every file it was read from, and the status shapes that carry no fabricated Git fields. `architecture.md` records the terminology, the two live schema versions, the CLI candidate against the authoritative retained source, and the inventory rows for the definition, the reader and both artifact formats. Two things are stated as undelivered rather than described as done: the artifact-backed fork, which this revision supplies the authenticated source input for, and #822's extraction of `workflowInstallation({ base })` — still the one place this package reaches Git to establish a run. Refs #443
taras
force-pushed
the
agent/issue-443-source-bundle
branch
from
September 17, 2026 13:47
08fc6ac to
897cef3
Compare
taras
enabled auto-merge (squash)
September 17, 2026 15:24
taras
disabled auto-merge
September 17, 2026 15:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #443.
Why
xmd workflow startlocated a repository, resolvedHEADand read the document throughrepositoryRoot(),revParse()andreadGitObject(). A file outside Git could not start, an untracked file could not start, and a modified tracked file executed its older committed bytes. That dependency also blocks #822: Workflow needs Git to load its own definition, while the Git Plugin needs Workflow's durable execution boundaries.What changes
Before:
A tracked file with unstaged edits started, but executed the bytes
HEADheld rather than the ones on disk.After:
The command snapshots the file's exact current bytes before the run becomes durable. The first execution and every resume use that retained snapshot even if the original file is edited, moved or deleted. Existing version-1 runs keep executing from their pinned commit, byte for byte, through a reader the host supplies.
How it works
A resume never reads the filesystem again. Under the executor lock, before any lifecycle mutation, it recomputes every retained length and content hash from the BLOBs it holds, and the bundle hash over the resulting manifest and mapping.
Two hashes, domain-separated. A source's content hash is
SHA-256(field("executablemd.workflow.source.v2") || u64(len) || bytes). The bundle hash commits to the entrypoint, the ordered manifest and the component mapping under its own domain.field(v) = u32(utf8(v).length) || utf8(v), so no length-extension or boundary ambiguity between adjacent fields.Logical paths are NFC Unicode scalar values,
/-separated, with no leading or trailing/, no NUL, C0, DEL, backslash or#, and no empty,.or..segment. They are compared and ordered by UTF-8 bytes — not by JavaScript<, which orders UTF-16 code units and would place U+FB01 after U+1F600.Review guide
Start with:
packages/workflow/src/storage/source-bundle.tsThen review:
packages/workflow/src/storage/definition.ts—parseWorkflowDefinitiondispatches onkind, notversion, which is what preserves every existing WD1–WD35 diagnostic including WD4's$.version.packages/workflow/src/lifecycle/source.tsandpackages/workflow/src/deno/definition-source.ts— the retained-source union and its authentication.packages/workflow/src/deno/transitions.tsandpackages/workflow/src/deno/schema.ts— source verification before lifecycle mutation, and live schema version 2.packages/cli/src/workflow-definition.ts— how a document reference becomes a bundle.Look carefully at:
settleSources()intransitions.ts: source verification happens under the executor lock and before stale recovery, attachment, replay or any lifecycle write. There is no rollback path, because nothing is written to roll back.validateLegacySources()indefinition-source.ts: Workflow recomputes every returned blob identity from the bytes that came back. A reader that answers with different content than the descriptor names is refused, not trusted.What must stay true
workflow-run-storage.test.tsandxmd-artifact.test.ts.beginsucceeds, only its authenticated closure reaches import. Enforced bybeginreturning the closure and the CLI having no second path to a candidate; checked by WFK42, which reads the live stagedworkflow_definition_blob.contentand asserts it equals the candidate bytes.packages/workflow/src/run.tsis the only Workflow module importing Git. Enforced by the import-boundary test, which pins that one existing import and its solerevParse()use and also catches dynamic and bare imports.compatibility.tscomparingkindbefore anything else; checked by the cross-version reuse case inworkflow-run-storage.test.ts.How to verify it
scripts/tests/plugin-compiled.test.ts— "starts and resumes a document outside any repository" proves the compiled binary needs no repository at all, and fails if any Git lookup remains on the start or resume path.workflow-definition.test.tsprove the hash vectors against fixed independent values, and fail if domain separation, field framing or UTF-8 ordering changes. The U+FB01 / U+1F600 pair fails specifically if ordering regresses to JavaScript<.workflow-cli.test.tsstarts with#Pub*and expects the reported target#Publish, proving the selector is canonicalized through Core before storage rather than retained as the caller wrote it.od#d.mdis addressable inside a bundle asod%23d.mdbut refused as an entrypoint, because#cannot be a logical path character.beginreturned rather than the candidate, which is unobservable at the storage layer because compatible reuse compares the whole manifest.readRunRowreads every record as v1.Manual check:
Scope
Included
WorkflowRun, forks and artifacts.LegacyWorkflowSourceReader, the direct non-contextual closure a trusted host supplies for v1.start,resumeandforkfrom retained source, including target resolution.Intentionally unchanged
user_version = 2.New abstractions
SourceBundleWorkflowDefinitionV2and its parser exist because identity must come from retained bytes rather than a Git object; consumed by storage, lifecycle, journals, artifacts and the CLI.LegacyWorkflowSourceReaderexists because a v1 run's bytes live in a repository Workflow must not reach into. It is a direct closure rather than a context so that Make Git and GitHub available in XMD runs and workflows #822 can move it without leaving a contextual seam behind.RetainedDefinitionSourcesexists as the closed v1/v2 union every executable path consumes, so a candidate cannot remain a second route to execution.Each new abstraction has multiple concrete uses or a clear justification.
No speculative functionality is included.
New dependencies
None. No manifest or lockfile changed.
Generated or mechanical changes
🐛 Narrow v1-only test hostsreplaces union member reads in five v1-only test hosts with the already-exportedisGitWorkflowDefinition/isGitWorkflowRunRecordpredicates. No casts.🐛 Install the legacy source reader in v1 CLI test hostspasseslegacySource: readLegacyDefinitionSourceat twelve installation sites in two suites whose helpers call themselves "the production host" while omitting what production installs.Risks and limitations
Scope confirmation