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:
parent
b2aff2045b
commit
e99aa733c9
5 changed files with 146 additions and 1 deletions
9
.cursor/mcp.json
Normal file
9
.cursor/mcp.json
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"mcp": "tsx --env-file=../../.env src/index.ts",
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"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": {
|
"dependencies": {
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.38.0",
|
||||||
|
|
|
||||||
53
packages/database/src/scripts/import-markdown-backlog.ts
Normal file
53
packages/database/src/scripts/import-markdown-backlog.ts
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -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.
|
||||||
Loading…
Reference in a new issue