Skip to content

task set / write path can emit duplicate task_identifier key, breaking frontmatter parse vault-wide #75

Description

@bborbe

What happened

A task file created by hand (no task_identifier in frontmatter) was passed to:

vault-cli task set "<name>" phase execution

Afterwards the file's frontmatter contained two task_identifier keys:

---
task_identifier: 1c36b1fc-7ad1-4e07-9643-20902f3a822c
assignee: bborbe
category: tooling
page_type: task
phase: execution
priority: 3
status: in_progress
task_identifier: 6a302733-20ca-43c5-98d1-a5345fd49938
themes:
    - '[[Automated Trading Profitability]]'
---

Note the first key is at the top, outside the otherwise alphabetical ordering — the second sits in sorted position.

Why it matters — one bad file breaks every task command

task complete (and anything else that scans tasks) then fails for all tasks in the vault, not just the corrupted one:

Error: find task: parse frontmatter: unmarshal yaml frontmatter: yaml: unmarshal errors:
  line 8: mapping key "task_identifier" already defined at line 1

The error names neither the offending file nor the vault path, so from the operator's side a working command simply stops working with no indication of where to look. I found the file with an awk scan across 24 Tasks/*.md. Hand-removing the duplicate key restored normal operation.

Root cause — not established, two candidates

serializeMapAsFrontmatter (pkg/storage/base.go:195) marshals a map[string]any, which cannot itself hold duplicate keys, so the duplication does not originate there. Two plausible mechanisms, neither confirmed:

  1. Race between two writers. ensureAllTaskIdentifiersOperation (pkg/ops/ensure_task_identifiers.go:65) and WriteTask (pkg/storage/task.go:45) both generate a UUID when TaskIdentifier() == "". If one reads the task before the other's write lands, both generate and both write. The observed positions (one prepended, one in sorted position) are consistent with two independent writes.
  2. frontmatterRegex partial match. serializeMapAsFrontmatter takes body = matches[2]; if the regex mis-matches on some input, retained frontmatter could reappear in the body.

Suggested fixes

  • Make the parse failure locatable. Wrap the unmarshal error with the file path. A vault-wide breakage that names no file is the expensive part of this bug, independent of the cause.
  • Skip-and-warn rather than fail-all on an unparseable task during scans, matching ListTasks' existing behaviour (pkg/storage/task.go uses slog.Debug("skipping unreadable task")).
  • Make identifier assignment idempotent under concurrency, or serialise it.
  • Consider a vault-cli lint rule for duplicate frontmatter keys — lint.go already checks missing/invalid task_identifier, so duplicate is a natural sibling.

Environment

  • macOS 26.6.1, vault: Personal
  • Reproduced once, 2026-08-10; not yet reduced to a deterministic repro

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions