- docs/history/echo-improvements-prompt.md (ROADMAP-2.0 §5 box ticked) - README repository layout brought to 1.5.1 reality: root docs (CHANGELOG/TODO-1.6/ROADMAP-2.0/docs/history), build.py + dist/ (gitignored, secret-bearing), current eval harness contents, hooks/, and the full scripts/ roster (recall/reflect/triage/queue/ concurrency/quality/output/doctor/hook modules) with the legacy commands/ dir annotated as the TODO-1.6 migration target. Also restores the eval-descriptor fix lost in the prior commit (edit was applied in memory but never written back). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.3 KiB
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.activefor person/company/reference/project) instead of omitting it. - Replace the empty
tags: []scaffold with either (a) an auto-seeded baseline tag (the note'skind, e.g.- company) so the key is never empty, or (b) a required--tagsargument for entity kinds, or (c) aneeds-tagsmarker tag that the completeness check (below) flags. Pick one; my preference is (a)+(b): seed- <kind>and accept--tags a,b,cto enrich at capture time. - Acceptance: a note created by
capture "X" --kind companypasses 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 activesucceeds whether or notstatusalready 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
tagskey, ortags: [], or atags: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.pyon a vault with atags: []company note reports oneincomplete-frontmatterviolation; a fully-populated note reports none.
Nice-to-have
- A
--fix/ remediation mode (or asweepsub-pass) that backfillsstatus: <default>and a- <kind>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 $vardoes NOT word-split in zsh — use arrays orwhile read.
Evidence / where I looked
echo.pycmd_fm(~L405), arg parser (~L673),cmd_patch.vault_lint.pyREQUIRED_FM(L42), per-note loop (~L161–173), check-label registry (~L301+),walk()/list_dir()enumeration.echo_ops.py_build_note(~L103) — has astatus_vparam already, so capture is positioned to stamp status.- Entity index
_agent/index/entities.jsonstorespath/kind/title/aliases/last_seen— nottags/status(so a completeness check must read notes, asvault_lintalready does).