# ECHO plugin — improvements & fixes (frontmatter completeness) ## Context While doing routine vault edits I found that partially-populated entity notes accumulate silently. An audit of `resources/people|companies|references` found **18 of 71 notes** missing `status` and/or with empty `tags` (`tags: []` or no key). Neither `echo doctor` nor `vault_lint.py` (`/echo-health`) catches this. I hand-fixed the vault, but the tooling should prevent the drift and make it easy to remediate. Three changes, root-cause first. ## Root cause — `capture` writes incomplete canonical frontmatter New entity notes created via `capture` land with `tags: []` and **no `status` key** (verified on freshly-captured `reference` and `company` notes). Every such note is then "incomplete" by convention (complete siblings like `resources/references/obsidian-local-rest-api.md` carry `status: active` + real tags). So the drift is created at write time. **Fix (in `capture` / `_build_note`, `echo_ops.py`):** - Stamp a kind-appropriate default `status` (e.g. `active` for person/company/reference/project) instead of omitting it. - Replace the empty `tags: []` scaffold with either (a) an auto-seeded baseline tag (the note's `kind`, e.g. `- company`) so the key is never empty, or (b) a required `--tags` argument for entity kinds, or (c) a `needs-tags` marker tag that the completeness check (below) flags. Pick one; my preference is (a)+(b): seed `- ` and accept `--tags a,b,c` to enrich at capture time. - Acceptance: a note created by `capture "X" --kind company` passes the completeness check below with zero follow-up edits. ## Fix 2 — `fm` cannot create a missing key (only replace) `echo.py: cmd_fm()` is `cmd_patch(path, "replace", "frontmatter", field, ...)`. If the field is absent, the Obsidian REST API returns `400 invalid-target` ("The patch you provided could not be applied"). This means `echo fm note.md status active` **fails on exactly the notes that need it** (status missing), forcing agents into GET-modify-PUT, which is error-prone (I clobbered `tags` on 2 notes with a naive rewrite before catching it). **Fix:** make `fm` create-or-replace. On `invalid-target`, fall back to inserting the key into the frontmatter block (after `type:`, preserving all other lines) and re-PUT — or add an explicit `fm --create` / `set` op. Must be surgical: never rewrite unrelated frontmatter or body. - Acceptance: `echo fm note.md status active` succeeds whether or not `status` already exists, changing nothing else in the file. ## Fix 3 — `vault_lint.py`: add an `incomplete-frontmatter` check Today `REQUIRED_FM = ("type", "created")` only, and `status` is validated solely for projects (folder↔status match). Nothing requires `status`/`tags` on entity notes. **Fix:** add a check that, for entity kinds (`person`, `company`, `reference`, and any others that should be classified — `area`, `skill`), flags: - missing or empty `status` - missing `tags` key, or `tags: []`, or a `tags:` key with no list items Emit as a new check id `incomplete-frontmatter` with a message like `"{path}: missing status"` / `"{path}: empty tags"`. Keep it kind-scoped so episodic/journal/session notes (which legitimately omit these) are not flagged. Consider a per-kind required-field map rather than the single global `REQUIRED_FM` tuple, so the rule is data-driven. - Acceptance: running `vault_lint.py` on a vault with a `tags: []` company note reports one `incomplete-frontmatter` violation; a fully-populated note reports none. ## Nice-to-have - A `--fix` / remediation mode (or a `sweep` sub-pass) that backfills `status: ` and a `- ` baseline tag on flagged notes, so cleanup is one command instead of a hand-rolled script. - Note the zsh gotcha for any shipped helper loops: unquoted scalar `for x in $var` does NOT word-split in zsh — use arrays or `while read`. ## Evidence / where I looked - `echo.py` `cmd_fm` (~L405), arg parser (~L673), `cmd_patch`. - `vault_lint.py` `REQUIRED_FM` (L42), per-note loop (~L161–173), check-label registry (~L301+), `walk()`/`list_dir()` enumeration. - `echo_ops.py` `_build_note` (~L103) — has a `status_v` param already, so capture is positioned to stamp status. - Entity index `_agent/index/entities.json` stores `path/kind/title/aliases/last_seen` — not `tags`/`status` (so a completeness check must read notes, as `vault_lint` already does).