chore(agent-coordination): wire Cursor MCP + one-shot importer for dogfooding

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>
This commit is contained in:
Randall Stillwell 2026-06-03 08:57:21 -05:00
parent b2aff2045b
commit e99aa733c9
5 changed files with 146 additions and 1 deletions

9
.cursor/mcp.json Normal file
View file

@ -0,0 +1,9 @@
{
"_comment": "Echodo's own MCP server. Spawned per Cursor session over stdio. The 'mcp' script in apps/mcp-server/package.json invokes tsx with --env-file=../../.env so DATABASE_URL is read from the repo's .env without leaking it into this committed file. The -s/--silent flag on pnpm suppresses its progress output that would otherwise corrupt the MCP stdio protocol. If you fork the repo, copy .env.example → .env and fill in DATABASE_URL.",
"mcpServers": {
"echodo": {
"command": "pnpm",
"args": ["-s", "--filter", "@tasks/mcp-server", "mcp"]
}
}
}

View file

@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"mcp": "tsx --env-file=../../.env src/index.ts",
"build": "tsup",
"start": "node dist/index.js",
"type-check": "tsc --noEmit"

View file

@ -19,7 +19,8 @@
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"watch:markdown-backlog": "tsx src/scripts/watch-markdown-backlog.ts"
"watch:markdown-backlog": "tsx src/scripts/watch-markdown-backlog.ts",
"import:markdown-backlog": "tsx src/scripts/import-markdown-backlog.ts"
},
"dependencies": {
"drizzle-orm": "^0.38.0",

View file

@ -0,0 +1,53 @@
/**
* One-shot markdown backlog import.
*
* Sibling of `watch-markdown-backlog.ts` but runs ONE sweep and exits. Use
* this when you want to bootstrap or refresh the DB from `plans/**` without
* leaving a long-running file watcher in place particularly important
* during agent sessions, because the watcher's upsert overwrites
* `markdown_backlog_items.status` from frontmatter on every sweep and would
* clobber `claim_task` / `complete_task` state.
*
* Until the DB markdown export side ships (see follow-up task), the
* convention is:
* - Edit `.md` files to author / refine intent.
* - Run this script when you want the changes in the DB.
* - Let agents drive runtime status via MCP tools BETWEEN syncs.
*
* Env:
* - DATABASE_URL (required)
* - MARKDOWN_BACKLOG_WORKSPACE_ID UUID of the target workspace (required)
* - MARKDOWN_BACKLOG_REPO_ROOT absolute path to repo root (default: cwd)
*/
import { resolve } from "node:path";
import { db } from "../client";
import { syncMarkdownBacklogScan } from "../markdown-backlog/sync";
const workspaceId = process.env.MARKDOWN_BACKLOG_WORKSPACE_ID?.trim();
const repoRootAbs = resolve(
process.env.MARKDOWN_BACKLOG_REPO_ROOT?.trim() || process.cwd(),
);
if (!workspaceId) {
console.error(
"[import-markdown-backlog] Set MARKDOWN_BACKLOG_WORKSPACE_ID to your workspace UUID.",
);
process.exit(1);
}
void (async () => {
try {
const result = await syncMarkdownBacklogScan(db, {
workspaceId,
repoRootAbs,
});
console.log(
`[import-markdown-backlog] done — scanned ${result.scannedFiles} files, upserted ${result.upsertedRows} rows, deleted ${result.deletedRows} stale rows. Repo root: ${repoRootAbs}`,
);
process.exit(0);
} catch (e) {
console.error("[import-markdown-backlog] sync failed:", e);
process.exit(1);
}
})();

View file

@ -0,0 +1,81 @@
---
kind: task
slug: export-db-to-markdown-frontmatter
title: Export DB state back to markdown frontmatter (close the sync loop)
plan_slug: agent-coordination
epic_slug: task-as-runnable-unit
status: ready
priority: P1
tenant_id: global
owner: unassigned
cursor_todo_id: null
updated_at: "2026-06-03"
# agent_prompt: |
# Treat this task as a correctness-critical change to the sync layer. The
# importer writes one direction (md → DB); after this task the system MUST
# also write the other direction (DB → md frontmatter) so MCP-driven status
# transitions survive the next importer pass. Be careful with diff noise,
# trailing newlines, and YAML key ordering — the markdown files are
# human-authored.
---
# 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:
1. Loads the `markdown_backlog_items` row.
2. Locates the corresponding `.md` file on disk using `repo_relative_path`.
3. Reads the existing file (must preserve the body verbatim).
4. Rewrites the YAML frontmatter so the canonical fields match the DB row:
- `status`
- `priority`
- `title` (only when changed; titles are usually human-authored)
- `updated_at` (bumped to today on any rewrite)
- `workflow_prompt` → frontmatter `agent_prompt:` (omit if null; serialize as a block scalar `|` if multi-line)
5. Preserves every other frontmatter key untouched, in the original order.
6. 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 to `in_progress`.
- `backlog.updateWorkflowPrompt` tRPC mutation (`apps/web/server/routers/backlog.ts`).
- Maybe `workspaces.archive` cascade 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 `.md` files 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 `yaml` package (already a dep of `@tasks/database`) supports preserving comments and key order via `Document` parsing. Use `Document.parse(source)`, mutate scalar values in place, and `String(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``done` only mutates `status` and `updated_at`.
- Setting `agent_prompt: null` removes the key (does not write `agent_prompt: null`).
- Block-scalar serialization for multi-line prompts.
- Preserves trailing newline and body separator (`---\n\n` vs `---\n`).
- Behind a workspace-level setting `markdown_export_enabled` (default `true`). Some operators may not want the app rewriting their `plans/` tree; let them opt out.
### Acceptance
1. After calling `complete_task` with `finalStatus: done`, the corresponding `.md` file's frontmatter status reads `done` on disk.
2. Running `pnpm --filter @tasks/database import:markdown-backlog` immediately after produces zero new upserts (the file already matches the DB).
3. Vitest coverage of the export helper.
4. `config/CursorSync.md` is updated to describe the new two-way contract and the conflict policy (DB wins on conflict between importer runs).
5. 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-run` flag on the export helper for first verification.
- Concurrent writes: an operator editing the `.md` in 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 `.md` files as derived state.