Skip to content

Task state plane split — semantic corpus, execution substrate

Status: open/accepted

Task frontmatter keeps only semantic state — what the work item is and how it ended. Execution state — who has it, what phase, what was verified for this run — becomes authoritative in the lease substrate. Four semantic transitions remain as commits on main; everything else is a lease write.

  • Task conflates two roles: the semantic work item (corpus) and the execution record (runtime). The schema already declares the doctrine — status “is a workflow cache — the lease ref at refs/sdlc/tasks/<id> is the authoritative claim” — but nothing enforces it: every run lands ~4 cache commits on main.
  • This decision finishes the split by storage plane, not by minting a second entity type. Frontmatter holds semantic state; the lease payload holds execution state.
  • Four semantic transitions stay as commits on main: creation, definition movement within planning/*, promotion to open/ready, and closure. After promotion, a task’s file changes on main exactly once more — at closure.
  • readiness_verified_at is written once, ON the promotion commit — promotion IS the readiness claim. Per-run re-verification writes a gates object in the lease payload and never commits to main.
  • in-progress[/blocked] become lease phases only. The enum values stay tolerated-but-never-written through a deprecation window (schema v6 deprecates, v7 removes).
  • Migration is staged as seven tasks, expand → cutover → contract, each leaving the live orchestrator operable.

Corpus frontmatter holds facts about the work item that outlive any run. The lease substrate holds facts about the current run. Three tests separate the families; they agree on every field:

  • Durability. closed/done is true forever. in-progress is only true while someone holds the work.
  • Crash behavior. If the executing agent dies, closed/done is still correct; in-progress becomes a lie a human must notice and revert. A field that goes stale on crash is runtime state wearing corpus clothes.
  • Re-derivability. A gate stamp is recomputable by re-running the gate. A completion_note records external events nothing can re-derive. Caches belong to the substrate; unrecoverable history belongs to the corpus.

The only status changes committed to main:

  1. Creation — the file appears at planning/draft (or planning/proposed).
  2. Definition movement within planning/* — draft → proposed → backlog, and the downshift to planning/needs-definition + definition_gap. A readiness fail is semantic: it changes what work the document needs.
  3. Promotionplanning/*open/ready. One commit carries both the status flip and readiness_verified_at: promotion is the readiness claim.
  4. Closure — → closed/<reason> + completion_note + the final prs[] record.

Transitions 1–2 ride along with body edits. Transitions 3–4 are the only pure state-change commits in a task’s life. Today’s per-run traffic (verify stamp, start commit, record-PR commit, close commit) collapses to promotion and closure.

FieldPost-split home
status majors (planning/*, open/ready, closed/*)Frontmatter — semantic.
in-progress[/blocked] minorsLease phase (working / blocked). Never written to frontmatter again; frontmatter stays open/ready through the run.
readiness_verified_atFrontmatter, written once on the promotion commit. Per-run re-verification updates lease gates only.
touchpoints_verified_atSame as above. It has no TS writer today; lease gates is its first real execution-side home.
prsFrontmatter, closure-only historical record (close-commit’s idempotent append — the Obsidian affordance survives at closure). In-flight binding is lease pr_number.
relevance_noteFrontmatter — a planning-plane triage artifact. Run-time relevance notes go to lease notes.
definition_gapFrontmatter — part of the needs-definition downshift, which stays semantic.
last_reviewedFrontmatter — review recency. The start commit’s last_reviewed → today write dies with the start commit.

The two planes answer different questions, so neither shadows the other: the frontmatter stamp answers “was this ready when promoted” (dispatch reads this); lease gates answers “was this verified for THIS run” (resume and the dashboard read this).

in-progress and in-progress/blocked stay in the status enum as tolerated-but-never-written values through a deprecation window: schema v6 marks them deprecated in the doc-comment; v7 removes them once a corpus sweep confirms zero occurrences in active tasks. Hard removal in v6 would make mid-flight task files schema-invalid under the live orchestrator — every validate-before-commit gate would refuse to commit on tasks currently in flight.

The ## Post-mortem body stub moves from the start commit to close-out: close-commit appends it when absent (idempotent). Interim run narrative lives in the lease’s handoff.md. The start transition becomes write-free on main.

The mirror machinery this decision retires is the highest-traffic write path in the system:

  • ensure_ready_mutate.ts commits the verify stamp to main (docs(tasks): verify <b> implementation-ready).
  • start_task.ts commits status: in-progress + last_reviewed + the post-mortem stub to main, then transitions the lease claimed → working — the same fact written twice.
  • append_pr_url.ts commits the PR URL to prs[] on the shared main checkout (the one writer not isolated in an ephemeral worktree), then transitions the lease to awaiting-review — again twice.
  • close-commit writes the genuinely semantic closure.

Meanwhile three surfaces classify “in flight” with three different rules: task inflight (frontmatter + open PR), probe-state (frontmatter + filesystem + open PR + a commit- subject literal), and the dashboard (frontmatter + lease — the only lease-aware reader). The lease payload already carries the full execution state machine (claimed, working, blocked, awaiting-review, responding, closing) and the PR binding (pr_number). The split makes the substrate authoritative instead of mirrored.

  • Task-state commit traffic on main collapses from ~4 per run to the two semantic transitions. The commitToMainViaWorktree start/verify/record-PR paths and append_pr_url.ts are retired; the shared-checkout write anomaly disappears with it.
  • The lease payload gains a gates object (readiness_verified_at, touchpoints_verified_at). The payload schema is strict, so this is a version event: bump the payload version and fence old clients via the control plane’s minimum-client version rather than letting them fail on parse.
  • probe-state, task inflight, and task next become lease-aware readers of the local ref mirror. Resume detection moves off the main_head_is_verify_stamp commit-subject literal (fragile — any later main commit broke it) to lease-phase inspection.
  • task next excludes candidates with an active lease ref in the local mirror — network-free; a stale mirror degrades safely (under-dispatch self-heals on the next fetch; a stale-missing ref is caught by cas_create failing at claim, which remains the hard backstop against double dispatch).
  • Ensure-ready’s fail carve-out becomes lease-aware: preserve status when the task is in-progress* OR holds an active lease. Post-split a leased task reads open/ready, and the old carve-out would downshift live work.
  • Obsidian / static-viewer visibility of in-flight state is lost, accepted: the dashboard (lease-mirror sync) is the run-state surface, and prs[] still lands at closure. A site-projection lease join is a non-blocking follow-up.
  • The task-state-origin lint narrows: its state-field set shrinks to the surviving semantic fields.
  • lease task sweep’s frontmatter read of closed/* survives unchanged — closure stays a semantic commit.

Seven stages, expand → cutover → contract, scaffolded as tasks with depends_on enforcing order. Each stage leaves the system operating under the live orchestrator.

  1. Lease payload gates + version handling (expand). No writer populates gates yet; old payloads parse under new code.
  2. probe-state + inflight go lease-aware (dual-mode). Resume gate accepts lease-phase OR the legacy verify-stamp during the window. Pre-cutover, lease and frontmatter agree, so classifications are unchanged.
  3. task next lease exclusion. Redundant pre-cutover (a leased task is also in-progress); the double-dispatch guard post-cutover. The orchestrate tick’s sync step gains the refs/sdlc/* refspec so the mirror is tick-fresh.
  4. Ensure-ready plane rework. Unleased pass on planning/* produces the single promotion commit; leased runs write lease gates only; the fail carve-out and the standalone --commit path become lease-aware. Must land BEFORE stage 5 — otherwise a standalone fail against a leased task downshifts live work.
  5. Cutover. Delete the start commit and append_pr_url.ts; the PR is recorded via the lease transition’s pr_number only; close-commit gains the post-mortem stub append. From this stage on, in-progress is never written.
  6. Dashboard. Dual stale rule — (in-progress ∧ no lease) ∨ (active lease ∧ expired ∧ phase ∉ {awaiting-review, blocked}) — correct on both sides of the cutover; surface lease gates.
  7. Contract. Schema v6 (deprecations), the v5→v6 migrate transform (in-progress + active lease ⇒ open/ready; no lease ⇒ flag the ambiguous tail), lint narrowing, retire the legacy resume arm and the start / record-pr commit templates. Gated on an operational drain: no pre-cutover runs still open.

A run dispatched under the old prose mid-cutover still writes in-progress harmlessly — readers keep the frontmatter fallback until stage 7 sweeps it.

  • Re-verify of an already-open/ready, unleased task: no-op, or a flag-gated stamp-refresh commit? Lean no-op.
  • v7 timing for hard enum removal — after the stage-7 sweep confirms zero active occurrences, but no consumer forces a date.
  • Site-projection lease join (in-flight visibility for the static viewer) — spawned as a backlog-status follow-up task, not part of the seven stages.
  • apps/sdlc/lib/model/entities/task/schema.ts — the “workflow cache” doctrine on status; the stamp-at-bottom convention.
  • apps/sdlc/lib/services/lease/schemas.ts — the strict lease payload (phase enum, pr_number) that gates extends.
  • apps/sdlc/skills/task-ensure-ready/ensure_ready_mutate.ts, apps/sdlc/skills/task-work/start_task.ts, apps/sdlc/skills/task-work/append_pr_url.ts, apps/sdlc/lib/model/entities/task/ops/close-commit.ts — the four mirror writers today.
  • apps/sdlc/lib/model/entities/task/ops/probe-state.ts, ops/inflight.ts, ops/next.ts — the readers that become lease-aware.
  • apps/sdlc/lib/model/entities/task/ops/lint-state-origin.ts — the state-field set that narrows at stage 7.
  • D-VSLI-distributed-work-runner-architecture — execution records are engine event-log territory, not entities; this decision is the interim step that stops mirroring them into the corpus.
  • T-SIHV-task-state-frontmatter-commits-on-main-not-worktree-branch — the convention this decision supersedes in part (task-state commits land on main; after the split, only semantic ones do).

← Back to Decisions