Implementation ledger — the work runner, and SDLC on it
State: closed/archived · Genre: plan
The working ledger for D-VSLI-distributed-work-runner-architecture: two tracks that meet at one cutover.
Track E builds the engine — a general work runner that knows nothing about SDLC. Track C reshapes SDLC into the first thing that runs on it. Neither is optional: an engine with nothing to dispatch is a demo, and a corpus of minimal work orders with no runner is a downgrade. They are separable in time because they touch different trees, and they are separable only until cutover.
Starting point: no running orchestrator, prose lifecycle skills, a shipped lease substrate. Full cutover, no strangler staging; the priority is the shortest path to a working tick.
How to use: each session picks the topmost unblocked phase in either track, checks items off as they land, and splits rows in place when work turns out bigger than one line. This file tracks state across sessions; keep it current in the same PR as the work it describes.
Starting state (definition of done)
Section titled “Starting state (definition of done)”- The engine runs as a local daemon (or foreground process) on a laptop and survives sleep/wake.
- One implementation work order flows end to end with no prose orchestrator: submitted, scheduled through the filter chain, claimed via lease, executed in a worktree by the agent driver, recorded, PR opened.
- Work orders carry the minimal shape; spec depth is the routed process’s contract, not a universal template.
- Corpus hygiene runs as a registered recurring process, not a standing task.
- A workflow authored in flowline TypeScript compiles to the engine’s DAG format and the engine executes it.
- SDF renders the read-only status API live: frontier with per-filter
explain(), active leases (including hard locks), run records, trigger schedule, process registry. - The parked orchestrator skills are retired; the skill-prose lint retires with them.
Decision gates (decided 2026-08-16)
Section titled “Decision gates (decided 2026-08-16)”The gates that blocked build work are decided; the ADR carries each in its section. Recorded here for the builder’s quick reference.
| Gate | Decision |
|---|---|
| Contract source of truth | Rust-first: types defined as Rust structs, generated to TS by ontogen (already in production here, drift-gated). Flowline consumes the generated types |
| Domain agnosticism | Engine and flowline know work scheduling only — no SDLC entity, no corpus schema, no substrate import. SDLC is one adapted instance behind the ports |
| Component placement | packages/rust/foreman and packages/ts/flowline — the general tiers, never under solutions/ontological/ |
| graph-scheduler scope | Build v0.1 in full (its whole ready roster), not a frontier subset |
| Scheduler language | Rust, at packages/rust/graph-scheduler — the engine calls the frontier in-process. D-BPD8’s semantics stand. No TS peer is built; sdlc task next keeps its own frontier until the CLI is Rust |
| Engine v0 build mode | Salvage the legacy supervisor/worker/agent engine, changed as freely as the coordination port demands |
| D-XKA8 / D-OIVM disposition | Delete both, surviving detail folded into D-VSLI — executed |
| Message protocol | stdio JSON-RPC step protocol; stdout markers never formalized |
| Event store | Append-only JSONL journals, one per run, under .sdlc/; embedded DB stays a port option |
| Work-order source | Level-triggered scan; ids owned by the source; in-code registrations are submission sugar |
| Hard-lock strictness | Declared intersections by default; per-lock strict flag also holds unscoped work |
| Per-process human gates | Declared per registration; implementation keeps the PR-merge gate; deterministic duties gateless; content-bearing output via PR |
| Recurrence | Skip overlapping firings; coalesce missed firings into one catch-up run on wake; no jitter; declarations in process registrations |
| Backends | Run records and coordination stay independent ports (git refs + local journals) |
Still open, deferred past the starting state: read/write scope modes, work-order ingress API, nested decomposition guardrails, cross-project federation, federated planning corpora, coordination port contract details, engine distribution.
Phase 0 — decisions and corpus execution
Section titled “Phase 0 — decisions and corpus execution”Complete.
- Accept D-VSLI; Summary populated with outcome bullets per the decision contract.
- Fold surviving D-XKA8 / D-OIVM detail into D-VSLI, execute the supersession map’s deletions (D-K9PX, D-0015, D-EQGC, D-XKA8, D-OIVM), and re-point inbound wikilinks.
- Amend D-0006 (scope bounded to the op/entity substrate; engine is Rust).
- Close B-QAHU (
promoted/decision→ D-VSLI; flowline lands in E5). - Correct D-S30G to
open/accepted; accept D-BPD8 with the Rust implementation noted. - Retire the
packages/ts/graph-schedulerscaffold; re-point PR-DZTZ-graph-scheduler at the Rust location; close B-CADO. - Regenerate docs artifacts.
Track E — the engine
Section titled “Track E — the engine”E1 — contracts
Section titled “E1 — contracts”Types before code, authored as Rust structs and generated to TypeScript by ontogen. Every type here is domain-neutral: nothing task-shaped or corpus-shaped enters the engine’s vocabulary. E1 unblocks the whole track.
Emitter constraints — established by spike, not inferred. Several of these produce silently wrong TypeScript rather than an error, so they are authoring rules rather than preferences.
| Constraint | Why |
|---|---|
#[serde(tag = "…")] is a hard error. Keep the tagged Rust enum and hand-write only its TS via #[ts_opaque(target = "…")] | tag/content/untagged/flatten are rejected at container level before any variant renders. ts_opaque still emits the payload structs, so only the union line is hand-maintained, and exhaustive Rust match plus serde error quality both survive |
Write uuid::Uuid and chrono::DateTime<chrono::Utc> fully qualified | Bare idents emit as dangling TS names. The config that would fix it is unreachable — ontogen hardcodes EmitConfig::default() |
No #[serde(flatten)] in contract types | Silently ignored at field level: emitted TS keeps the nesting the wire flattens away |
No case-changing rename_all on an enum with struct variants | It leaks into the variant’s field names, so the TS disagrees with the real wire. Prefer newtype variants over named payload structs |
| No generics | Paged<T> emits T as a dangling name and reference sites lose the instantiation — silently |
| Flat, globally-unique type names | Cross-module collisions are a hard error, and #[ts_name] is unusable in 0.1.0/0.1.1, so the only fix is renaming the Rust type |
Pair skip_serializing_if with default | Otherwise the field emits required while the wire omits it |
Avoid u64 above 2^53 | bigint_behavior defaults to Number and is not configurable through ontogen, so large values truncate silently |
Consequence for the scouted shapes: StepDef, RunEvent and TriggerCause
are all tagged unions needing the ts_opaque treatment, and RunRecord and
the lease payload carry Uuid / DateTime fields that must be fully
qualified.
The crate is packages/rust/foreman (PR-FRMN-foreman) at
layer: 'library'. Library rather than tool despite shipping a binary: that is
exactly determined-platform’s shape — an importable crate that also ships
platform-server — and every crate under packages/rust/ uses library. Both
layers are picked up by CI, which excludes only application.
- Work-order payload type: goal, acceptance,
kind:, upward links, optional scope claim and scheduling hints. - DAG format v0: plain JSON — nodes, edges kept separate so a rewiring
diff touches only the edge list, trigger rules, retry/backoff — versioned
and diffable. Node status is exactly graph-scheduler’s five-value enum
per
D-BPD8-graph-scheduler-api/README, so E3 meets no impedance mismatch. - Run-record and event types; each run cites its work order, its process,
and the process version. Events reuse the step-protocol payload structs,
so there is one definition of “a step reported progress” and the journal
cannot drift from the wire. The JSONL store under
.sdlc/is E6 — these are the types it writes. - Coordination port contract extracted from
lease/primitives.ts: single-key CAS, untyped payloads, opaque version, authority bound at construction, single deterministic attempts. TTL expiry and fencing are theLeasepayload layered above the port, not port operations. - Work-order source port contract: level-triggered scan of minimal payloads into the frontier; ids owned by the source.
- Step protocol — start / progress / heartbeat / result / cancel over
newline-delimited JSON on stdio, one envelope symmetric in both
directions. Deliberately not literally JSON-RPC 2.0: the emitter
rejects internally-tagged enums, so a
methodfield would sit beside an untypedparams— two discriminators that can disagree. The externally tagged payload key is one discriminator that cannot disagree with itself. - Crate scaffolded at
packages/rust/foremanwith itsmoon.yml(layer: 'library',merge: 'replace', hand-declareddependsOn). - ontogen wired via
build.rsemittingpackages/rust/foreman/generated/; drift gate registered asforeman:codegen-drift. Since ontogen emits only the closure reachable from anapi/v1signature, the read-only status API doubles as the emission root. Where the output finally lands moves with its first consumer —packages/ts/flowlinedoes not exist yet. - Retire the hand-projected lease parity fixture. Not E1’s to close:
that fixture guards SDF’s read model of SDLC’s own
TaskLifecycleLease, andforeman::lease::Leaseis a different, domain-neutral type. It retires with the SDLC adapter in C4, not here.
E2 — state planes
Section titled “E2 — state planes”D-S30G’s substance, built in rather than staged: lease phase is authoritative execution state, frontmatter status a derived cache. The ADR’s seven-stage expand → cutover → contract migration collapsed into one change, as D-VSLI’s Migration section directs — with the orchestrator parked there was no live system to stage around, so what landed is the end state, not a window.
- The plane split.
in-progress[/blocked]is never written to frontmatter again. Promotion (planning/*→open/ready+ the readiness stamp) and closure are the only per-run commits on main; the verify stamp, the start commit and the mid-flightprs[]append are gone, andappend_pr_url.ts— the one writer that mutated the shared main checkout — is deleted with them. Per-run verification writes leasegatesinstead. AbsorbsT-5LP4,T-IVEJ,T-S27R,T-2T0RandT-AQR9;T-5G81’s payloadgateswere already in place. - Readers went lease-aware.
probe-stategainedlease_present/lease_phase/lease_pr_numberand recomputedresume_candidateoff the lease phase — resume detection no longer hangs on a commit-subject literal at main’s HEAD, which broke whenever any later commit landed.inflightprefers lease phase over frontmatter.readiness_verified_atandmain_head_is_verify_stampsurvive as legacy output fields only;T-11QZretires them with schema v6. - The readiness contract binds leaves.
task gap-reportreportsparenthoodas a fact and the marker carriesparent=; a parent task is an organizational rollup, exempt because its readiness is its children’s (T-ARCR). NoteT-C83Hspecifies the same feature with an incompatible design (suppress the other slices, different marker) — it needs closing as superseded. - The reconciler is a level-triggered controller.
reconcile.tsis no longer a stub: seventeen real detectors over both planes, plus the loop that drives frontmatter toward what the lease says. Observe → detect → converge → observe again, bounded, withobserve/applyinjected so convergence is testable without git. Two detectors carry remedies —frontmatter-status-drifts-from-lease(a live lease disagrees with the cache) andtasks-in-progress-without-lease(the crash-stale lie D-S30G names) — and both resolve to the same write. Everything else reports only: closure needs acompletion_noteno machine can invent, and an expired lease needs a steal, not a frontmatter edit.sdlc lease reconcile --applyruns it; repairs land in the working tree and mint no lifecycle commit, because repairing a cache is not one of D-S30G’s four semantic transitions.RECONCILE_STUBBEDis false and the honest-signal guard now fires off the live table, so re-emptying it still cannot read as a clean bill of health. - Not done here. Schema v6 (
T-11QZ) — deprecating thein-progress*enum values and retiring the legacy probe fields. The controller’s whole job is healing files that still carry them, so the enum has to outlive the sweep that removes them from the corpus.T-YQY2(task nextlease exclusion) is also untouched: it is a dispatch-plane filter that belongs with E3’s filter chain. And the control plane’smin_client_versionstill needs its bump — the firstgateswriter exists now, so an old client can write a payload the new readers expect to carry gates. - Two open judgment calls the specs never made. A readiness FAIL
against a LEASED task still commits its downshift to main, which is
right by T-5LP4 AC-6 and T-S27R AC-3 but sits oddly beside “a leased run
writes the execution plane only”. And
promote-readylabels the gap-clearing commit on an already-open/readytask, so the subject says “promote” when no status moves; aclear-definition-gapaction would read truthfully.
E3 — scheduling and scope claims
Section titled “E3 — scheduling and scope claims”- Re-spec the nine ready gs tasks onto Rust. Not eleven: T-N2EJ
(package exports) and T-G0D2 (documentation site) are
closed/supersededand T-PDDK (the TypeScript scaffold) isclosed/done. All nine targeted the deletedpackages/ts/graph-scheduler, so this was a re-specification rather than a path swap — subpath exports became Rust modules and Cargo features,package.jsonexports work disappeared, and T-ROJC’s corpus became Rust data tables. Capability bodies were re-pointed at the same time; theirlocations[]needed no correction, having already been swept empty. - Crate scaffold and the executable spec
(T-ROJC-gs-test-corpus-and-disabled-suites).
packages/rust/graph-schedulerexists atlayer: 'library'on serde + serde_json, with the five v0.1 modules stubbed at their real signatures and 63 fixtures transcribed as ignored suites over Rustconstdata tables — no JSON corpus and noinclude_str!, since a conformance corpus with one implementation has nothing to conform to.cargo test -p graph-schedulerreports zero failures and zero non-ignored tests;-- --ignored --listis the remaining spec. Three sketch signatures the Rust type system rejected are corrected in their tasks: a closureStatusSourceneeds a newtype (a blanketFnimpl overlaps everything),ReadyOptionsneeds builder setters (a#[non_exhaustive]struct admits no functional update outside its crate), and both sorter constructors take the graph (downstream propagation walksdescendants, andcomparehas no graph). - Build the five dispatcher layers over that scaffold —
graph,status,ready,schedule,sorters— each task un-ignoring its own suite. Not the full eight-layer roster. This is the phase’s bulk. - Filter chain in front of the frontier: filters return
ok/blocked(reason, retry-hint);explain()powered by the same returns. - Filters: dependency satisfaction, kind-has-registered-process, leaf-only,
in-flight caps, scope conflict (pairwise intersect vs. active lease
scopes), scoped concurrency policies (intersect
index()). - D-Q2WR sort chain supplied as sorters.
- Rust peer for intersect at
packages/rust/intersect— the engine now calls the scope filters in-process for the same reason the frontier is in-process. 69/69 conformance against the six JSON fixture files both tiers share, plus a 30×30 pattern matrix diffed byte-identical across tiers. The corpus is referenced rather than copied (the TS package is a published mirror that reads its ownfixtures/), which costspublish = falseon the crate. Moon ids follow the twin precedent: the Rust crate takes bareintersect, the TS project becomesintersect-ts. Thin spot:fsparity rests on 3 corpus cases, and tinyglobby vs walkdir could still diverge on symlinks. - Scope claims: touchpoint parse/resolve → pattern set → stamped into the lease payload at claim.
- Hard-lock escalation tier:
enforcement: mandatoryclaims held as leases (TTL, heartbeat, steal-on-expired); declared intersections by default, per-lockstrictflag also holds unscoped work.
Crate name and location. packages/rust/graph-scheduler, Cargo package
graph-scheduler, bare derived moon id graph-scheduler with no id: declared.
Per S-0009-moon-project-ids the Cargo alias claims the bare name whatever id
is given, so a derived bare id keeps the project’s two names in agreement. The
-ts twin rule is not engaged: no TypeScript peer is built, and the deleted
packages/ts/graph-scheduler freed the name.
v0.1 is the dispatcher critical path, not the full roster. Six tasks stay
open/ready and are what E6 needs to dispatch its first real work order. Three
move to planning/backlog — a real status meaning triaged and parked, so they
are not pickable — and each states its deferral in its body. Nothing keeps citing
a deleted TypeScript package either way.
| Layer | Task | v0.1 |
|---|---|---|
| Crate scaffold, corpus, ignored suites | T-ROJC-gs-test-corpus-and-disabled-suites | yes |
graph — immutable DAG core | T-JVXC-gs-dag-core | yes |
status — status enum, trigger rules, StatusSource | T-CLV1-gs-status-model-and-trigger-rules | yes |
ready — the push frontier | T-I3QP-gs-push-readiness-frontier | yes |
schedule — filters, verdicts, explain | T-7OJL-gs-eligibility-filters-and-explain | yes |
sorters — ordering | T-BR6H-gs-priority-sorters | yes |
goal — goal-directed frontier | T-8L0G-gs-goal-directed-frontier | deferred |
tracker — stateful wrapper | T-EJ3P-gs-stateful-tracker | deferred |
pull — demand-driven resolution | T-0WNK-gs-pull-resolution | deferred |
T-ROJC absorbs the crate scaffold, because the TypeScript scaffold it used to depend on was deleted with the package and no scaffold task remains.
The DAG contract types: NodeStatus and TriggerRule move down, and nothing
else does. graph-scheduler becomes their owner; foreman depends on the crate,
deletes its definitions, and re-exports both from foreman::dag so
foreman::dag::NodeStatus keeps resolving and its public API survives. This ends
a duplication foreman’s own doc comment conceded — it called NodeStatus
“exactly the graph-scheduler status enum, and deliberately so”.
The rest of foreman::dag stays where it is, and the reason is a hard cycle
rather than a preference. DagNodeDef (foreman/src/dag.rs:51) carries
pub step: StepDef at line 56. StepDef is the work runner’s execution
vocabulary — a compiled-in handler key, a command line, a child process speaking
the step protocol, an agent driver — so it belongs to foreman. Moving
DagNodeDef would make graph-scheduler name StepDef and depend on foreman
while foreman depends on graph-scheduler, which Cargo rejects. DagDef,
DagNodeDef, DagEdgeDef, RetryPolicy, BackoffPolicy and FailurePolicy
therefore stay.
They are also not duplicated. D-BPD8’s core is an in-memory Graph built by
add_node / depends_on and queried; DagDef is a serialized wire format
carrying retry, timeout and step payloads. Only the two enums are the same type
in both designs.
The ontogen emit stays in foreman, over the imported types. ontogen’s
long-tail walker is a syntactic syn::Item scan of source directories, and
ClientsConfig::pool_extra_roots exists for exactly this case — its in-source
comment describes the extra roots as “workspace-sibling crates the consuming
crate re-exports types from”, with relative paths joined to
CARGO_MANIFEST_DIR. So foreman/build.rs adds
pool_extra_roots: vec!["../graph-scheduler/src".into()] and keeps emitting one
generated/bindings.ts behind one drift gate.
Moving the emit was rejected on two grounds: ontogen has no types-only generator
— TypeScript is emitted only for the closure reachable from an api/v1 service
signature — so graph-scheduler would need a fabricated status surface to root an
emit it has no use for, and it would take ontogen and biome-fmt build
dependencies into a crate meant to stay dependency-light. tests/codegen_sync.rs
is what makes this safe to attempt: it asserts the emitted NodeStatus union
text verbatim, so a pool that fails to reach the moved enums fails the test
rather than emitting incomplete bindings.
Two consequences worth recording. graph-scheduler carries serde and
serde_json, so D-BPD8’s “zero-dep” claim becomes dependency-light — the same
position packages/rust/intersect holds with thiserror. serde is
deliberately not an optional feature, because the derives would then sit behind
#[cfg_attr] and ontogen’s syntactic walker reads derive and serde attributes
straight from source text, so a gated rename_all could emit variants that
disagree with the wire and raise no error. And the shared-corpus pattern does
not transfer: intersect shares one JSON corpus because it has two tiers to
keep honest, and graph-scheduler has none, so its fixtures are Rust const data
tables — kept as data so a JSON projection stays cheap if a twin ever arrives.
- Open, needs a human call. Whether the split above is the right line, or
whether the DAG definition types should also move by some means that
breaks the cycle — a node payload generic in graph-scheduler that foreman
instantiates, or a third crate holding the shared vocabulary. The generic
option is constrained: E1’s emitter rules forbid generics in any type the
emit reaches, because
Paged<T>emitsTas a dangling name silently.
E4 — process registry v1
Section titled “E4 — process registry v1”- Registration contract keyed on
kind:: readiness contract, step definition, output contract, scheduling requirements, human-gate placement, runner context. - Step implementation as a tagged variant. The tag ships with only the
shell variant implemented. All four v1 variants are declared in
foreman::step::StepDef(built-in, command, child-process, agent) and the enum is#[non_exhaustive]so WASM can join later — but v1 needs exactly one of them working, because a work order does not execute anything itself. It routes to a process, and the process shells out. - The registry mechanism is engine-side and domain-neutral.
- Human-gate placement per registration: implementation keeps the PR-merge gate; deterministic duties gateless; content-bearing output always via PR.
A work order calls out; the engine never executes. Scheduling, claiming and supervising are the engine’s job; doing the work is always somebody else’s. That is what keeps the loop domain-blind — the engine cannot acquire opinions about work it only ever hands off.
Shell alone is a complete v1, which is easy to miss. The two things this project
actually needs to invoke are already commands: sdlc <noun> <verb> --output json is a machine-contract executor, and dispatching an agent is claude -p ….
So the implementation process can be registered end to end with nothing but
command steps. The dedicated agent variant earns its place later, when a
blocking subprocess is no longer enough and the run record wants a streamed
event feed — the legacy engine’s agent delegation was blocking too, so this
concedes nothing that exists today.
Keep the discriminator regardless of how few variants are implemented. Retrofitting one onto a registry that assumed a single shape is a rewrite; leaving three arms unimplemented costs nothing.
Validation is colocated with the strategy it validates. A process registration carries its own readiness contract, as code, beside the process — not in a central schema every kind must satisfy. That is the same rule from the other direction as C1’s relaxation: the universal contract loosens precisely because each process tightens its own. It also means a project-local process brings its own validation with it, via the S-0003-deterministic-general-extension-points mechanism, without the engine knowing what it checks.
E5 — flowline authoring kit
Section titled “E5 — flowline authoring kit”- Recover the spike from
origin/consolidate/workflow-library-full; land it behind the settled definitions (levels: pipe / llm / workflow / dag / durable). - Compile target is the E1 DAG format; in-code workflows lower to it at the boundary — the engine executes data, never user code.
- Bind to the ontogen-generated contract types rather than declaring its own; strip anything SDLC-shaped from the spike as it lands.
E6 — engine v0 (Rust)
Section titled “E6 — engine v0 (Rust)”- Salvage base: lift
apps/ontological-legacy/src-tauri/src/engine/(supervisor loop, worker + handler registry, session lifecycle, agent delegation, validation) out of its host app into the crate. Its SQL-store claim path is replaced by the coordination port; change the rest as freely as that demands. - Dispatcher tick: frontier → filters → claim → dispatch.
- Runner context: engine-supervised child process over the step protocol.
- Agent driver port with the Claude Code implementation.
- Corpus-writer actor (one process per serial domain).
- Trigger service v0: cron + watch predicates; skip overlapping firings, coalesce missed firings into one catch-up run on wake, no jitter.
- Run records on the JSONL journal store; coordination stays git refs.
- Read-only status API: frontier +
explain(), active leases and locks, run records, trigger schedule, process registry. Observation only. - Own lifecycle CLI (daemon + foreground), independently runnable.
E7 — SDF dashboard
Section titled “E7 — SDF dashboard”- Event-stream contract from the run-record types.
- Views: frontier with explain, leases and hard locks, runs, trigger
schedule, and the registry —
kind:→ registration record, with each run linking back to the process version that executed it.
Track C — SDLC as the first instance
Section titled “Track C — SDLC as the first instance”Independent of Track E except where noted. C1 is on the critical path to cutover: the engine cannot dispatch a real work order until work orders are the shape it expects.
C1 — work orders become minimal
Section titled “C1 — work orders become minimal”The risk model is the inverse of the intuitive one, established by a
blast-radius scout rather than assumed. Body-shape changes are corpus-safe and
CI-invisible: the task contracts use lenientBody (allowUnknown: true,
order: "none"), so extra sections are legal and order is unenforced, and
sdlc entities audit classifies every body finding as prose drift, which its
exit-1 cue deliberately excludes. Frontmatter changes are the hostile ones:
TaskSchema ends in .strict(), and frontmatter drift is in the serious set.
So the schema work goes first and the body work second.
Corpus facts the sequence rests on, re-measured at step 3: 578 task files, 68
live. All carry the birth floor (Goal, Acceptance criteria, Out of scope), so
relaxing breaks nothing. Every live file carries Today; 62 carry Approach.
18 live files carry a definition_gap.
The scout’s counts (166 live, 73 definition_gap) did not survive to steps 1–3;
the file count did. The conclusions they support — the birth floor is universal,
relaxing breaks nothing — held on re-measurement.
Two premises turned out false and are worth not re-deriving. Files to touch
does not feed scope claims yet — intersect has zero importers under
solutions/ontological/ and the lease payload has no scope field — so
relocating the table breaks no scope consumer. And kind: is landed in the
schema but entirely unexercised: no corpus file carries the key, and task next
is its only reader.
Nothing defined is deleted; the relaxation is a transition, not the end
state. Every section keeps its definition — required becomes optional, and
the typed shapes (Files to touch, the implementation-ready contract) move
intact to become the implementation process’s private contract. The end state is
more precise than today, not less: one universal contract that must fit every
kind is necessarily the loosest of them, whereas per-process contracts can be as
strict as their kind actually needs. Implementation’s may end up stricter than
today’s. The loose middle exists only between step 3 and E4.
markdown-contract is the mechanism, and it already has the right shape.
The body grammar is built from a plain Spec[] — section(), optional(),
table(), bundled by sections() into a contract(). This repo already
projects that from data: TASK_BODY is a nine-row table and taskBody(mode)
renders it per lifecycle state (birth / ready). A per-kind contract is a third
projection axis over the same table, not new machinery and not a library
change. The library needs nothing added; contract() has no compose or extend
because it does not need one when the specs are data.
That is why step 6’s cost is entirely SDLC-side dispatch — contractForType()
keyed on entity type, and three callers to route on kind — rather than
contract expression. Worth knowing before anyone proposes extending
markdown-contract for this.
Ordered so each step is independently landable and leaves the corpus valid.
- 1. Additive schema fields.
scopeand scheduling hints as optional onTaskSchema; nothing writes them yet. Bumpschema_versionto 6 with a stamp-only migration mirroringv4-to-v5. Schema before docs — theproject-check-entitieshook requires every backtickedfield_name:in the entity dir to resolve to a schema property, so doc-first fails the commit.scopemirrorsforeman::work_order::ScopeClaim(pattern set,advisory/mandatoryenforcement, strict flag);schedulingmirrorsSchedulingHintsminus its priority and retry budget. The 68 live files carry v6; closed files stay at their birth version. - 2. Collapse the four section-name lists onto one export.
TASK_BODYis the source of truth, butgap-report.ts,scan-placeholders.tsandparse-touchpoints.tseach re-spell it, and two already disagree —gap-reporthard-requiresTodaywhile the ready contract marks it optional. Pure refactor; it turns steps 3–5 into one-line edits. Landed with two columns the collapse needed: a stable machinekey(whatparse-touchpointsnames its output fields after) and aspecBearingflag (whatscan-placeholdersscans —DependenciesandDiscovery contextare narrative and stay out). A second divergence surfaced and closed: both ops accepted a bare## Problem statementfor Goal, which the contract does not. Zero corpus effect either way — no live file is missing Today and none uses the bare Goal spelling. - 3. Relax the contracts.
ApproachandFiles to touchto optional at ready; dropTodayfromgap-report’s required set. Rows stay inTASK_BODYwith their aliases and typed content — this is a required→optional flip, not a deletion, so the definitions survive to be re-tightened per kind in E4. Zero corpus breakage. The payoff was measured and the prediction was wrong; see below. - 4. Relocate
implementation-ready.mdand the typed Files-to-touch contract into the implementation process’s private contract. 88 files cite the path, not ~15 (re-counted at step 3): 58 corpus task files plus 30 elsewhere, of which only four are.ts—gap-report.ts,schema.ts,ready-contract.test.ts,ensure_ready.test.ts. The rest is prose: nine SKILL/README files, the per-skill docs, the data-model template, four planning entities. The entity-dir field-reference gate stops covering it, which is a lost gate rather than a break. - 5. Shrink
body-template.eta. It can still break creation rather than existing files:authorEntitythrows on any body finding, not just errors, so a template that drops a still-required section hard-failssdlc task create. But it does not require step 3, and never did —contractForType("task")resolves to the BIRTH contract, and the birth floor (Goal, Acceptance criteria, Out of scope) has always been permissive about Today / Approach / Files to touch. Step 3 moved the READY flags only, so it neither gated nor unblocked this. Template and contract still belong in one commit; ~13 test files assert on the rendered sections. - 6.
--kindselects the template. Per-kind contracts need a seam that does not exist —contractForType()is keyed on entity type, and three callers would have to route onkind. The contract expression is free (a third axis ontaskBody); only the dispatch is work. Ship per-kind template selection against one relaxed contract; defer per-kind contracts to E4, where the process registry exists to own them and the floor gets re-tightened per kind. - 7.
Todayresolved at dispatch. Note the existing resolver validates rows against the codebase; it does not synthesise a Today table. Synthesis lives in LLM prose today, so this is net-new deterministic code, not a relocation. Step 3’s measurement promotes this: stale citation is the whole remaining gap population.
Step 3’s payoff, measured — and the prediction was wrong. Over the 68 live
files, deterministically gapped tasks fell 57 → 54, and of the 18 carrying a
definition_gap, 16 → 15. Every missing-section gap dissolved (7 tasks → 0),
which is the whole of what relaxing a presence contract can reach.
The residue is stale citation, not missing structure. touchpoint-unresolved
(45 tasks) and claim-paths (30) dominate the gap population: rows naming paths
the codebase has since moved. No contract change touches them. C3 should be
budgeted for re-pointing citations, not for the gate demotion dissolving the
backlog, and step 7 is the step that actually reaches this residue.
C2 — work-plane lifecycle policy
Section titled “C2 — work-plane lifecycle policy”Registered recurring processes, not standing tasks (pairs with E4 and E6’s trigger service).
-
last_reviewedaging: stale, then rotten. - Rotten work orders batch into one auto-close proposal for one-shot human approval — never silent closure.
- Closed work orders archive out of the hot directory.
- Milestone rosters become derived views; hand-maintained deliverable checklists retire.
- Backlog promotion re-homes the authored body instead of forking it; the duplicate terminal accepts any entity wikilink.
C3 — corpus migration
Section titled “C3 — corpus migration”- Converge the four live schema versions (
/sdlc:entities-migrate). - Re-evaluate the
definition_gaps against the implementation process’s contract rather than the universal gate. Do not expect the demotion to dissolve them — C1 step 3 measured it (see its payoff note): relaxing the universal gate cleared every missing-section gap and moved the gapped count 57 → 54 over 68 live files. What remains is stale citation, which no contract change reaches. Budget for re-pointing citations. - Existing bodies converge by attrition, not retro-editing: new authoring follows the registry, the C2 sweep closes the tail.
C4 — the SDLC engine
Section titled “C4 — the SDLC engine”- The Rust layer wrapping the domain-blind engine: the corpus behind the work-order source, SDLC’s processes registered, its policy as filters and sorters. Rust because wrapping is the next layer of the same machine (P-0012-rust-core-adapters-as-needed).
-
sdlc task nextrebased as the policy layer over the generic frontier (pairs with E3). -
sdlc engineverbs delegate to the engine’s own CLI. - Source port reads the corpus by shelling to the
sdlcCLI — see below.
The foreman/markdown boundary is not open. Foreman is ignorant of markdown entirely. Its data model is Rust structs; its wire and storage formats are JSON — the compiled DAG, the run journal, the lease payload. Markdown is SDLC’s corpus format, an implementation detail of one work-order source, and never foreman’s modelling language.
The anti-pattern that forecloses: expressing engine-level concepts — DAG definitions, process registrations, run records — as markdown prose. That is precisely the prose-runner regression D-VSLI exists to end, and it is already decided elsewhere: registrations are versioned code, never corpus entities, and per P-0005-schema-over-prose schema-modelled does not mean corpus-resident. A foreman concept that seems to want a markdown home is signalling that it belongs to the SDLC adapter instead.
The narrower question is the seam where SDLC’s markdown corpus meets the source
port. Something must read task files and project them into the domain-neutral
WorkOrder.
Split it into validation and extraction. Validation asks “is this file well-formed?”. It is corpus hygiene, runs at authoring, commit and audit time, and stays in TypeScript on markdown-contract. Extraction asks “what work order does this file describe?”. It needs frontmatter plus a few fields.
Decided: the source port shells out to the sdlc CLI. The scan is
level-triggered once per tick, not consulted per dispatch decision, so a
subprocess per scan is cheap enough. This is the same seam as the intersect
question and gets the opposite answer, for that reason.
A Rust markdown-contract exists upstream (sksizer/markdown-contract,
crates/markdown-contract-engine) but covers only the declarative part, so it
does not remove the need for this decision. packages/rust/markdown-util also
covers the byte-exact framing half in Rust. Either could support native
extraction later if the subprocess cost ever matters.
C5 — the CLI in Rust
Section titled “C5 — the CLI in Rust”Anticipated, not decided. Recorded because it changes the shape of C4 and dissolves the engine-distribution open question.
- Rewrite the
sdlcCLI in Rust, collapsing the JS/Rust runtime boundary. - Ordering is open: it can follow cutover, or absorb C4 if it lands first.
Cutover
Section titled “Cutover”- C1 has landed — work orders are the shape the engine expects. This is the hard dependency between tracks.
- Engine dispatches one real implementation work order end to end.
- Lifecycle skills shrink to heads. The audited roster is 36 skills / 9,876
lines, of which seven skills / 4,187 lines are pure sequencing the engine
absorbs:
task-work(1465),orchestrate(879),task-close-out(478),pr-check(413),pr-respond(399) retire outright;consolidate-task-prs(295) andproject-cleanup(258) convert to registered recurring processes. The agent prompts insidetask-workstep 6 andpr-respondstep 6 survive as agent steps. - Skill-prose lint retired. Its 14 pinned slugs are the concrete checklist: backlog-triage, entities-migrate, import-planning, milestones-from-file, orchestrate, pr-check, pr-respond, project-cleanup, review-todos, spawn-task-pr, task-close-out, task-define, task-work, update-skill-doc.
- The stdout-marker convention in
skills/CLAUDE.mdretires into the step protocol’s result envelope. - Starting-state checklist fully checked; post-cutover work (network API, ingress, federation, server peer) proceeds from the ADR’s open questions.
Milestone reconciliation
Section titled “Milestone reconciliation”An audit of all 31 open milestones found twelve needing change, not the five first assumed. Left unreconciled they contradict this ledger.
| Milestone | Disposition |
|---|---|
| M-0011 graph-scheduler v0.1 | Retarget to Rust; its roster is E3’s roster. Every success criterion and deliverable names the deleted TypeScript package |
| M-VRVE Task fully fleshed out | Redirect. Its goal is a coherent universal Task surface — the shape C1 dismantles — and its runner-registry criterion puts registrations in the corpus, which E4 forbids |
| M-QXR3-claude-orchestrated-task-execution-prototype | Superseded — its whole deliverable set is the prose orchestrator the engine replaces; nothing distinct survives |
| M-0013 Conflict-aware parallel dispatch | Splits three ways: one member absorbed by E3, one redirected to E2 (drift becomes a reconciler detector, not a lint), one to E4/E6 |
| M-LCW6 Task plane split | Absorbed by E2; its expand/cutover/contract staging is overridden by the single cutover |
M-K7DP Initial product definition | Repair — an automated sweep collapsed two distinct ADR links into one duplicated wikilink |
| M-YV6R markdown-contract typed-cell wins | Repair — that same sweep rewrote a reference to markdown-contract’s own upstream D-0015, which has nothing to do with this corpus |
| M-ML68 Self-hosting SDLC entities | Survives; two lines need repair, including a dangling section anchor into a D-VSLI section that does not exist |
| M-QDQ4 Milestone & orchestration | Superseded — one criterion already shipped, the rest become C2 derived rosters and the E3 conflict filter |
| M-0007 Agent personas + verification | Redirect; body is an unfilled template, and its target (task-work) retires. Verification and rework become steps of the implementation process |
M-0001.1 Task execution consistency | Review — needs a human call. Most ACs are absorbed; only the repair-on-prepare substance survives, and it rests on the gate C1 demotes |
| M-5EEX SDF control-plane app | Redirect one bullet — SDF is a day-one consumer of the status API (E7), not a later add, and the migration-step number changed |
Also flagged, no D-VSLI conflict: M-0012 and M-XVOC both have every criterion checked and look closeable.
- Run the reconciliation as one pass, not per-milestone drive-bys.
Corpus reconciliation beyond milestones
Section titled “Corpus reconciliation beyond milestones”Audits of the rest of the planning corpus found three further classes.
- Principles. P-0004 said SDLC’s differentiation is “not the orchestrator, runtime” and left the door open to the engine being third-party — which D-VSLI closes; it now carries a named carve-out. P-0003’s target end state said “server/client system” where D-VSLI is laptop-first, peer-to-peer. Both corrected. P-0001, P-0002, P-0005, P-0006, P-0008 and P-0010 need smaller amendments (retired vocabulary, run records superseding prose post-mortems, self-hosting stopping at the adapter).
- Standards. S-0008 as written forbids D-VSLI’s component placement; S-0003’s layer 3 names the prose orchestrator as architecture; S-0006’s sequencing rules go inert for generated heads; S-0005 assumes one body shape per entity type where C1 makes it per-kind.
- Path drift, orthogonal to D-VSLI but blocking legibility. Roughly 63
references to a
plugin/tree that no longer exists, plus a third convention (apps/sdlc/), against a real root ofsolutions/ontological/. Four areapplies_to.pathsfrontmatter entries, so those standards currently govern no files at all. Twenty-one more are capabilitylocations[]entries, which break tooling rather than only prose. - Missing product entities. The engine and flowline have none, while
intersect and graph-scheduler both do — and two drivers (DR-G846, DR-QKVQ)
are attached to graph-scheduler when what they describe is a dispatcher.
The engine’s Product entity is also where its name gets decided; the ledger
still writes
packages/rust/foremanas a placeholder.
Ordering
Section titled “Ordering”E1 unblocks Track E entirely. E2–E5 are parallel once contracts exist; E6 consumes all four; E7 follows E6’s status API.
Track C needs no engine. C1–C3 are corpus and tooling work that can run start to finish beside Track E, and C1 should start early because cutover waits on it. C4 is the one Track C phase that needs Track E finished.
E3 and E4 are worth having even if the engine slipped indefinitely — they replace prose sequencing with typed modules, which is the durable win underneath the architecture.