B-HUT2-rethink-frontmatter-as-task-state-store
Status: closed/duplicate
Origin
Section titled “Origin”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.
Why frontmatter is load-bearing today
Section titled “Why frontmatter is load-bearing today”- 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.jsonconstrains the allowed shapes, andvalidators/validate_frontmatter.pyenforces them cheaply. - No external service required: no DB, no API, no sync job.
Where frontmatter-as-state hurts
Section titled “Where frontmatter-as-state hurts”- Concurrent writes from different branches collide.
status,last_reviewed,readiness_verified_at, anddefinition_gapare 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.yamlholds the mutable bits (status, timestamps, definition_gap); the.mdkeeps 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>.loglines (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
*.mdthat 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/—statusbecomes agit mvrather than an edit. Solves the conflict but loses one-grep visibility and complicates wikilinks.
When to revisit
Section titled “When to revisit”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.