Skip to content

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.

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

The gates that blocked build work are decided; the ADR carries each in its section. Recorded here for the builder’s quick reference.

GateDecision
Contract source of truthRust-first: types defined as Rust structs, generated to TS by ontogen (already in production here, drift-gated). Flowline consumes the generated types
Domain agnosticismEngine and flowline know work scheduling only — no SDLC entity, no corpus schema, no substrate import. SDLC is one adapted instance behind the ports
Component placementpackages/rust/foreman and packages/ts/flowline — the general tiers, never under solutions/ontological/
graph-scheduler scopeBuild v0.1 in full (its whole ready roster), not a frontier subset
Scheduler languageRust, 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 modeSalvage the legacy supervisor/worker/agent engine, changed as freely as the coordination port demands
D-XKA8 / D-OIVM dispositionDelete both, surviving detail folded into D-VSLI — executed
Message protocolstdio JSON-RPC step protocol; stdout markers never formalized
Event storeAppend-only JSONL journals, one per run, under .sdlc/; embedded DB stays a port option
Work-order sourceLevel-triggered scan; ids owned by the source; in-code registrations are submission sugar
Hard-lock strictnessDeclared intersections by default; per-lock strict flag also holds unscoped work
Per-process human gatesDeclared per registration; implementation keeps the PR-merge gate; deterministic duties gateless; content-bearing output via PR
RecurrenceSkip overlapping firings; coalesce missed firings into one catch-up run on wake; no jitter; declarations in process registrations
BackendsRun 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-scheduler scaffold; re-point PR-DZTZ-graph-scheduler at the Rust location; close B-CADO.
  • Regenerate docs artifacts.

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.

