System architecture — the distributed work runner
Status: open/accepted
Summary
Section titled “Summary”- SDLC is a distributed work runner: work orders are scheduled, claimed, and executed by registered processes; the SDLC entities and flows are the primary registered instance, not the system itself.
- One vocabulary — work order / process / run / runner / engine — ends the three-way naming collision among the predecessor drafts (“runner registry” / “process registry” / “workflow”).
- Scheduling is the graph-scheduler frontier plus a policy filter chain; scope conflicts and scoped concurrency limits are filters over declared scope claims, adjudicated by intersect — advisory by default, escalatable per claim to a mandatory hard lock.
- The execution plane has virtual-actor semantics: the lease ref is the actor identity, dispatch is activation, TTL/steal is supervision, and all actor state rehydrates from the substrate.
- The engine is a single self-contained Rust binary deployed on a laptop / user PC — no cluster, database, or broker; distribution is several user machines — later, optionally an always-on server as one more peer — sharing one ref authority. A read-only status API ships with engine v0; SDF is the dashboard over it. The compiled DAG format is the authoring contract — surfaces are plural; TypeScript (flowline) is the first. Contract types are Rust-first, generated to TypeScript by ontogen.
- The engine and flowline are general work-scheduling components, blind to the SDLC model — they live in the shared package tiers and never import the SDLC substrate; SDLC is one adapted instance bound in through the ports.
- Named seams are ports with swappable implementations behind them — coordination, run records, work-order source (per-project; SDLC’s is and stays the corpus), runner context, agent driver. Git implements coordination today (zero-infrastructure, free); a broker or database is a later implementation, not a rewrite. The corpus itself is never a port.
- This decision replaces the prior current-state and target-state architecture maps. The supersession map is fully executed: superseded files are deleted, not tombstoned — git history is the archive — and the surviving-doc amendments are applied.
Status
Section titled “Status”Accepted 2026-08-16. The supersession map is fully executed — the superseded maps are deleted
(git history is the archive) and D-0006 is amended — so this document is the architecture map.
Section prose is deliberately compact; it deepens as the implementation ledger’s phases land
(docs/planning/d-vsli-implementation-plan.md).
Context
Section titled “Context”The architecture this decision names already exists in fragments, never unified:
- The runner itself is prose: ~3,750 lines of SKILL.md across five lifecycle skills sequence all
dispatch, branching, retry, and escalation; no TypeScript module sequences more than one lifecycle
step, and workflow invariants are enforced by linting the English
(
gate/ops/_skill_prose_contract.ts). - The Task entity carries three jobs at once — work submission, process specification, execution
record. The universal implementation-ready gate demands
implementation-grade precision at definition time; 111 of 567 tasks carry a
definition_gap, and four schema versions are live in the corpus. - Two overlapping state machines — frontmatter
status(a cache) and leasephase(authoritative) — with the reconciler stubbed. - The distributed substrate is already shipped and accepted (
github-ref-leases/README: CAS claims, fencing, heartbeats, steal-on-expired, dispatcher/executor/reconciler roles) but no architecture document claims it as the system’s core. - The framing was split across two overall-architecture ADRs (a current-state and a target-state
map), two overlapping state-plane drafts, and one un-decided workflow engine spike (flowline,
origin/consolidate/workflow-library-full, B-QAHU) — consolidated here; the fully superseded drafts are deleted.
Decision
Section titled “Decision”The shape
Section titled “The shape”SDLC is a distributed work runner. The core loop:
work order → schedule (frontier + filters) → claim (lease) → run (registered process) → record (execution substrate) → verdict (terminal write to corpus)Everything SDLC-specific — the task lifecycle, PR shepherding, backlog triage, docs generation — is a registered process, a scheduling policy, or a work-order type riding this loop. The loop itself is domain-blind.
Domain-blind is a build constraint, not a description. The engine and flowline know work
scheduling and nothing else: they carry no SDLC entity, no Task, no corpus schema, and never
import the SDLC substrate. SDLC is one adapted instance, bound in through the ports — its corpus
behind the work-order source, its lifecycle as registered processes, its readiness contract private
to the implementation process. Three consequences follow and are binding:
| Consequence | Effect |
|---|---|
| Both live in the general package tiers | packages/rust/foreman and packages/ts/flowline, never under solutions/ontological/ |
| Contract types are domain-neutral | A work order is goal + acceptance + kind: + links + optional scope; nothing task-shaped leaks in |
| A second instance must be conceivable at all times | If a change only makes sense for SDLC, it belongs in the adapter, not the engine |
The general-vs-consumer rule this repo already applies to solutions/ontological applies one tier
further down: an engine feature that encodes SDLC’s conventions is misplaced.
The adapted instance is itself a layer, not a script: an SDLC engine wraps the domain-blind one, binding this project’s corpus, processes and policy — and it stays Rust, because wrapping is the next layer of the same machine rather than a translation of it (P-0012-rust-core-adapters-as-needed). Three layers, one language: scheduling core → work-runner engine → SDLC engine. Adapters in other languages sit outside that stack.
Vocabulary
Section titled “Vocabulary”| Term | Meaning | Replaces / absorbs |
|---|---|---|
| Work order | Durable, minimal job submission; Task is the primary type | ”work order” (kept) |
| Process | Versioned, registered definition of how a kind executes | ”runner registry” entry, “process”, “workflow definition” |
| Run | One execution of a process against a work order | — |
| Runner | The component executing a run (agent session today; engine-supervised child later) | “workflow runner” (kept) |
| Engine | Long-lived host: scheduler, supervisor, event log | ”engine” (kept) |
Work orders
Section titled “Work orders”The job payload is minimal: goal, acceptance, kind: routing key, upward links, optional scope
claim and scheduling hints. Spec depth is a property of the routed process, not the work-order
template. implementation-ready.md stops being a universal gate and becomes the
implementation process’s private readiness contract; other kinds declare their own. Reference,
don’t copy: a work order cites capability locations or paths, never restates code state — the
demoted Today table is resolved fresh at dispatch, so freshness is the dispatch gate’s job.
Tool-improvement micro-fixes accrue on a process’s hardening checklist until a batched work order
is cut, instead of minting one order each.
Work-plane lifecycle policy, run as registered recurring processes: last_reviewed aging
flags stale, then rotten; rotten work orders batch into an auto-close proposal for one-shot human
approval, never silent closure. Closed work orders archive out of the hot directory. Milestone
rosters are derived views, never hand-maintained checklists. Backlog stays the single intake for
every plane; promotion re-homes the authored body, and a duplicate may terminally point at any
entity.
Submission is a separate duty from scheduling; they meet only at the corpus. To submit is to
mint a durable work order — a corpus write, whatever carries it: an agent session in-project,
spawn-task-pr cross-project, later a network ingress accepting incoming jobs. To schedule is to
decide when accepted work activates. Submitters never touch the frontier, claims, or leases; the
scheduler mints no work. In actor terms the corpus is the mailbox — anyone may append to it, only
the engine activates from it. In-code flowline registrations are submission sugar: they mint work
orders into the project’s store rather than acting as a live source.
The process registry
Section titled “The process registry”Keyed on kind:. A registration declares: readiness contract, step
definition, output contract, scheduling requirements (scope declaration, concurrency class),
human-gate placement, and runner context (in-session skill today; engine-supervised child process
later). Gates are per-process: implementation keeps the human PR-merge gate; deterministic system
duties run gateless; content-bearing output always routes through a PR.
A step’s implementation is a tagged variant, and the tag exists from v1 even though not every
variant ships at once — retrofitting a discriminator onto a registry that assumed one
implementation shape is a rewrite, and the point of the registry is that kind binds to behavior
loosely.
| Variant | Ships | Notes |
|---|---|---|
| Built-in | v1 | Compiled into the engine; the handler registry the salvaged worker already has |
| Shell command | v1 | The floor — a registration is allowed to be one command line, with no plugin machinery in the way |
| Child process over the step protocol | v1 | stdio JSON-RPC; sdlc --output json is the standing example |
| Agent step | v1 | The judgment port, through the agent-driver port |
| WASM plugin | later | Sandboxed and language-agnostic, so a kind can be registered without touching the engine or trusting a subprocess. The toolchain already targets wasm32-unknown-unknown and yaml-splice-wasm is the in-repo precedent |
Shell and WASM are what keep the registry flexible rather than merely extensible: registering a work kind should not require writing Rust, and in the WASM case should not require trusting the thing being registered.
Kind v1 roster: implementation (the default when absent), planning, research; only
implementation has a registered process — leaves of other kinds stay human-driven until theirs
exist. kind: is immutable at creation. Dispatch is leaf-only: a work order with children is a
structural rollup, never dispatched, whatever its kind; nested traversal (parents decomposing into
children) is the end state, held behind the decomposition-guardrails open question.
Project-local processes ride the S-0003-deterministic-general-extension-points mechanism — a
consuming repo adds or overrides definitions without forking the plugin. SKILL.md is the harness
adapter over a definition, generated from or validated against it, never where procedure truth
lives. Registrations are
versioned code under apps/sdlc/, never corpus entities (donor finding F8). Dispatch is a lookup:
the work order carries only the key.
Scheduling
Section titled “Scheduling”The frontier comes from graph-scheduler (D-BPD8-graph-scheduler-api/README),
implemented in Rust so the engine calls it in-process rather than across a child-process
boundary on the dispatcher’s hot path. D-BPD8’s design is semantic — its layering, status enum,
trigger rules, capacity and sorter model all survive; only its illustrative TypeScript surface is
replaced, and its subpath layering (graph-scheduler/graph, /tracker, /pull) needs a Rust
expression, since subpath exports are a packaging idiom Rust does not share. No TypeScript peer
is built — nothing needs one, since sdlc task next carries its own frontier implementation and
keeps it until the CLI is Rust. Should a TS consumer ever force the question, the
markdown-util / yaml-splice pattern is there — twinned tiers
against one language-neutral conformance corpus — and its necessity shrinks as the sdlc CLI moves
to Rust. All policy is a filter chain in front of the frontier. Filters return ok /
blocked(reason, retry-hint) and power explain():
| Filter | Source |
|---|---|
| Dependency satisfaction | built-in trigger rule |
| Kind has a registered process | dispatch predicate (process registry above) |
| Leaf-only — containers are structural rollups | orchestrator-selection rule (process registry above) |
| In-flight caps | sdlc.yaml orchestrator caps, as counting filters |
| Scope conflict | pairwise intersect vs. active lease scopes |
| Scoped concurrency policies | sdlc.yaml pattern-set → limit registry, via intersect index() |
Ordering stays the D-Q2WR sort chain, supplied as sorters; sdlc task next becomes the SDLC policy
layer over the generic scheduler.
The last two filters are adjudicated by PR-0002-intersect, which gains a Rust peer so the engine calls it in-process for the same reason the frontier is in-process — the dispatcher tick carries no child-process boundary. The twinned-tier pattern applies: both tiers run one language-neutral conformance corpus, which intersect already ships as its JSON fixture set.
Scope claims
Section titled “Scope claims”A work order MAY declare a scope: a pattern set (paths, dirs, globs) it expects to write. The
scope is stamped into the lease payload at claim. Conflict detection is advisory dispatch-time
filtering, not locking — worktrees plus the human merge gate remain the correctness backstop; a
missed conflict costs a rebase. Unscoped work orders schedule as today (no conflict filtering), so
adoption is additive. The demoted Files to touch table is the natural scope source; the
touchpoint parse/resolve ops already emit the pattern set. Not named impact — that word is taken.
Hard locks are an escalation tier on the same claims, not a new default. A work order MAY mark
its claim enforcement: mandatory (advisory stays the default): the lock is held as a lease —
TTL, heartbeat, steal-on-expired — for the work order’s lifetime, and the conflict filter answers
blocked(locked-by <work order>, retry: on-release) to anything whose declared scope intersects
it, serializing all declared work on the region behind the holder. Motivating case: a major
refactor that must not race concurrent edits. By default the lock is hard only against declared
scopes — unscoped work passes it, so worktrees plus the merge gate stay the backstop; a per-lock
strict flag may additionally hold unscoped work orders while the lock lives. TTL expiry is
load-bearing on a deployment target that sleeps.
Triggers and recurring work
Section titled “Triggers and recurring work”Cron-style execution decomposes into data, one duty, and ordinary runs — not per-job actors. A
recurring job is a process registration carrying a trigger declaration; time is one trigger
source among several (cron expressions, level-triggered watch predicates — today’s
orchestrate watch — external events). The trigger service turns due triggers into activations.
Defaults: an overlapping firing is skipped while the prior run holds its operation lease; firings
missed during sleep coalesce into one catch-up run on wake; no jitter.
Each firing rides the normal loop: mutual exclusion via the existing operation-lease class
(refs/sdlc/ops/…, where reconcile and backlog-triage are already registered), the same
filters and caps, the same run records. Firings mint runs, not corpus work orders (donor
finding F8); semantic work a recurring run discovers is emitted as ordinary work orders. Today’s
/loop + ScheduleWakeup + orchestrate watch combination is this service in primitive form,
with the dispatcher as its one consumer.
Execution plane: virtual-actor semantics
Section titled “Execution plane: virtual-actor semantics”Adopted as semantics, not a runtime — no actor framework:
| Actor concept | Existing primitive |
|---|---|
| Identity | lease ref (refs/sdlc/tasks/<id>) |
| Single activation | CAS claim + fencing tokens |
| Activation / deactivation | dispatch → run → exit at PR open; reacquire is reactivation |
| Persisted state | lease payload + worktree + (later) event log |
| Behavior | the registered process for the work order’s kind |
| Supervision | TTL expiry, steal-on-expired, reconcile |
| Messages | stdout markers + handoff.md today; typed protocol later (open question) |
A process’s behavior may decompose: mint child work orders and let the scheduler dispatch them —
the formalization of the nested-traversal end state (process registry above). Messages may also
cross package and project boundaries: a work order addressed to a monorepo package or peer repo is
delivered into that target’s own corpus (today’s spawn-task-pr PR-delivery path) and executed by
the target’s own loop — packages and peer projects are addressable actors whose mailbox is their
work-order corpus.
Ownership nests logically — repo → branch → package — but only the branch level is a physical
serialization point: a git ref admits one commit order, so the corpus-writer stays one-per-branch.
Package-level ownership is a scheduling and policy scope — per-package mailbox semantics (at most
one active work order writing a package) come from the scoped-concurrency filter over the package
root, and per-package review or merge policy attaches to the same scope — never a second
main-writer. Long-lived system actors own genuinely
serial domains: dispatcher, reconciler, corpus-writer (serializes all main-branch writes, dissolving
the shared-checkout race class), PR shepherd, heartbeat, and the trigger service — the duty of
turning trigger sources (cron schedules, watch predicates, external events) into activations. These
generalize today’s defineService tier and the lease protocol’s role model.
Two rules keep it honest: actor state is always rehydratable from the substrate (no in-memory truth), and an actor boundary exists only where exclusive ownership and behavior coincide — everything else is a plain op call (the D-0007 granularity rule, one level up).
Identity and granularity
Section titled “Identity and granularity”| Thing | Gets | Why |
|---|---|---|
| Work order | actor identity (lease ref) | lifecycle, strategy, ownership |
| Run | identity in the execution record | beginning, end, outcome |
| System duty (dispatch, reconcile, corpus writes, PR shepherding) | long-lived actor identity | exclusive duty over a serial domain |
| PR under shepherding | lightweight virtual identity (cursor) | per-identity state, op-supplied behavior |
| Package / peer project | durable identity + mailbox (its work-order corpus) | has a contract, lifecycle, and consumers; acts only through work orders addressed to it |
| Recurring job | durable identity (registration + operation lease + run history) | schedule is data; ticking is one system duty; each firing is a run |
| Directory / scope | claim in a lease + policy entry | contended but passive |
| File | nothing | data |
State planes
Section titled “State planes”The semantic/execution split of D-S30G-task-state-plane-split is adopted as this architecture’s
state model: four semantic transitions commit to main (create, definition movement, promotion,
closure); execution state lives on the lease (phase + gate memos) and later the engine event log.
Frontmatter status remains a cache with a deprecate/remove window for the execution values.
Engine runtime and deployment
Section titled “Engine runtime and deployment”Two hard requirements:
- The engine is Rust — one self-contained binary, no runtime dependencies. This exercises the
replaceable-engine seam (“every layer is replaceable including the engine’s language”) and follows
the repo’s existing kernel drift (markdown/vault CST core, yaml-splice, suspenders-core, and the
SDF Tauri shell are already Rust). D-0006-typescript-substrate is unchanged for the op/entity
substrate: the engine invokes existing TS ops as child-process steps over the step protocol —
stdio JSON-RPC with a start / progress / heartbeat / result / cancel lifecycle, one contract for
engine↔runner, runner↔step, and runner↔agent-driver — and the
sdlcbinary with--output jsonis already a machine-contract step executor. - Contract types are Rust-first. Work orders, run records, events, and the DAG format are defined once as Rust structs and generated to TypeScript by ontogen — already in production here for three apps, with CI drift gates. The Rust definition is the source; the emitted TS is what every TypeScript consumer binds to. This settles the source-of-truth direction D-0017-ontogen-shared-codegen-convergence left open; that ADR’s convergence evaluation is neither adopted nor required by this choice, since ontogen is used as it already works rather than extended to consume Zod. The hand-projected parity fixture guarding today’s lease DTO retires as its types move behind the generator.
- The deployment target is a laptop / user PC. A local daemon (or foreground process) managed
by
sdlc enginelifecycle verbs or the SDF shell —sdlcwraps the engine and adapts it to the domain. That costs the engine no domain knowledge: the dependency runs one way, the same waysdlcalready consumes intersect and graph-scheduler. The engine also ships its own CLI — it is a standalone tool, usable by consumers that are not SDLC and by runners on other hosts;sdlc engineis a convenience over it, not its only door. No Docker, no cluster, no external database or broker — the substrate already guarantees the last. Taken seriously: idle footprint stays near zero (virtual actors — nothing resident but the daemon); machines sleep, so misfire/catch-up policy is core engine behavior, not a cron nicety; and github-ref-leases offline mode is load-bearing — a laptop is a partition waiting to happen. Distribution (migration step 8) means several user machines coordinating over the same ref authority, never a hosted control plane.
Engine/runner split. The runner is a library plus binary that walks one compiled DAG, asking
the graph-scheduler frontier for ready steps; the engine schedules and supervises many runners as
child processes. The runner alone gives the no-daemon one-shot path (sdlc workflow run <def>).
The engine core is an append-only event log with a deterministic reducer; the runner is
deterministic given step results; all nondeterminism is confined inside step implementations
(agents, network, filesystem).
Server deployment comes later and changes nothing here. The laptop is the harsher target — sleep, partitions, zero infrastructure — so an always-on host running the same binary is the easy case: one more peer over the same ref authority, with better misfire/catch-up characteristics. It is a deployment option, not a control plane; nothing in v1 may depend on one existing.
A read-only status API ships with engine v0, and SDF is the dashboard over it — a day-one
consumer, not a later add. The daemon serves a local read surface over the planes it already
holds: the frontier with per-filter explain(), active leases (actors, TTLs, scopes and hard
locks), run records, the trigger schedule, and the process registry — each kind: with the
registration record describing its execution. Runs cite their process and version, so the
dashboard joins any run back to the registration that produced it. Observation only — it mutates
nothing; change
enters through submission and scheduling. SDF’s event-stream contract already falls out of the
run-record decision; the status API supplies the rest of what it renders. Step 8’s network API is
distribution and control; this surface is read-only and local first.
The compiled DAG format is the authoring contract; surfaces are plural. The engine executes
one format — plain JSON: nodes, edges, trigger rules, retry/backoff policy — workflows as data,
versioned and diffable (the Step Functions precedent). Hosts only ever see compiled DAGs, and any
surface that compiles to it is admissible; later surfaces (other languages, a UI, an LLM emitting
the format directly) bind to the format, not to each other. TypeScript is the first: workflows are
defined against flowline’s typed API — settling B-QAHU; the parked spike on
origin/consolidate/workflow-library-full lands as the authoring kit, not the engine core.
Flowline is where the API’s shape gets exercised in practice, but it consumes the generated
contract types rather than defining them — authority sits with the Rust definition (above), which
is also where the engine reads it.
Flowline also supports in-code workflows — authored as ordinary TypeScript in a consumer’s codebase. The adaptation is at the boundary: code lowers to the DAG format before the engine sees it. The Temporal decline stands at the execution plane — durable run records are borrowed, code-replay is not; the engine executes data, never user code.
Pluggable seams — ports
Section titled “Pluggable seams — ports”The system is a set of contracts with swappable implementations behind them. Current choices were selected for cost and zero-infrastructure operation, not as architectural commitments. (This is the hexagonal “ports and adapters” pattern, but we say implementation for what sits behind a port — adapter already means a generated protocol surface, TM-0003-adapter.)
| Port | Today’s implementation | Prospective implementations | Contract the port fixes |
|---|---|---|---|
| Coordination / claims | git refs under refs/sdlc/… (lease/primitives.ts) | embedded store (SQLite/redb), Redis, Postgres, etcd/Consul | linearizable single-key CAS, TTL expiry, fencing token, namespace list |
| Run records / event log | lease payload + .sdlc/ files; engine v0: append-only JSONL journals, one per run | embedded append-only store, remote log | append-only, ordered, replayable |
| Work-order source | the planning corpus (markdown + frontmatter) | per-project stores: embedded DB, external tracker | level-triggered scan of minimal work orders (goal, kind:, links, scope claim, hints) into the frontier; ids owned by the source |
| Runner context | in-session agent skill | engine-supervised child process, container, remote runner | stdio JSON-RPC step protocol (start / progress / heartbeat / result / cancel) |
| Agent driver | Claude Code | other harnesses, local/OSS models | start(prompt, context) → event stream → result |
| Op adapters | CLI | MCP, HTTP | already generated from the registry (D-0007-deterministic-op-substrate, D-H7FS-op-substrate-surface) |
| Scheduling filters / sorters | built-in + sdlc.yaml policy | project-supplied predicates | ok / blocked(reason, retry-hint) (C-GUGX) |
| Process definitions | the implementation process | per-kind processes | the process registration contract above |
Never a port: the corpus — markdown + frontmatter on disk, git history as the audit log — and the contracts themselves. That is the bet (P-0007-long-lived-data-formats, P-0008-harness-agnostic-substrate); a pluggable system of record would forfeit it. The work-order source port does not touch this bet: it exists because the engine is domain-blind and another project may keep its work orders elsewhere — chosen once per project, not swappable per run. SDLC’s source is the corpus and stays so.
Two rules keep the seams honest:
- Design each port to its weakest implementation. Git offers linearizable CAS on one ref and no multi-key transaction; the coordination port therefore assumes exactly that, so richer backends implement it trivially and git remains viable forever. A port that assumed transactions would silently make git impossible to return to.
- A port earns its place only where a second implementation is genuinely in prospect and the contract is narrow enough to state on a page — the D-0007 op-granularity rule and the actor-boundary rule, applied to seams. Everything else stays a direct call.
The state-plane split is what makes coordination swappable at all: because execution state is not
corpus state (D-S30G-task-state-plane-split), replacing the coordination implementation never
touches the markdown substrate. github-ref-leases/README’s rejection of “local SQLite state”
and a “dedicated orchestrator API” stands as a rejection of those as the mechanism — chosen
against git’s zero-cost coordination — not as a prohibition on implementations behind this port.
Supersession and absorption map
Section titled “Supersession and absorption map”The map below is fully executed.
Completely superseded documents are deleted, not tombstoned — git
history is the archive; a superseded file left in the corpus is cruft that costs maintenance and
misleads readers. Deletion includes a link sweep: inbound wikilinks are re-pointed here. No
supersedes: frontmatter is written — this map and the deletion commits carry the lineage.
Amended and absorbed documents stay — they carry live content.
| Entity | Verdict |
|---|---|
| D-K9PX (system architecture) | deleted — current-state map folded into this document |
| D-0015 (Dark Factory target architecture) | deleted — target map, contracts, and migration arc folded in |
| D-EQGC (task state planes) | deleted — superseded by D-S30G’s substance, adopted here |
| D-S30G-task-state-plane-split | open/accepted — its state-plane substance is normative; the staged migration collapses into the single cutover (see Migration) |
| D-OIVM (task kinds and runners) | deleted — kind: routing, the kind roster, and the leaf-only rule absorbed here |
| D-XKA8 (work/process planning planes) | deleted — vocabulary, plane split, and work-plane lifecycle policy absorbed here |
github-ref-leases/README | referenced-authoritative — the coordination substrate, unchanged |
| D-0006-typescript-substrate | amended — scope bounded to the op/entity substrate; the engine is Rust |
| D-0007-deterministic-op-substrate / D-H7FS-op-substrate-surface | referenced-authoritative — ops are the executable leaf |
D-BPD8-graph-scheduler-api/README | referenced-authoritative — the frontier math |
| D-Q2WR-task-pickup-order | referenced — becomes the SDLC policy layer over the scheduler |
| D-HRNS-harness-model-and-exporters | referenced — agent-host portability |
| D-7F2M-why-what-verify-chain | referenced — planning ontology, orthogonal to execution |
Consolidation over invention. Every load-bearing piece exists — leases, ops, frontier math, intersect, the process-registry idea, the state-plane split — but scattered across two architecture maps, three names for the same concept, and one spike with no decision. The measurable pain (the definition-gap rate, the prose runner, the unreconciled dual state machines) traces to the missing unification, not to any missing capability.
Prior art
Section titled “Prior art”What each established system settles for this architecture, and what we deliberately decline.
Scheduling-library prior art (Airflow trigger rules, Kubernetes filter/score, Buck2 DICE) is
already mined in docs/planning/graph-scheduler-research.md and D-BPD8; internal prior art is the
flowline use-case survey (docs/planning/analysis/flowline-use-cases/). This table covers the
system shape.
| System | Model | We borrow | We decline |
|---|---|---|---|
| Temporal / Cadence | Durable execution: workflow code replayed from an event-history log; activities as side-effecting leaves; workers poll task queues; declarative retry/timeout policy | Event-sourced run records and deterministic replay for resume (the engine’s log + reducer); the workflow/activity split — our LLM steps are bounded activities, never the host | A central service + database as source of truth; code-as-workflow (we compile declarative DAGs) |
| AWS Step Functions | JSON state-machine DSL executed by a managed service | The compiled-workflow-format precedent: workflows as data, versioned and diffable | Vendor runtime |
| Erlang/OTP | Processes, mailboxes, supervision trees; let-it-crash | Supervision semantics — TTL expiry and steal-on-expired as restart; one process per serial domain (corpus-writer) | In-memory state as truth |
| Akka / Pekko | JVM actor framework; cluster sharding; event-sourced persistence | Single-activation placement and persisted-actor patterns, as validation of lease-as-identity | Framework adoption (Akka relicensed to BSL in 2022; Apache Pekko is the fork) |
| Microsoft Orleans | Virtual actors (“grains”): identity ≠ activation; runtime-guaranteed single activation; state persisted outside the activation | The entire virtual-actor reading of the execution plane — identity is a name plus durable state, activation on demand | The resident cluster runtime and RPC surface |
| Cloudflare Durable Objects / Dapr actors | Id-addressed single-writer objects with attached storage | Proof the virtual-actor model works without a resident cluster | Platform coupling |
| Kubernetes | Level-triggered reconcile controllers; scheduling as filter → score; CronJob concurrencyPolicy (Allow / Forbid / Replace) and startingDeadlineSeconds | Reconciler-as-controller for the stubbed reconcile detectors; CronJob’s overlap and misfire vocabulary for the recurrence open question | The API-server / etcd centrality |
| Airflow / Dagster / Prefect | Cron + sensor scheduling over DAGs; catchup / backfill | Sensors as the watch-predicate trigger source; catchup/backfill semantics for misfire policy | A scheduler database as system of record |
| Bazel / Buck2 | Action graph; remote-execution workers; persistent worker processes | The persistent-worker protocol as runner-process prior art (the step protocol) | Content-addressed hermeticity as a requirement |
| Job queues (SQS, Sidekiq, BullMQ) | At-least-once delivery; visibility timeouts; idempotent consumers | Visibility timeout ≡ lease TTL; idempotency + fencing as the delivery contract | A broker as an infrastructure dependency |
| CI runners (GitHub Actions, Buildkite) | Declarative workflow files; registered runners poll a control plane; event triggers | The register-then-poll runner topology for the distribution step; repo events as a trigger source | YAML as the authoring surface |
| Manufacturing MES / job-shop | Work orders routed through registered routings; operations as steps | The work-order / process vocabulary — already adopted | — |
The composite is deliberate: Temporal’s durable runs, Orleans’ virtual identity, OTP’s supervision,
Kubernetes’ reconcile loops and filter scheduling, Airflow’s trigger vocabulary — each adopted as
semantics over the substrate we already have. What none of them supply — coordination with no
server, no broker, and no database, over git refs and markdown — is the part this system has
already built and shipped (github-ref-leases/README). Prior art informs every layer above the
substrate; the substrate itself is the novel bet.
Consequences
Section titled “Consequences”- Lifecycle skills shrink to heads; the skill-prose lint retires at cutover, when process steps become engine-executed.
- The task readiness contract narrows to the implementation process; scope declaration flips from gate to incentive.
- SDF’s event-stream contract falls out of the run-record decision here.
Agent/WorkSessionentities stay deferred: actor state is runtime, not corpus.- The flowline TS spike becomes the workflow-authoring kit: workflows defined in its typed API, compiled to the engine’s DAG format, its types driving the engine’s API definition. B-QAHU is closed; the parked implementation lands behind that contract (implementation-ledger E5).
Migration
Section titled “Migration”Full cutover, not strangler-fig. The prose orchestrator is parked — brittle, not running — so there is nothing to keep live: the engine replaces it rather than runs beside it, and every week before engine v0 is a week without an orchestrator at all. That sets the priority: the shortest path to a working tick. Alpha buys the cutover — no external consumers, and the corpus carries over as-is; work orders are durable markdown, so migration is schema-level frontmatter work, not data movement. The order below is a build order, not a sequence of live cutovers.
The work splits into two tracks that meet at the cutover: the engine, a general work runner
knowing nothing about SDLC, and SDLC’s own reshaping into the first instance that runs on it —
work orders slimmed to the floor, the lifecycle policy registered as recurring processes, the
corpus migrated. Neither is optional; an engine with nothing to dispatch is a demo, and minimal
work orders with no runner are a downgrade. docs/planning/d-vsli-implementation-plan.md is the
working ledger and carries both tracks phase by phase.
- Accept vocabulary + supersession map.
- State planes (D-S30G) built into the engine; the expand/cutover/contract staging collapses into the single cutover.
- Process registry v1: today’s implementation contract becomes the
implementationprocess. - graph-scheduler built to v0.1 (its full ready roster, not a frontier subset), then the
scheduling filter chain over it;
task nextrebased as the policy layer. - Scope claims + intersect conflict filter (advisory default, hard-lock tier).
- Engine v0 (Rust): dispatcher tick, long-lived system actors (corpus-writer first), read-only
status API; SDF renders it. Built by salvage — the supervisor/worker/agent-delegation engine
in
apps/ontological-legacy/src-tauri/src/engine/is the working shape; it is ported onto the coordination port (its SQL-store claim gives way to lease CAS) and freed of its host app, changed as freely as the port demands. - Cutover: the engine takes the tick; the parked orchestrator skills retire.
- Engine API goes network; runners on other hosts.
Out of scope
Section titled “Out of scope”Planning-plane ontology (D-7F2M), harness exporters (D-HRNS), per-component decisions in referenced
ADRs, and any change to the human merge gate — the auto-merge tension in vision.md is flagged, not
decided here.
Open questions
Section titled “Open questions”- Engine distribution:
sdlcships as one JS npm artifact (D-0014-cli-primary-npm-distribution) and the engine is a Rust binary, so today a runtime boundary sits between them — per-platform optional dependencies, a postinstall fetch, a Tauri sidecar as the workbench already does, or SDF-only. A Rust rewrite of thesdlcCLI is anticipated, which would collapse this into an ordinary cargo-workspace question; the interim answer should not outlive that. - Read vs. write scope modes (shared/exclusive) — deferred from scope-claim v1.
- Work-order ingress API: a network surface that accepts incoming jobs and mints work orders riding the normal loop — engine-hosted duty vs. separate ingress service, its auth story, and its relation to migration step 8. Submission plane only; never a scheduler.
- Nested decomposition guardrails: depth and fan-out limits, cost containment.
- Cross-project federation: the addressing registry for packages and peer projects (workspace
graph in-repo; a peer roster cross-repo), the message taxonomy (adopt-api-change, fix-bug as
addressed work-order kinds), and the work-order-via-PR delivery contract generalizing
spawn-task-pr. - Federated planning corpora: per-package planning roots inside the monorepo (the package mailbox made literal), union corpus loading, cross-corpus id uniqueness and wikilink resolution, and repo-level rollups derived rather than hand-maintained.
- Coordination port contract: the exact CAS/TTL/fencing surface extracted from
lease/primitives.ts, how an implementation is selected (sdlc.yamlvs. engine flag), and whether offline mode belongs to the port or to each implementation.
References
Section titled “References”github-ref-leases/README· D-S30G-task-state-plane-splitD-BPD8-graph-scheduler-api/README·D-4FRD-intersect-api/README· D-Q2WR-task-pickup-orderdocs/planning/graph-scheduler-research.md·docs/planning/analysis/flowline-use-cases/·docs/planning/analysis/2026-08-01-task-corpus-audit.md