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",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
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