ConstraintWhy
#[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 qualifiedBare idents emit as dangling TS names. The config that would fix it is unreachable — ontogen hardcodes EmitConfig::default()
No #[serde(flatten)] in contract typesSilently ignored at field level: emitted TS keeps the nesting the wire flattens away
No case-changing rename_all on an enum with struct variantsIt leaks into the variant’s field names, so the TS disagrees with the real wire. Prefer newtype variants over named payload structs
No genericsPaged<T> emits T as a dangling name and reference sites lose the instantiation — silently
Flat, globally-unique type namesCross-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 defaultOtherwise the field emits required while the wire omits it
Avoid u64 above 2^53bigint_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 the Lease payload 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 method field would sit beside an untyped params — two discriminators that can disagree. The externally tagged payload key is one discriminator that cannot disagree with itself.
  • Crate scaffolded at packages/rust/foreman with its moon.yml (layer: 'library', merge: 'replace', hand-declared dependsOn).
  • ontogen wired via build.rs emitting packages/rust/foreman/generated/; drift gate registered as foreman:codegen-drift. Since ontogen emits only the closure reachable from an api/v1 signature, the read-only status API doubles as the emission root. Where the output finally lands moves with its first consumer — packages/ts/flowline does 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, and foreman::lease::Lease is a different, domain-neutral type. It retires with the SDLC adapter in C4, not here.

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-flight prs[] append are gone, and append_pr_url.ts — the one writer that mutated the shared main checkout — is deleted with them. Per-run verification writes lease gates instead. Absorbs T-5LP4, T-IVEJ, T-S27R, T-2T0R and T-AQR9; T-5G81’s payload gates were already in place.
  • Readers went lease-aware. probe-state gained lease_present / lease_phase / lease_pr_number and recomputed resume_candidate off the lease phase — resume detection no longer hangs on a commit-subject literal at main’s HEAD, which broke whenever any later commit landed. inflight prefers lease phase over frontmatter. readiness_verified_at and main_head_is_verify_stamp survive as legacy output fields only; T-11QZ retires them with schema v6.
  • The readiness contract binds leaves. task gap-report reports parenthood as a fact and the marker carries parent=; a parent task is an organizational rollup, exempt because its readiness is its children’s (T-ARCR). Note T-C83H specifies 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.ts is 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, with observe/apply injected so convergence is testable without git. Two detectors carry remedies — frontmatter-status-drifts-from-lease (a live lease disagrees with the cache) and tasks-in-progress-without-lease (the crash-stale lie D-S30G names) — and both resolve to the same write. Everything else reports only: closure needs a completion_note no machine can invent, and an expired lease needs a steal, not a frontmatter edit. sdlc lease reconcile --apply runs 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_STUBBED is 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 the in-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 next lease exclusion) is also untouched: it is a dispatch-plane filter that belongs with E3’s filter chain. And the control plane’s min_client_version still needs its bump — the first gates writer 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-ready labels the gap-clearing commit on an already-open/ready task, so the subject says “promote” when no status moves; a clear-definition-gap action would read truthfully.
  • Re-spec the nine ready gs tasks onto Rust. Not eleven: T-N2EJ (package exports) and T-G0D2 (documentation site) are closed/superseded and T-PDDK (the TypeScript scaffold) is closed/done. All nine targeted the deleted packages/ts/graph-scheduler, so this was a re-specification rather than a path swap — subpath exports became Rust modules and Cargo features, package.json exports work disappeared, and T-ROJC’s corpus became Rust data tables. Capability bodies were re-pointed at the same time; their locations[] 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-scheduler exists at layer: 'library' on serde + serde_json, with the five v0.1 modules stubbed at their real signatures and 63 fixtures transcribed as ignored suites over Rust const data tables — no JSON corpus and no include_str!, since a conformance corpus with one implementation has nothing to conform to. cargo test -p graph-scheduler reports zero failures and zero non-ignored tests; -- --ignored --list is the remaining spec. Three sketch signatures the Rust type system rejected are corrected in their tasks: a closure StatusSource needs a newtype (a blanket Fn impl overlaps everything), ReadyOptions needs builder setters (a #[non_exhaustive] struct admits no functional update outside its crate), and both sorter constructors take the graph (downstream propagation walks descendants, and compare has 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 own fixtures/), which costs publish = false on the crate. Moon ids follow the twin precedent: the Rust crate takes bare intersect, the TS project becomes intersect-ts. Thin spot: fs parity 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: mandatory claims held as leases (TTL, heartbeat, steal-on-expired); declared intersections by default, per-lock strict flag 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.

LayerTaskv0.1
Crate scaffold, corpus, ignored suitesT-ROJC-gs-test-corpus-and-disabled-suitesyes
graph — immutable DAG coreT-JVXC-gs-dag-coreyes
status — status enum, trigger rules, StatusSourceT-CLV1-gs-status-model-and-trigger-rulesyes
ready — the push frontierT-I3QP-gs-push-readiness-frontieryes
schedule — filters, verdicts, explainT-7OJL-gs-eligibility-filters-and-explainyes
sorters — orderingT-BR6H-gs-priority-sortersyes
goal — goal-directed frontierT-8L0G-gs-goal-directed-frontierdeferred
tracker — stateful wrapperT-EJ3P-gs-stateful-trackerdeferred
pull — demand-driven resolutionT-0WNK-gs-pull-resolutiondeferred

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> emits T as a dangling name silently.
  • 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.

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

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.

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. scope and scheduling hints as optional on TaskSchema; nothing writes them yet. Bump schema_version to 6 with a stamp-only migration mirroring v4-to-v5. Schema before docs — the project-check-entities hook requires every backticked field_name: in the entity dir to resolve to a schema property, so doc-first fails the commit. scope mirrors foreman::work_order::ScopeClaim (pattern set, advisory/mandatory enforcement, strict flag); scheduling mirrors SchedulingHints minus 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_BODY is the source of truth, but gap-report.ts, scan-placeholders.ts and parse-touchpoints.ts each re-spell it, and two already disagree — gap-report hard-requires Today while 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 machine key (what parse-touchpoints names its output fields after) and a specBearing flag (what scan-placeholders scans — Dependencies and Discovery context are narrative and stay out). A second divergence surfaced and closed: both ops accepted a bare ## Problem statement for 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. Approach and Files to touch to optional at ready; drop Today from gap-report’s required set. Rows stay in TASK_BODY with 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.md and 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: authorEntity throws on any body finding, not just errors, so a template that drops a still-required section hard-fails sdlc 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. --kind selects 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 on kind. The contract expression is free (a third axis on taskBody); 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. Today resolved 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.

Registered recurring processes, not standing tasks (pairs with E4 and E6’s trigger service).

  • last_reviewed aging: 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.
  • 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.
  • 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 next rebased as the policy layer over the generic frontier (pairs with E3).
  • sdlc engine verbs delegate to the engine’s own CLI.
  • Source port reads the corpus by shelling to the sdlc CLI — 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.

Anticipated, not decided. Recorded because it changes the shape of C4 and dissolves the engine-distribution open question.

  • Rewrite the sdlc CLI in Rust, collapsing the JS/Rust runtime boundary.
  • Ordering is open: it can follow cutover, or absorb C4 if it lands first.
  • 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) and project-cleanup (258) convert to registered recurring processes. The agent prompts inside task-work step 6 and pr-respond step 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.md retires 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.

An audit of all 31 open milestones found twelve needing change, not the five first assumed. Left unreconciled they contradict this ledger.

MilestoneDisposition
M-0011 graph-scheduler v0.1Retarget to Rust; its roster is E3’s roster. Every success criterion and deliverable names the deleted TypeScript package
M-VRVE Task fully fleshed outRedirect. 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-prototypeSuperseded — its whole deliverable set is the prose orchestrator the engine replaces; nothing distinct survives
M-0013 Conflict-aware parallel dispatchSplits 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 splitAbsorbed by E2; its expand/cutover/contract staging is overridden by the single cutover
M-K7DP Initial product definitionRepair — an automated sweep collapsed two distinct ADR links into one duplicated wikilink
M-YV6R markdown-contract typed-cell winsRepair — 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 entitiesSurvives; two lines need repair, including a dangling section anchor into a D-VSLI section that does not exist
M-QDQ4 Milestone & orchestrationSuperseded — one criterion already shipped, the rest become C2 derived rosters and the E3 conflict filter
M-0007 Agent personas + verificationRedirect; 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 consistencyReview — 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 appRedirect 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.

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 of solutions/ontological/. Four are applies_to.paths frontmatter entries, so those standards currently govern no files at all. Twenty-one more are capability locations[] 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/foreman as a placeholder.

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.


← Back to Notes