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:
- 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.
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
What happened
A task file created by hand (no
task_identifierin frontmatter) was passed to:Afterwards the file's frontmatter contained two
task_identifierkeys: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: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
awkscan across24 Tasks/*.md. Hand-removing the duplicate key restored normal operation.Root cause — not established, two candidates
serializeMapAsFrontmatter(pkg/storage/base.go:195) marshals amap[string]any, which cannot itself hold duplicate keys, so the duplication does not originate there. Two plausible mechanisms, neither confirmed:ensureAllTaskIdentifiersOperation(pkg/ops/ensure_task_identifiers.go:65) andWriteTask(pkg/storage/task.go:45) both generate a UUID whenTaskIdentifier() == "". 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.frontmatterRegexpartial match.serializeMapAsFrontmattertakesbody = matches[2]; if the regex mis-matches on some input, retained frontmatter could reappear in the body.Suggested fixes
ListTasks' existing behaviour (pkg/storage/task.gousesslog.Debug("skipping unreadable task")).vault-cli lintrule for duplicate frontmatter keys —lint.goalready checks missing/invalidtask_identifier, so duplicate is a natural sibling.Environment