Skip to content

System architecture — the distributed work runner

Status: open/accepted

  • 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.

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).

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 lease phase (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.

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:

ConsequenceEffect
Both live in the general package tierspackages/rust/foreman and packages/ts/flowline, never under solutions/ontological/
Contract types are domain-neutralA work order is goal + acceptance + kind: + links + optional scope; nothing task-shaped leaks in
A second instance must be conceivable at all timesIf 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.

TermMeaningReplaces / absorbs
Work orderDurable, minimal job submission; Task is the primary type”work order” (kept)
ProcessVersioned, registered definition of how a kind executes”runner registry” entry, “process”, “workflow definition”
RunOne execution of a process against a work order
RunnerThe component executing a run (agent session today; engine-supervised child later)“workflow runner” (kept)
EngineLong-lived host: scheduler, supervisor, event log”engine” (kept)

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.

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.

VariantShipsNotes
Built-inv1Compiled into the engine; the handler registry the salvaged worker already has
Shell commandv1The floor — a registration is allowed to be one command line, with no plugin machinery in the way
Child process over the step protocolv1stdio JSON-RPC; sdlc --output json is the standing example
Agent stepv1The judgment port, through the agent-driver port
WASM pluginlaterSandboxed 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.

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():

FilterSource
Dependency satisfactionbuilt-in trigger rule
Kind has a registered processdispatch predicate (process registry above)
Leaf-only — containers are structural rollupsorchestrator-selection rule (process registry above)
In-flight capssdlc.yaml orchestrator caps, as counting filters
Scope conflictpairwise intersect vs. active lease scopes
Scoped concurrency policiessdlc.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.

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.

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.

Adopted as semantics, not a runtime — no actor framework:

Actor conceptExisting primitive
Identitylease ref (refs/sdlc/tasks/<id>)
Single activationCAS claim + fencing tokens
Activation / deactivationdispatch → run → exit at PR open; reacquire is reactivation
Persisted statelease payload + worktree + (later) event log
Behaviorthe registered process for the work order’s kind
SupervisionTTL expiry, steal-on-expired, reconcile
Messagesstdout 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).

ThingGetsWhy
Work orderactor identity (lease ref)lifecycle, strategy, ownership
Runidentity in the execution recordbeginning, end, outcome
System duty (dispatch, reconcile, corpus writes, PR shepherding)long-lived actor identityexclusive duty over a serial domain
PR under shepherdinglightweight virtual identity (cursor)per-identity state, op-supplied behavior
Package / peer projectdurable identity + mailbox (its work-order corpus)has a contract, lifecycle, and consumers; acts only through work orders addressed to it
Recurring jobdurable identity (registration + operation lease + run history)schedule is data; ticking is one system duty; each firing is a run
Directory / scopeclaim in a lease + policy entrycontended but passive
Filenothingdata

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.

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 sdlc binary with --output json is 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 engine lifecycle verbs or the SDF shell — sdlc wraps the engine and adapts it to the domain. That costs the engine no domain knowledge: the dependency runs one way, the same way sdlc already 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 engine is 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.

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.)

PortToday’s implementationProspective implementationsContract the port fixes
Coordination / claimsgit refs under refs/sdlc/… (lease/primitives.ts)embedded store (SQLite/redb), Redis, Postgres, etcd/Consullinearizable single-key CAS, TTL expiry, fencing token, namespace list
Run records / event loglease payload + .sdlc/ files; engine v0: append-only JSONL journals, one per runembedded append-only store, remote logappend-only, ordered, replayable
Work-order sourcethe planning corpus (markdown + frontmatter)per-project stores: embedded DB, external trackerlevel-triggered scan of minimal work orders (goal, kind:, links, scope claim, hints) into the frontier; ids owned by the source
Runner contextin-session agent skillengine-supervised child process, container, remote runnerstdio JSON-RPC step protocol (start / progress / heartbeat / result / cancel)
Agent driverClaude Codeother harnesses, local/OSS modelsstart(prompt, context) → event stream → result
Op adaptersCLIMCP, HTTPalready generated from the registry (D-0007-deterministic-op-substrate, D-H7FS-op-substrate-surface)
Scheduling filters / sortersbuilt-in + sdlc.yaml policyproject-supplied predicatesok / blocked(reason, retry-hint) (C-GUGX)
Process definitionsthe implementation processper-kind processesthe 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.

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.

