Connect Echodo to itself: Cursor sessions can now call this repo's MCP server (`claim_task`, `complete_task`) over stdio, and operators can bootstrap the DB from `plans/` without leaving a long-running file watcher in place. - `.cursor/mcp.json`: register `echodo` MCP server. Spawns `pnpm -s --filter @tasks/mcp-server mcp`. The `mcp` script invokes tsx with `--env-file=../../.env` so DATABASE_URL is picked up at the per-session process boundary without leaking into committed config. - `import:markdown-backlog`: new one-shot importer (sibling of the existing watch script). Until the DB → markdown export side lands (see follow-up task), running the watcher continuously would clobber agent-driven status flips on every sweep. The one-shot variant runs a single `syncMarkdownBacklogScan` pass and exits. - File `Task-export-db-to-markdown-frontmatter.md` documenting the remaining direction of the sync loop. Smoke-tested end-to-end against the homelab DB: full `claim_task` → `complete_task` round-trip via real MCP stdio protocol, with agent_runs + audit_log rows landing as expected and the backlog item status restored via `finalStatus`. Co-authored-by: Cursor <cursoragent@cursor.com>
5 KiB
| kind | slug | title | plan_slug | epic_slug | status | priority | tenant_id | owner | cursor_todo_id | updated_at |
|---|---|---|---|---|---|---|---|---|---|---|
| task | export-db-to-markdown-frontmatter | Export DB state back to markdown frontmatter (close the sync loop) | agent-coordination | task-as-runnable-unit | ready | P1 | global | unassigned | null | 2026-06-03 |
Task summary
The markdown importer at packages/database/src/markdown-backlog/sync.ts is one-way: it reads plans/**/*.md and upserts into markdown_backlog_items. There is no path back from the DB to the markdown frontmatter. As soon as an agent calls complete_task (status → done), running the importer would clobber that transition back to whatever the markdown still says.
For now we paper over this by running the importer once at bootstrap and ad-hoc thereafter (pnpm --filter @tasks/database import:markdown-backlog). That's tolerable for a single-operator dogfood loop, but it doesn't scale and it's a correctness bug: any change made through the app — status flips, prompt edits, new tasks created in the UI — is at risk of being silently reverted.
This task closes that loop by adding a DB → markdown export.
Description
Scope
Implement exportBacklogItemToMarkdown(db, { workspaceId, backlogItemId }) that:
- Loads the
markdown_backlog_itemsrow. - Locates the corresponding
.mdfile on disk usingrepo_relative_path. - Reads the existing file (must preserve the body verbatim).
- Rewrites the YAML frontmatter so the canonical fields match the DB row:
statusprioritytitle(only when changed; titles are usually human-authored)updated_at(bumped to today on any rewrite)workflow_prompt→ frontmatteragent_prompt:(omit if null; serialize as a block scalar|if multi-line)
- Preserves every other frontmatter key untouched, in the original order.
- Writes the file back atomically (write to tmp, rename) to avoid half-written reads from any watcher.
Wire it into:
complete_task(after the status transition commits): export the touched row.claim_task: optional — probably yes when status flipped toin_progress.backlog.updateWorkflowPrompttRPC mutation (apps/web/server/routers/backlog.ts).- Maybe
workspaces.archivecascade for backlog items (lower priority — archive is a UI-driven verb and the markdown may not need to reflect it).
Out of scope:
- Creating brand-new
.mdfiles from DB rows (we never UI-create backlog items today; punt to a separate task). - Two-way conflict resolution. If someone edits both the file and the DB row between importer runs we just take the DB. Document this clearly in
config/CursorSync.md.
Implementation notes
- Use a small, well-tested YAML rewriter, not a regex. The
yamlpackage (already a dep of@tasks/database) supports preserving comments and key order viaDocumentparsing. UseDocument.parse(source), mutate scalar values in place, andString(doc)to serialize. - File I/O lives in
packages/database/src/markdown-backlog/export.ts. Keep this module free of tRPC / Next imports so the MCP server can call it directly. - Add Vitest cases covering:
- Round-trip: read → no change → write produces identical bytes.
- Status update:
ready→doneonly mutatesstatusandupdated_at. - Setting
agent_prompt: nullremoves the key (does not writeagent_prompt: null). - Block-scalar serialization for multi-line prompts.
- Preserves trailing newline and body separator (
---\n\nvs---\n).
- Behind a workspace-level setting
markdown_export_enabled(defaulttrue). Some operators may not want the app rewriting theirplans/tree; let them opt out.
Acceptance
- After calling
complete_taskwithfinalStatus: done, the corresponding.mdfile's frontmatter status readsdoneon disk. - Running
pnpm --filter @tasks/database import:markdown-backlogimmediately after produces zero new upserts (the file already matches the DB). - Vitest coverage of the export helper.
config/CursorSync.mdis updated to describe the new two-way contract and the conflict policy (DB wins on conflict between importer runs).- No diff noise: re-exporting an unchanged row leaves the file byte-identical.
Risks
- YAML formatting is finicky; agents may produce huge accidental diffs the first time this runs. Mitigate with the round-trip test and an
--dry-runflag on the export helper for first verification. - Concurrent writes: an operator editing the
.mdin their editor at the moment of export could lose changes. The atomic write helps but doesn't fully solve it. Document the contract: while agents are running, treat the.mdfiles as derived state.