Skip to content

B-HUT2-rethink-frontmatter-as-task-state-store

Status: closed/duplicate

Raised by sksizer on PR #69 review of T-KG6Y-task-ensure-ready-flags-spec-placeholders:

Should we fundamentally rethink how we are using frontmatter for task state tracking?

The immediate trigger was the rebase conflict captured in T-2QXZ-start-task-handles-frontmatter-rebase-cleanly — two different skills (task-ensure-ready on a feat branch and start_task.py on main) both write to the same YAML frontmatter fields (status, last_reviewed, readiness_verified_at, definition_gap), so any rebase that crosses both is structurally guaranteed to conflict. The narrow fix is to auto-merge that conflict shape; the broader question is whether frontmatter is the right state store at all.

  • Single source of truth: the task file itself is the artifact. State lives where the spec lives, so the whole thing travels together through git, PRs, and worktrees.
  • Human-readable: an operator can grep status: docs/planning/tasks/ and see project state without tooling.
  • Schema-validated: plugin/entities/task/schema.json constrains the allowed shapes, and validators/validate_frontmatter.py enforces them cheaply.
  • No external service required: no DB, no API, no sync job.
  • Concurrent writes from different branches collide. status, last_reviewed, readiness_verified_at, and definition_gap are all touched by multiple skills running at different points in the lifecycle, often on different branches. Every cross-branch write becomes a merge conflict by construction. See the start_task/verify-stamp collision above.
  • Mutable state mixed with immutable spec. The body of a task is intent (immutable-ish after readiness); the frontmatter mixes that intent with rapidly-changing state (status, timestamps). PRs that touch state look identical in diff to PRs that touch intent.
  • Per-field write authority is implicit. Today the convention is “HEAD wins for status/last_reviewed; feat wins for readiness_verified_at/definition_gap” — but that rule lives in a spawned task’s prose, not in code. The next field added (e.g. last_pr) will need its own implicit convention.
  • Frontmatter atomicity is per-file, not per-field. Two writers who touch disjoint fields still produce a textual conflict.

Possible shapes worth thinking about (not committing to)

Section titled “Possible shapes worth thinking about (not committing to)”
  • Split state out into a sidecar. docs/planning/tasks/<slug>.state.yaml holds the mutable bits (status, timestamps, definition_gap); the .md keeps only intent. Sidecars can be .gitattributes merge=ours-marked or kept on a state branch.
  • State journal instead of mutable state. Append-only docs/planning/state/<slug>.log lines (2026-05-21 status=ready), with current-state derived by replay. No two writers ever touch the same line.
  • External state store, file-as-cache. A SQLite db (or even a single YAML registry) is the source of truth; the frontmatter is refreshed from it by tooling but never authoritative. Hard conflict resolution moves out of git.
  • Field-level merge driver. Keep frontmatter, register a custom git merge driver for *.md that parses YAML and merges field-by-field with declared write authority. Smallest change to the operator model; biggest tooling commitment.
  • Status-by-directory. docs/planning/tasks/open/, .../in-progress/, .../done/status becomes a git mv rather than an edit. Solves the conflict but loses one-grep visibility and complicates wikilinks.

After T-2QXZ-start-task-handles-frontmatter-rebase-cleanly ships and we see whether the narrow auto-merge is enough in practice, or whether new field collisions keep surfacing the same class of friction. If a third concurrent-write conflict shape shows up before then, promote this to a real task.


← Back to Backlog