diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..2025e50 --- /dev/null +++ b/.cursor/mcp.json @@ -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"] + } + } +} diff --git a/apps/mcp-server/package.json b/apps/mcp-server/package.json index 471a032..042e11c 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -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" diff --git a/packages/database/package.json b/packages/database/package.json index 4a24a5a..2ad297f 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -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", diff --git a/packages/database/src/scripts/import-markdown-backlog.ts b/packages/database/src/scripts/import-markdown-backlog.ts new file mode 100644 index 0000000..914d67d --- /dev/null +++ b/packages/database/src/scripts/import-markdown-backlog.ts @@ -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); + } +})(); diff --git a/plans/Plan-agent-coordination/Epic-task-as-runnable-unit/Task-export-db-to-markdown-frontmatter.md b/plans/Plan-agent-coordination/Epic-task-as-runnable-unit/Task-export-db-to-markdown-frontmatter.md new file mode 100644 index 0000000..7b72098 --- /dev/null +++ b/plans/Plan-agent-coordination/Epic-task-as-runnable-unit/Task-export-db-to-markdown-frontmatter.md @@ -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.