EntityVerdict
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-splitopen/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/READMEreferenced-authoritative — the coordination substrate, unchanged
D-0006-typescript-substrateamended — scope bounded to the op/entity substrate; the engine is Rust
D-0007-deterministic-op-substrate / D-H7FS-op-substrate-surfacereferenced-authoritative — ops are the executable leaf
D-BPD8-graph-scheduler-api/READMEreferenced-authoritative — the frontier math
D-Q2WR-task-pickup-orderreferenced — becomes the SDLC policy layer over the scheduler
D-HRNS-harness-model-and-exportersreferenced — agent-host portability
D-7F2M-why-what-verify-chainreferenced — 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.

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.

SystemModelWe borrowWe decline
Temporal / CadenceDurable execution: workflow code replayed from an event-history log; activities as side-effecting leaves; workers poll task queues; declarative retry/timeout policyEvent-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 hostA central service + database as source of truth; code-as-workflow (we compile declarative DAGs)
AWS Step FunctionsJSON state-machine DSL executed by a managed serviceThe compiled-workflow-format precedent: workflows as data, versioned and diffableVendor runtime
Erlang/OTPProcesses, mailboxes, supervision trees; let-it-crashSupervision semantics — TTL expiry and steal-on-expired as restart; one process per serial domain (corpus-writer)In-memory state as truth
Akka / PekkoJVM actor framework; cluster sharding; event-sourced persistenceSingle-activation placement and persisted-actor patterns, as validation of lease-as-identityFramework adoption (Akka relicensed to BSL in 2022; Apache Pekko is the fork)
Microsoft OrleansVirtual actors (“grains”): identity ≠ activation; runtime-guaranteed single activation; state persisted outside the activationThe entire virtual-actor reading of the execution plane — identity is a name plus durable state, activation on demandThe resident cluster runtime and RPC surface
Cloudflare Durable Objects / Dapr actorsId-addressed single-writer objects with attached storageProof the virtual-actor model works without a resident clusterPlatform coupling
KubernetesLevel-triggered reconcile controllers; scheduling as filter → score; CronJob concurrencyPolicy (Allow / Forbid / Replace) and startingDeadlineSecondsReconciler-as-controller for the stubbed reconcile detectors; CronJob’s overlap and misfire vocabulary for the recurrence open questionThe API-server / etcd centrality
Airflow / Dagster / PrefectCron + sensor scheduling over DAGs; catchup / backfillSensors as the watch-predicate trigger source; catchup/backfill semantics for misfire policyA scheduler database as system of record
Bazel / Buck2Action graph; remote-execution workers; persistent worker processesThe 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 consumersVisibility timeout ≡ lease TTL; idempotency + fencing as the delivery contractA broker as an infrastructure dependency
CI runners (GitHub Actions, Buildkite)Declarative workflow files; registered runners poll a control plane; event triggersThe register-then-poll runner topology for the distribution step; repo events as a trigger sourceYAML as the authoring surface
Manufacturing MES / job-shopWork orders routed through registered routings; operations as stepsThe 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.

  • 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 / WorkSession entities 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).

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.

  1. Accept vocabulary + supersession map.
  2. State planes (D-S30G) built into the engine; the expand/cutover/contract staging collapses into the single cutover.
  3. Process registry v1: today’s implementation contract becomes the implementation process.
  4. graph-scheduler built to v0.1 (its full ready roster, not a frontier subset), then the scheduling filter chain over it; task next rebased as the policy layer.
  5. Scope claims + intersect conflict filter (advisory default, hard-lock tier).
  6. 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.
  7. Cutover: the engine takes the tick; the parked orchestrator skills retire.
  8. Engine API goes network; runners on other hosts.

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.

  • Engine distribution: sdlc ships 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 the sdlc CLI 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.yaml vs. engine flag), and whether offline mode belongs to the port or to each implementation.
  • github-ref-leases/README · D-S30G-task-state-plane-split
  • D-BPD8-graph-scheduler-api/README · D-4FRD-intersect-api/README · D-Q2WR-task-pickup-order
  • docs/planning/graph-scheduler-research.md · docs/planning/analysis/flowline-use-cases/ · docs/planning/analysis/2026-08-01-task-corpus-audit.md

← Back to Decisions